Skip to content

Commit

Permalink
Features/#277 create missing env file (#445)
Browse files Browse the repository at this point in the history
fix: Intention to create env file when it's missing #277

Closes: #277
  • Loading branch information
dakochik authored Jan 15, 2022
1 parent 1136ac5 commit 428c589
Show file tree
Hide file tree
Showing 9 changed files with 360 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Released on ...
- TODO (see [#NNN](https://github.com/JetBrains-Research/snakecharm/issues/NNN))

### Added
- Quick fix for unresolved files (conda, configfile. etc.) (see [#277](https://github.com/JetBrains-Research/snakecharm/issues/277))
- Warning: Snakemake support is disabled (see [#109](https://github.com/JetBrains-Research/snakecharm/issues/109))
- TODO (see [#NNN](https://github.com/JetBrains-Research/snakecharm/issues/NNN))

Expand Down
11 changes: 7 additions & 4 deletions src/main/kotlin/com/jetbrains/snakecharm/SmkNotifier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ object SmkNotifier {
}).notify(module.project)
}

fun notify(content: String, type: NotificationType = NotificationType.INFORMATION, project: Project? = null) =
fun notify(
title: String = "",
content: String,
type: NotificationType = NotificationType.INFORMATION,
project: Project? = null
) =
NotificationGroupManager.getInstance().getNotificationGroup(NOTIFICATION_GROUP_ID)
.createNotification(content, type).also {
it.notify(project)
}
.createNotification(title, content, type).notify(project)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.jetbrains.snakecharm.inspections

import com.intellij.codeInspection.LocalQuickFix
import com.intellij.psi.PsiReference
import com.jetbrains.python.inspections.PyUnresolvedReferenceQuickFixProvider
import com.jetbrains.snakecharm.inspections.quickfix.CreateMissedFileQuickFix
import com.jetbrains.snakecharm.inspections.quickfix.CreateMissedFileUndoableAction
import com.jetbrains.snakecharm.lang.psi.SmkArgsSection
import com.jetbrains.snakecharm.lang.psi.SmkFileReference

class SmkUnresolvedReferenceInspectionExtension : PyUnresolvedReferenceQuickFixProvider {

override fun registerQuickFixes(reference: PsiReference, existing: MutableList<LocalQuickFix>) {
val section = reference.element as? SmkArgsSection ?: return
val sectionName = section.sectionKeyword ?: return
if (CreateMissedFileUndoableAction.sectionToDefaultFileContent.containsKey(sectionName)) {
val fileReference = (section.reference as? SmkFileReference) ?: return
if (!fileReference.hasAppropriateSuffix()) {
return
}
val name = fileReference.path
existing.add(CreateMissedFileQuickFix(section,
name,
sectionName,
fileReference.searchRelativelyToCurrentFolder))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.jetbrains.snakecharm.inspections.quickfix

import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement
import com.intellij.notification.NotificationType
import com.intellij.openapi.command.undo.UndoManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.jetbrains.snakecharm.SmkNotifier
import com.jetbrains.snakecharm.SnakemakeBundle
import java.nio.file.InvalidPathException
import java.nio.file.Paths
import kotlin.io.path.Path

class CreateMissedFileQuickFix(
element: PsiElement,
private val fileName: String,
private val sectionName: String,
private val searchRelativelyToCurrentFolder: Boolean,
) : LocalQuickFixAndIntentionActionOnPsiElement(element) {
companion object {
private val LOGGER = Logger.getInstance(CreateMissedFileQuickFix::class.java)
}

override fun getFamilyName() = SnakemakeBundle.message("INSP.NAME.conda.env.missing.fix", fileName)

override fun getText() = familyName

override fun invoke(
project: Project,
file: PsiFile,
editor: Editor?,
startElement: PsiElement,
endElement: PsiElement,
) {
val vFile = file.virtualFile ?: return

val fileIndex = ProjectRootManager.getInstance(project).fileIndex
val relativeDirectory = when {
searchRelativelyToCurrentFolder -> vFile.parent
else -> fileIndex.getContentRootForFile(vFile)
} ?: return

val fileToCreatePath = try {
when {
Path(fileName).isAbsolute -> Path(fileName)
else -> Paths.get(relativeDirectory.path, fileName)
}
} catch (e: InvalidPathException) {
LOGGER.error(e)
SmkNotifier.notify(
title = SnakemakeBundle.message("notifier.msg.create.env.file.title"),
content = SnakemakeBundle.message("notifier.msg.create.env.file.name.invalid.file.exception", fileName),
type = NotificationType.ERROR,
project = project
)
return
}
val action = CreateMissedFileUndoableAction(file, fileToCreatePath, sectionName)
action.redo()
UndoManager.getInstance(project).undoableActionPerformed(action)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package com.jetbrains.snakecharm.inspections.quickfix

import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.notification.NotificationType
import com.intellij.openapi.command.undo.DocumentReference
import com.intellij.openapi.command.undo.DocumentReferenceManager
import com.intellij.openapi.command.undo.UndoableAction
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiFile
import com.jetbrains.snakecharm.SmkNotifier
import com.jetbrains.snakecharm.SnakemakeBundle
import com.jetbrains.snakecharm.lang.SnakemakeNames
import org.apache.commons.io.FileUtils
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.appendText
import kotlin.io.path.isDirectory
import kotlin.io.path.name
import kotlin.io.path.notExists

class CreateMissedFileUndoableAction(
private val actionInvocationTarget: PsiFile,
private val fileToCreatePath: Path,
private val sectionName: String,
) : UndoableAction {

companion object {
private val LOGGER = Logger.getInstance(CreateMissedFileUndoableAction::class.java)
private val condaDefaultContent = """
channels:
dependencies:
""".trimIndent()

// Update it if wee need default text
val sectionToDefaultFileContent = mapOf(
SnakemakeNames.SECTION_CONDA to condaDefaultContent,
SnakemakeNames.SECTION_NOTEBOOK to null,
SnakemakeNames.SECTION_SCRIPT to null,
SnakemakeNames.MODULE_SNAKEFILE_KEYWORD to null,

SnakemakeNames.WORKFLOW_CONFIGFILE_KEYWORD to null,
SnakemakeNames.WORKFLOW_PEPFILE_KEYWORD to null,
SnakemakeNames.WORKFLOW_PEPSCHEMA_KEYWORD to null
)
}

private val actionInvocationTargetVFile = actionInvocationTarget.virtualFile!!
private val project = actionInvocationTarget.project
private val firstCreatedFileOrDir: Path
private val firstCreatedFileOrDirParent: VirtualFile?

init {
// Memorize first directory that will be created & delete it on "undo" step
var fileOrDirToCreate = fileToCreatePath
while (fileOrDirToCreate.parent.notExists()) {
fileOrDirToCreate = fileOrDirToCreate.parent ?: break
}
firstCreatedFileOrDir = fileOrDirToCreate

val parent = fileOrDirToCreate.parent
firstCreatedFileOrDirParent = when (parent) {
null -> null
else -> VirtualFileManager.getInstance().findFileByNioPath(parent)
}
}

override fun undo() {
// invoke in such way because:
// (1) changing document inside undo/redo is not allowed (see ChangeFileEncodingAction)
// (2) we don't want to use UI thread, e.g. if disk IO operation will be slow due to NFS, or etc.
ProgressManager.getInstance().run(object : Task.Backgroundable(
project,
SnakemakeBundle.message("notifier.msg.deleting.env.file", fileToCreatePath.name),
false
) {
override fun run(indicator: ProgressIndicator) {
doUndo()
// XXX: Sometimes VFS refresh in onSuccess() called to early and doesn't see FS changes
Thread.sleep(1000)
}

override fun onSuccess() {
// Here is AWT thread!
refreshFS()
}
})
}

override fun redo() {
// invoke in such way because:
// (1) changing document inside undo/redo is not allowed (see ChangeFileEncodingAction)
// (2) we don't want to use UI thread, e.g. if disk IO operation will be slow due to NFS, or etc.
ProgressManager.getInstance().run(object : Task.Backgroundable(
project,
SnakemakeBundle.message("notifier.msg.creating.env.file", fileToCreatePath.name),
false
) {
override fun run(indicator: ProgressIndicator) {
doRedo(fileToCreatePath, project)
// XXX: Sometimes VFS refresh in onSuccess() called to early and doesn't see FS changes
Thread.sleep(1000)
}

override fun onSuccess() {
// Here is AWT thread!
refreshFS()
}
})
}

private fun refreshFS() {
firstCreatedFileOrDirParent?.refresh(true, true) {
// Post action is called in AWT thread:
DaemonCodeAnalyzer.getInstance(project).restart(actionInvocationTarget)
}
}

override fun getAffectedDocuments(): Array<DocumentReference> {
// * If not affected files - action not in undo/redo
// * If 'all' affected files - action undo doesn't work

// So:mark only the current editor file as affected to make feature convenient.
// Otherwise, new file opening will be regarded as a new action
// and 'undone' will be banned
return arrayOf(DocumentReferenceManager.getInstance().create(actionInvocationTargetVFile))
}

override fun isGlobal() = true

private fun doRedo(
fileToCreate: Path,
project: Project,
) {
try {
// file could be created in background by someone else or if action triggred twice
if (fileToCreate.notExists()) {
Files.createDirectories(fileToCreate.parent)
val targetFile = Files.createFile(fileToCreate)

val context = sectionToDefaultFileContent[sectionName]
if (context != null) {
// We don't use the result of 'createChildFile()' because it has inappropriate
// type (and throws UnsupportedOperationException)
targetFile.appendText(context)
}
}
} catch (e: SecurityException) {
val message = e.message ?: "Error: ${e.javaClass.name}"
SmkNotifier.notify(
title = SnakemakeBundle.message("notifier.msg.create.env.file.title"),
content = message,
type = NotificationType.ERROR,
project = project
)
} catch (e: IOException) {
LOGGER.warn(e)
SmkNotifier.notify(
title = SnakemakeBundle.message("notifier.msg.create.env.file.title"),
content = SnakemakeBundle.message(
"notifier.msg.create.env.file.io.exception", fileToCreate.name
),
type = NotificationType.ERROR,
project = project
)
}
}

private fun doUndo() {
if (firstCreatedFileOrDir.notExists()) {
return
}

when {
firstCreatedFileOrDir.isDirectory() -> FileUtils.deleteDirectory(
firstCreatedFileOrDir.toFile()
)
else -> Files.delete(firstCreatedFileOrDir)
}
}
}
Loading

0 comments on commit 428c589

Please sign in to comment.