diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e19af4..ebde4b8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,16 @@ Released on ... ### Fixed - Resolve/completion for checkpoints after `rules` keyword (see [#262](https://github.com/JetBrains-Research/snakecharm/issues/262)) +- Resolve for rule names in `use rule` section (see [#455](https://github.com/JetBrains-Research/snakecharm/issues/455)) +- Multiple args inspection in `workdir` case (see [#140](https://github.com/JetBrains-Research/snakecharm/issues/140)) +- `localrules` and `ruleorder` now take into account `use rule` (see [#448](https://github.com/JetBrains-Research/snakecharm/issues/448)) +- Keyword arguments highlighting (see [#454](https://github.com/JetBrains-Research/snakecharm/issues/454)) - Resolve for `rules` keyword if `snakemake` version less than `6.1` (see [#359](https://github.com/JetBrains-Research/snakecharm/issues/359)) - TODO (see [#NNN](https://github.com/JetBrains-Research/snakecharm/issues/NNN)) ### Added +- Inspection: warns that that all docstrings except the first one will be ignored (see [#341](https://github.com/JetBrains-Research/snakecharm/issues/341)) +- 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)) diff --git a/src/main/kotlin/com/jetbrains/snakecharm/SmkNotifier.kt b/src/main/kotlin/com/jetbrains/snakecharm/SmkNotifier.kt index 53ec0bc9..5536f0f2 100644 --- a/src/main/kotlin/com/jetbrains/snakecharm/SmkNotifier.kt +++ b/src/main/kotlin/com/jetbrains/snakecharm/SmkNotifier.kt @@ -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) } \ No newline at end of file diff --git a/src/main/kotlin/com/jetbrains/snakecharm/codeInsight/SnakemakeAPI.kt b/src/main/kotlin/com/jetbrains/snakecharm/codeInsight/SnakemakeAPI.kt index 21f76c6b..b14c713c 100644 --- a/src/main/kotlin/com/jetbrains/snakecharm/codeInsight/SnakemakeAPI.kt +++ b/src/main/kotlin/com/jetbrains/snakecharm/codeInsight/SnakemakeAPI.kt @@ -129,7 +129,7 @@ object SnakemakeAPI { */ val SINGLE_ARGUMENT_WORKFLOWS_KEYWORDS = setOf( WORKFLOW_CONTAINERIZED_KEYWORD, WORKFLOW_CONTAINER_KEYWORD, - WORKFLOW_SINGULARITY_KEYWORD + WORKFLOW_SINGULARITY_KEYWORD, WORKFLOW_WORKDIR_KEYWORD ) // List of top-level sections diff --git a/src/main/kotlin/com/jetbrains/snakecharm/inspections/SmkDocstringsWillBeIgnoredInspection.kt b/src/main/kotlin/com/jetbrains/snakecharm/inspections/SmkDocstringsWillBeIgnoredInspection.kt new file mode 100644 index 00000000..5cbbe93e --- /dev/null +++ b/src/main/kotlin/com/jetbrains/snakecharm/inspections/SmkDocstringsWillBeIgnoredInspection.kt @@ -0,0 +1,33 @@ +package com.jetbrains.snakecharm.inspections + +import com.intellij.codeInspection.LocalInspectionToolSession +import com.intellij.codeInspection.ProblemsHolder +import com.jetbrains.snakecharm.SnakemakeBundle +import com.jetbrains.snakecharm.lang.psi.* + +class SmkDocstringsWillBeIgnoredInspection : SnakemakeInspection() { + override fun buildVisitor( + holder: ProblemsHolder, + isOnTheFly: Boolean, + session: LocalInspectionToolSession + ) = object : SnakemakeInspectionVisitor(holder, getContext(session)) { + + override fun visitSmkRule(rule: SmkRule) { + visitSMKRuleLike(rule) + } + + override fun visitSmkCheckPoint(checkPoint: SmkCheckPoint) { + visitSMKRuleLike(checkPoint) + } + + fun visitSMKRuleLike(rule: SmkRuleLike) { + val docstrings = rule.getStringLiteralExpressions().drop(1) + if (docstrings.isEmpty()) { + return + } + for (docstring in docstrings) { + registerProblem(docstring, SnakemakeBundle.message("INSP.NAME.docstrings.will.be.ignored")) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetbrains/snakecharm/inspections/SmkLocalrulesRuleorderConfusingReference.kt b/src/main/kotlin/com/jetbrains/snakecharm/inspections/SmkLocalrulesRuleorderConfusingReference.kt index 66d5bcea..e94b44ec 100644 --- a/src/main/kotlin/com/jetbrains/snakecharm/inspections/SmkLocalrulesRuleorderConfusingReference.kt +++ b/src/main/kotlin/com/jetbrains/snakecharm/inspections/SmkLocalrulesRuleorderConfusingReference.kt @@ -30,9 +30,7 @@ class SmkLocalrulesRuleorderConfusingReference : SnakemakeInspection() { ?.filterIsInstance(SmkReferenceExpression::class.java) if (args != null) { - val rules = file.collectRules().map { it.first } - val checkPoints = file.collectCheckPoints().map { it.first } - val ruleLike = rules.plus(checkPoints).toSet() + val ruleLike = file.advancedCollectRules(mutableSetOf()).map { it.first }.toSet() args.forEach { expr -> val name = expr.name if (name != null && name !in ruleLike) { diff --git a/src/main/kotlin/com/jetbrains/snakecharm/inspections/SmkUnresolvedReferenceInspectionExtension.kt b/src/main/kotlin/com/jetbrains/snakecharm/inspections/SmkUnresolvedReferenceInspectionExtension.kt new file mode 100644 index 00000000..a42f54af --- /dev/null +++ b/src/main/kotlin/com/jetbrains/snakecharm/inspections/SmkUnresolvedReferenceInspectionExtension.kt @@ -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) { + 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)) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetbrains/snakecharm/inspections/quickfix/CreateMissedFileQuickFix.kt b/src/main/kotlin/com/jetbrains/snakecharm/inspections/quickfix/CreateMissedFileQuickFix.kt new file mode 100644 index 00000000..acb75e0e --- /dev/null +++ b/src/main/kotlin/com/jetbrains/snakecharm/inspections/quickfix/CreateMissedFileQuickFix.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetbrains/snakecharm/inspections/quickfix/CreateMissedFileUndoableAction.kt b/src/main/kotlin/com/jetbrains/snakecharm/inspections/quickfix/CreateMissedFileUndoableAction.kt new file mode 100644 index 00000000..5d21684f --- /dev/null +++ b/src/main/kotlin/com/jetbrains/snakecharm/inspections/quickfix/CreateMissedFileUndoableAction.kt @@ -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 { + // * 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) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetbrains/snakecharm/lang/highlighter/SmkSyntaxAnnotator.kt b/src/main/kotlin/com/jetbrains/snakecharm/lang/highlighter/SmkSyntaxAnnotator.kt index 13d4bc7c..38a72105 100644 --- a/src/main/kotlin/com/jetbrains/snakecharm/lang/highlighter/SmkSyntaxAnnotator.kt +++ b/src/main/kotlin/com/jetbrains/snakecharm/lang/highlighter/SmkSyntaxAnnotator.kt @@ -97,15 +97,5 @@ object SmkSyntaxAnnotator : SmkAnnotator() { st.getSectionKeywordNode()?.let { addHighlightingAnnotation(it, SnakemakeSyntaxHighlighterFactory.SMK_DECORATOR) } - if (st is SmkArgsSection) { - st.keywordArguments?.forEach { - it.keywordNode?.psi?.let { name -> - addHighlightingAnnotation( - name, - SnakemakeSyntaxHighlighterFactory.SMK_KEYWORD_ARGUMENT - ) - } - } - } } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetbrains/snakecharm/lang/highlighter/SnakemakeSyntaxHighlighterFactory.kt b/src/main/kotlin/com/jetbrains/snakecharm/lang/highlighter/SnakemakeSyntaxHighlighterFactory.kt index e3675ddc..55985271 100644 --- a/src/main/kotlin/com/jetbrains/snakecharm/lang/highlighter/SnakemakeSyntaxHighlighterFactory.kt +++ b/src/main/kotlin/com/jetbrains/snakecharm/lang/highlighter/SnakemakeSyntaxHighlighterFactory.kt @@ -33,10 +33,8 @@ class SnakemakeSyntaxHighlighterFactory : SyntaxHighlighterFactory() { ) val SMK_PREDEFINED_DEFINITION: TextAttributesKey = PyHighlighter.PY_PREDEFINED_DEFINITION // IDK why, but explicit creating via '.createText...' works improperly - val SMK_KEYWORD_ARGUMENT = TextAttributesKey.createTextAttributesKey( - "SMK_KEYWORD_ARGUMENT", - DefaultLanguageHighlighterColors.PARAMETER - ) + val SMK_KEYWORD_ARGUMENT: TextAttributesKey = + PyHighlighter.PY_KEYWORD_ARGUMENT // The same to SMK_PREDEFINED_DEFINITION case val SMK_TEXT = TextAttributesKey.createTextAttributesKey("SMK_TEXT", DefaultLanguageHighlighterColors.STRING) val SMK_TRIPLE_QUOTED_STRING = TextAttributesKey.createTextAttributesKey( "SMK_TRIPLE_QUOTED_STRING", diff --git a/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/SmkFile.kt b/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/SmkFile.kt index 5e0161ff..f8c202ed 100644 --- a/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/SmkFile.kt +++ b/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/SmkFile.kt @@ -117,6 +117,18 @@ class SmkFile(viewProvider: FileViewProvider) : PyFileImpl(viewProvider, Snakema return includeStatements } + fun collectIncludedFiles(visitedFiles: MutableSet = mutableSetOf()): Set{ + val includes = collectIncludes() + visitedFiles.add(containingFile.originalFile) + includes.forEach { include -> + val file = include.references.firstOrNull()?.resolve() as? PsiFile + if (file !in visitedFiles && file is SmkFile){ + file.collectIncludedFiles(visitedFiles) + } + } + return visitedFiles + } + /** * Returns [PsiElement] from [SmkUse] which may produce [name] or returns null if no such element */ diff --git a/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/SmkFileReference.kt b/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/SmkFileReference.kt index 55f676d1..7aedff47 100644 --- a/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/SmkFileReference.kt +++ b/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/SmkFileReference.kt @@ -24,8 +24,8 @@ open class SmkFileReference( element: SmkArgsSection, private val textRange: TextRange, private val stringLiteralExpression: PyStringLiteralExpression, - private val path: String, - private val searchRelativelyToCurrentFolder: Boolean = true, + val path: String, + val searchRelativelyToCurrentFolder: Boolean = true, ) : PsiReferenceBase(element, textRange), PsiReferenceEx { // Reference caching can be implemented with the 'ResolveCache' class if needed @@ -158,6 +158,8 @@ open class SmkFileReference( } override fun getUnresolvedDescription(): String? = null + + open fun hasAppropriateSuffix(): Boolean = false } /** @@ -173,6 +175,9 @@ class SmkIncludeReference( override fun getVariants() = collectFileSystemItemLike { it is SmkFile && it.originalFile != element.containingFile.originalFile } + + override fun hasAppropriateSuffix() = + (path.endsWith(".smk") || path == "Snakemake") && element.containingFile.virtualFile.path != path } /** @@ -193,8 +198,10 @@ class SmkConfigfileReference( searchRelativelyToCurrentFolder = false ) { override fun getVariants() = collectFileSystemItemLike { - isYamlFile(it) + isYamlFile(it.name) } + + override fun hasAppropriateSuffix() = isYamlFile(path) } /** @@ -215,8 +222,10 @@ class SmkPepfileReference( searchRelativelyToCurrentFolder = false ) { override fun getVariants() = collectFileSystemItemLike { - isYamlFile(it) + isYamlFile(it.name) } + + override fun hasAppropriateSuffix() = isYamlFile(path) } /** @@ -230,8 +239,10 @@ class SmkPepschemaReference( path: String ) : SmkFileReference(element, textRange, stringLiteralExpression, path) { override fun getVariants() = collectFileSystemItemLike { - isYamlFile(it) + isYamlFile(it.name) } + + override fun hasAppropriateSuffix() = isYamlFile(path) } /** @@ -245,11 +256,13 @@ class SmkCondaEnvReference( path: String ) : SmkFileReference(element, textRange, stringLiteralExpression, path) { override fun getVariants() = collectFileSystemItemLike { - isYamlFile(it) + isYamlFile(it.name) } + + override fun hasAppropriateSuffix() = isYamlFile(path.lowercase()) } -private fun isYamlFile(it: PsiFileSystemItem) = it.name.endsWith(".yaml") || it.name.endsWith(".yml") +private fun isYamlFile(it: String) = it.endsWith(".yaml") || it.endsWith(".yml") /** * The path must built from directory with current snakefile @@ -265,6 +278,8 @@ class SmkNotebookReference( val name = it.name.lowercase() name.endsWith(".ipynb") } + + override fun hasAppropriateSuffix() = path.endsWith(".ipynb") } /** @@ -279,8 +294,13 @@ class SmkScriptReference( ) : SmkFileReference(element, textRange, stringLiteralExpression, path) { override fun getVariants() = collectFileSystemItemLike { val name = it.name.lowercase() - name.endsWith(".py") or name.endsWith(".r") or name.endsWith(".rmd") or name.endsWith(".jl") or name.endsWith(".rs") + hasCorrectEnding(name) } + + override fun hasAppropriateSuffix() = hasCorrectEnding(path.lowercase()) + + private fun hasCorrectEnding(name: String) = + name.endsWith(".py") or name.endsWith(".r") or name.endsWith(".rmd") or name.endsWith(".jl") or name.endsWith(".rs") } /** @@ -296,6 +316,8 @@ class SmkReportReference( override fun getVariants() = collectFileSystemItemLike { it.name.endsWith(".html") } + + override fun hasAppropriateSuffix() = path.endsWith(".html") } /** diff --git a/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/SmkRuleLike.kt b/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/SmkRuleLike.kt index 04fd9183..2fb7ec78 100644 --- a/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/SmkRuleLike.kt +++ b/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/SmkRuleLike.kt @@ -4,6 +4,7 @@ import com.intellij.psi.PsiNameIdentifierOwner import com.jetbrains.python.psi.PyElementType import com.jetbrains.python.psi.PyStatement import com.jetbrains.python.psi.PyStatementListContainer +import com.jetbrains.python.psi.PyStringLiteralExpression interface SmkRuleLike: SmkSection, SmkToplevelSection, PyStatementListContainer, PyStatement, @@ -13,4 +14,5 @@ interface SmkRuleLike: SmkSection, SmkToplevelSection, PySta val sectionTokenType: PyElementType fun getSections(): List fun getSectionByName(sectionName: String): S? + fun getStringLiteralExpressions(): List } \ No newline at end of file diff --git a/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/impl/SmkRuleLikeImpl.kt b/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/impl/SmkRuleLikeImpl.kt index b6ebb00f..02cb03d7 100644 --- a/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/impl/SmkRuleLikeImpl.kt +++ b/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/impl/SmkRuleLikeImpl.kt @@ -7,6 +7,7 @@ import com.intellij.psi.stubs.NamedStub import com.jetbrains.python.PyElementTypes import com.jetbrains.python.PyNames.UNNAMED_ELEMENT import com.jetbrains.python.psi.PyStatementList +import com.jetbrains.python.psi.PyStringLiteralExpression import com.jetbrains.python.psi.PyUtil import com.jetbrains.python.psi.impl.PyBaseElementImpl import com.jetbrains.python.psi.impl.PyElementPresentation @@ -50,6 +51,8 @@ abstract class SmkRuleLikeImpl, PsiT : SmkRuleLike, o override fun getNameIdentifier() = getNameNode()?.psi + override fun getStringLiteralExpressions(): List = statementList.children.filterIsInstance() + /** * Use name start offset here, required for navigation & find usages, e.g. when ask for usages on name identifier */ diff --git a/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/impl/refs/SmkRuleOrCheckpointNameReference.kt b/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/impl/refs/SmkRuleOrCheckpointNameReference.kt index 5362156d..e838356d 100644 --- a/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/impl/refs/SmkRuleOrCheckpointNameReference.kt +++ b/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/impl/refs/SmkRuleOrCheckpointNameReference.kt @@ -60,6 +60,7 @@ class SmkRuleOrCheckpointNameReference( if (!parentIsImportedRuleNames && !itIsModuleMameReference) { return results } + val allImportedFiles = smkFile.collectIncludedFiles() return results.filter { resolveResult -> // If we resolve module references, there must be only SmkModules (resolveResult.element is SmkModule && itIsModuleMameReference) || @@ -67,7 +68,7 @@ class SmkRuleOrCheckpointNameReference( (moduleRef != null // Module name reference is defined and resolve result is from another file && element.containingFile.originalFile != resolveResult.element?.containingFile?.originalFile) // OR There are no 'from *name*' combination, so it hasn't been imported - || (moduleRef == null && resolveResult.element?.containingFile?.originalFile == element.containingFile.originalFile) + || (moduleRef == null && resolveResult.element?.containingFile?.originalFile in allImportedFiles) }.toMutableList() } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index c66e0d68..0f1158ae 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -418,6 +418,17 @@ implementationClass="com.jetbrains.snakecharm.inspections.SmkRuleOrCheckpointNameYetUndefinedInspection" /> + + + \ No newline at end of file diff --git a/src/main/resources/SnakemakeBundle.properties b/src/main/resources/SnakemakeBundle.properties index 1f4d3e59..d4b8b23f 100644 --- a/src/main/resources/SnakemakeBundle.properties +++ b/src/main/resources/SnakemakeBundle.properties @@ -89,6 +89,9 @@ INSP.NAME.wrapper.args.missed.message=Argument ''{0}'' missed in ''{1}'' INSP.NAME.wrapper.args.section.missed.message=Section ''{0}'' is missed INSP.NAME.wrapper.args.section.with.args.missed.message=Section ''{0}'' with args ''{1}'' is missed +# SmkUnresolvedReferenceInspectionExtension +INSP.NAME.conda.env.missing.fix=Create ''{0}'' + # SmkSubworkflowRedeclarationInspection INSP.NAME.subworkflow.redeclaration=Only last subworkflow with the same name will be executed @@ -102,6 +105,9 @@ INSP.NAME.codestyle.avoid.whitespace.tab=Tab character detected. PEP-8 recommend INSP.NAME.rule.or.checkpoint.name.yet.undefined=Rule or Checkpoint name has not been defined yet INSP.NAME.rule.or.checkpoint.name.yet.undefined.msg=Rule or Checkpoint name ''{0}'' has not been defined yet +# SmkDocstringsWillBeIgnoredInspection +INSP.NAME.docstrings.will.be.ignored=All docstrings except the first one will be ignored. + # SmkLocalrulesRuleorderRepeatedRuleInspection INSP.NAME.localrules.ruleorder.repeated.rule=This rule has already been added to this section. @@ -259,6 +265,11 @@ notifier.group.title=SnakeCharm plugin notifications notifier.msg.framework.by.snakefile.title=Snakemake framework detected notifier.msg.framework.by.snakefile.action.configure=Configure Framework... notifier.msg.framework.by.snakefile=Snakefile was found in ''{0}''. +notifier.msg.creating.env.file=Creating ''{0}''... +notifier.msg.deleting.env.file=Deleting ''{0}''... +notifier.msg.create.env.file.title=Failed to create file +notifier.msg.create.env.file.io.exception=Failed to create ''{0}''. Check permissions and the target path correctness. +notifier.msg.create.env.file.name.invalid.file.exception=File has invalid name: ''{0}''. ####################### # Banner diff --git a/src/main/resources/inspectionDescriptions/SmkDocstringsWillBeIgnoredInspection.html b/src/main/resources/inspectionDescriptions/SmkDocstringsWillBeIgnoredInspection.html new file mode 100644 index 00000000..eecc1af9 --- /dev/null +++ b/src/main/resources/inspectionDescriptions/SmkDocstringsWillBeIgnoredInspection.html @@ -0,0 +1,5 @@ + + +Snakemake will ignore all docstrings except the first one. + + \ No newline at end of file diff --git a/src/test/resources/features/completion/localrules_and_ruleorder_completion.feature b/src/test/resources/features/completion/localrules_and_ruleorder_completion.feature index 57afff9e..e2425ec9 100644 --- a/src/test/resources/features/completion/localrules_and_ruleorder_completion.feature +++ b/src/test/resources/features/completion/localrules_and_ruleorder_completion.feature @@ -12,6 +12,9 @@ Feature: Completion for rule names in localrules and ruleorder sections rule rule3: output: touch("_output.txt") + use rule rule3 as rule4 with: + output: touch("_output_rule4.txt") + localrules: rule1, """ When I put the caret after rule1, @@ -19,6 +22,7 @@ Feature: Completion for rule names in localrules and ruleorder sections Then completion list should only contain: | rule2 | | rule3 | + | rule4 | Scenario: Complete in ruleorder section Given a snakemake project @@ -33,6 +37,9 @@ Feature: Completion for rule names in localrules and ruleorder sections rule rule3: output: touch("_output.txt") + use rule rule3 as rule4 with: + output: touch("_output_rule4.txt") + ruleorder: rule3 > """ When I put the caret after rule3 > @@ -40,6 +47,7 @@ Feature: Completion for rule names in localrules and ruleorder sections Then completion list should only contain: | rule1 | | rule2 | + | rule4 | Scenario Outline: Complete in localrules/ruleorder section from included files Given a snakemake project @@ -47,6 +55,9 @@ Feature: Completion for rule names in localrules and ruleorder sections """ rule rule4: input: "path/to/input" + + use rule rule4 as rule6 with: + output: touch("_output_rule6.txt" """ Given a file "soo.smk" with text """ @@ -82,6 +93,7 @@ Feature: Completion for rule names in localrules and ruleorder sections | rule2 | | rule4 | | rule5 | + | rule6 | Examples: | section | separator | | localrules | , | @@ -94,6 +106,9 @@ Feature: Completion for rule names in localrules and ruleorder sections rule boo1: input: "file.txt" rule boo2: input: "file1.txt" + + use rule boo2 as boo3 with: + input: "file2.txt" """ Given a file "soo.smk" with text """ @@ -118,6 +133,7 @@ Feature: Completion for rule names in localrules and ruleorder sections | soo | | boo1 | | boo2 | + | boo3 | Examples: | section | separator | | localrules | , | diff --git a/src/test/resources/features/highlighting/inspections/confusing_localrule_ruleorder_reference.feature b/src/test/resources/features/highlighting/inspections/confusing_localrule_ruleorder_reference.feature index 7aa8921d..586b6292 100644 --- a/src/test/resources/features/highlighting/inspections/confusing_localrule_ruleorder_reference.feature +++ b/src/test/resources/features/highlighting/inspections/confusing_localrule_ruleorder_reference.feature @@ -4,12 +4,15 @@ Feature: Inspection warns about confusing localrules or ruleorder names. Given a snakemake project And a file "boo.smk" with text """ - boo: + boo1: input: "in" + + use rule boo1 as boo2 with: + input: "in_2" """ And I open a file "foo.smk" with text """ -
: foo2 boo foo1 +
: foo2 foo1 rule foo1: input: "in" @@ -17,17 +20,17 @@ Feature: Inspection warns about confusing localrules or ruleorder names. input: "in" """ When SmkLocalrulesRuleorderConfusingReference inspection is enabled - Then I expect inspection weak warning on with message + Then I expect inspection weak warning on <> with message """ - Rule 'boo' isn't defined in this file, not an error but it is confusing. + Rule '' isn't defined in this file, not an error but it is confusing. """ When I check highlighting weak warnings Examples: - | rule_like | section | separator | - | rule | localrules | , | - | checkpoint | localrules | , | - | rule | ruleorder | > | - | checkpoint | ruleorder | > | + | rule_like | section | separator | name | + | rule | localrules | , | boo1 | + | checkpoint | localrules | , | boo2 | + | rule | ruleorder | > | boo1 | + | checkpoint | ruleorder | > | boo2 | Scenario Outline: No confusing localrule/ruleorder ref when overridden Given a snakemake project @@ -54,4 +57,24 @@ Feature: Inspection warns about confusing localrules or ruleorder names. | rule | rule | ruleorder | | rule | checkpoint | ruleorder | | checkpoint | rule | ruleorder | - | checkpoint | checkpoint | ruleorder | \ No newline at end of file + | checkpoint | checkpoint | ruleorder | + + Scenario Outline: No confusing localrule/ruleorder ref for 'use rule' + Given a snakemake project + Given I open a file "foo.smk" with text + """ +
: boo2 + + rule boo: + input: "in" + + use rule boo as boo2 with: + input: "in_2" + """ + When SmkLocalrulesRuleorderConfusingReference inspection is enabled + Then I expect no inspection weak warnings + When I check highlighting weak warnings + Examples: + | section | + | localrule | + | ruleorder | \ No newline at end of file diff --git a/src/test/resources/features/highlighting/inspections/docstrings_will_be_ignored.feature b/src/test/resources/features/highlighting/inspections/docstrings_will_be_ignored.feature new file mode 100644 index 00000000..eaa6028c --- /dev/null +++ b/src/test/resources/features/highlighting/inspections/docstrings_will_be_ignored.feature @@ -0,0 +1,44 @@ +Feature: Inspection warns that all docstrings will be ignored except the first one. + + Scenario Outline: Weak warning if there are more than one docstring + Given a snakemake project + Given I open a file "foo.smk" with text + """ + : + 'docstring 1' + "docstring 2" + \"\"\" docstring 3 \"\"\" + output: "output_file.txt" + """ + And SmkDocstringsWillBeIgnoredInspection inspection is enabled + Then I expect inspection weak warning on <"docstring 2"> with message + """ + All docstrings except the first one will be ignored. + """ + Then I expect inspection weak warning with message "All docstrings except the first one will be ignored." on + """ + \"\"\" docstring 3 \"\"\" + """ + When I check highlighting weak warnings + Examples: + | rule_like | + | rule | + | checkpoint | + + Scenario Outline: No warning if there is only one docstring or none + Given a snakemake project + Given I open a file "foo.smk" with text + """ + : + + output: "output_file.txt" + """ + And SmkDocstringsWillBeIgnoredInspection inspection is enabled + And I expect no inspection weak warnings + When I check highlighting weak warnings + Examples: + | rule_like | str | + | rule | "docstring 1" | + | rule | | + | checkpoint | "docstring 1" | + | checkpoint | | \ No newline at end of file diff --git a/src/test/resources/features/highlighting/inspections/multiple_args_inspection.feature b/src/test/resources/features/highlighting/inspections/multiple_args_inspection.feature index 177590e4..9c58ddb9 100644 --- a/src/test/resources/features/highlighting/inspections/multiple_args_inspection.feature +++ b/src/test/resources/features/highlighting/inspections/multiple_args_inspection.feature @@ -85,3 +85,4 @@ Feature: Inspection for multiple arguments in various sections | containerized | | singularity | | container | + | workdir | diff --git a/src/test/resources/features/highlighting/inspections/unresolved_reference_extension.feature b/src/test/resources/features/highlighting/inspections/unresolved_reference_extension.feature new file mode 100644 index 00000000..83ef3268 --- /dev/null +++ b/src/test/resources/features/highlighting/inspections/unresolved_reference_extension.feature @@ -0,0 +1,31 @@ +Feature: Inspection: SmkUnresolvedReferenceInspectionExtension + + Scenario Outline: Quick fix fot missed files + Given a snakemake project + Given I open a file "foo.smk" with text + """ +
: "" + """ + And PyUnresolvedReferencesInspection inspection is enabled + Then I expect inspection error on <> with message + """ + Unresolved reference '' + """ + When I check highlighting warnings + And I see available quick fix: Create '' + Examples: + | path | section | + | NAME.yaml | rule NAME: conda | + | envs/NAME.yaml | rule NAME: conda | + | ../envs/NAME.yaml | rule NAME: conda | + | NAME.py.ipynb | rule NAME: notebook | + | NAME.py | rule NAME: script | + | boo.smk | module NAME: snakefile | + | NAME.yaml | configfile | + | NAME.yaml | pepfile | + | NAME.yml | pepschema | + + # Impossible to check whether the file has been created because: + # 1) It is being creating asynchronously + # 2) So, we may need async refresh() (see LightTempDirTestFixtureImpl.java:137) + # It leads to Exception: "Do not perform a synchronous refresh under read lock ..." \ No newline at end of file diff --git a/src/test/resources/features/highlighting/smk_syntax_annotator.feature b/src/test/resources/features/highlighting/smk_syntax_annotator.feature index 62d6dbbe..71296159 100644 --- a/src/test/resources/features/highlighting/smk_syntax_annotator.feature +++ b/src/test/resources/features/highlighting/smk_syntax_annotator.feature @@ -222,21 +222,6 @@ Feature: Annotate additional syntax """ When I check highlighting infos - Scenario: Annotate keyword argument - Given a snakemake project - Given I open a file "foo.smk" with text - """ - rule NAME: - input: - "file1", - arg = "file2" - """ - Then I expect inspection info on with message - """ - SMK_KEYWORD_ARGUMENT - """ - When I check highlighting infos ignoring extra highlighting - Scenario: 'use' section highlighting, part 2 Given a snakemake project Given I open a file "foo.smk" with text diff --git a/src/test/resources/features/resolve/localrules_and_ruleorder_resolve.feature b/src/test/resources/features/resolve/localrules_and_ruleorder_resolve.feature index ad98fbe2..48eff208 100644 --- a/src/test/resources/features/resolve/localrules_and_ruleorder_resolve.feature +++ b/src/test/resources/features/resolve/localrules_and_ruleorder_resolve.feature @@ -13,17 +13,21 @@ Feature: Resolve for rules in localrules and ruleorder rule cccc: output: touch("_output.txt") + use rule cccc as dddd with: + output: touch("_output_dddd.txt") + localrules: """ When I put the caret after Then reference should resolve to "" in "" Examples: - | ptn | text | symbol_name | file | - | localrules: aaa | aaaa | aaaa | foo.smk | - | localrules: bbb | bbbb | bbbb | foo.smk | - | localrules: ccc | cccc | cccc | foo.smk | - | localrules: aaaa, bb | aaaa, bbbb | bbbb | foo.smk | + | ptn | text | symbol_name | file | + | localrules: aaa | aaaa | aaaa | foo.smk | + | localrules: bbb | bbbb | bbbb | foo.smk | + | localrules: ccc | cccc | cccc | foo.smk | + | localrules: aaaa, bb | aaaa, bbbb | bbbb | foo.smk | + | localrules: ddd | dddd | dddd | foo.smk | Scenario Outline: Resolve in localrules section above all rule declarations Given a snakemake project @@ -39,16 +43,20 @@ Feature: Resolve for rules in localrules and ruleorder checkpoint cccc: output: touch("_output.txt") + + use rule cccc as dddd with: + output: touch("_output_dddd.txt") """ When I put the caret after Then reference should resolve to "" in "" Examples: - | ptn | text | symbol_name | file | - | localrules: aaa | aaaa | aaaa | foo.smk | - | localrules: bbb | bbbb | bbbb | foo.smk | - | localrules: ccc | cccc | cccc | foo.smk | - | localrules: aaaa, bb | aaaa, bbbb | bbbb | foo.smk | + | ptn | text | symbol_name | file | + | localrules: aaa | aaaa | aaaa | foo.smk | + | localrules: bbb | bbbb | bbbb | foo.smk | + | localrules: ccc | cccc | cccc | foo.smk | + | localrules: aaaa, bb | aaaa, bbbb | bbbb | foo.smk | + | localrules: ddd | dddd | dddd | foo.smk | Scenario Outline: Resolve in ruleorder section Given a snakemake project @@ -63,15 +71,19 @@ Feature: Resolve for rules in localrules and ruleorder checkpoint cccc: output: touch("_output.txt") + use rule cccc as dddd with: + output: touch("_output_dddd.txt") + ruleorder: aaaa > """ When I put the caret after Then reference should resolve to "" in "" Examples: - | ptn | text | symbol_name | file | - | > bbb | bbbb | bbbb | foo.smk | - | > ccc | cccc | cccc | foo.smk | + | ptn | text | symbol_name | file | + | > bbb | bbbb | bbbb | foo.smk | + | > ccc | cccc | cccc | foo.smk | + | > ddd | dddd | dddd | foo.smk | Scenario Outline: Resolve in ruleorder section above all rule declarations @@ -88,15 +100,19 @@ Feature: Resolve for rules in localrules and ruleorder checkpoint cccc: output: touch("_output.txt") + + use rule cccc as dddd with: + output: touch("_output_dddd.txt") """ When I put the caret after Then reference should resolve to "" in "" Examples: - | ptn | text | symbol_name | file | - | > bbb | bbbb | bbbb | foo.smk | - | > ccc | cccc | cccc | foo.smk | - | ruleorder: aa | aaaa | aaaa | foo.smk | + | ptn | text | symbol_name | file | + | > bbb | bbbb | bbbb | foo.smk | + | > ccc | cccc | cccc | foo.smk | + | ruleorder: aa | aaaa | aaaa | foo.smk | + | > ddd | dddd | dddd | foo.smk | Scenario Outline: Multiresolve in ruleorder section Given a snakemake project @@ -159,6 +175,9 @@ Feature: Resolve for rules in localrules and ruleorder """ rule dddd: input: "path/to/input" + + use rule dddd as eeee with: + output: touch("_output_dddd.txt") """ Given I open a file "foo.smk" with text """ @@ -179,6 +198,8 @@ Feature: Resolve for rules in localrules and ruleorder | ptn | text | section | separator | symbol_name | file | | ddd | dddd | localrules | , | dddd | boo.smk | | ddd | dddd | ruleorder | > | dddd | boo.smk | + | eee | eeee | localrules | , | eeee | boo.smk | + | eee | eeee | ruleorder | > | eeee | boo.smk | Scenario Outline: Resolve in localrules/ruleorder section for rules from included files Given a snakemake project @@ -189,6 +210,9 @@ Feature: Resolve for rules in localrules and ruleorder checkpoint eeee: input: "path/to/input/2" + + use rule eeee as ffff with: + output: touch("_output_dddd.txt") """ Given I open a file "foo.smk" with text """ @@ -213,6 +237,8 @@ Feature: Resolve for rules in localrules and ruleorder | ddd | dddd | dddd | boo.smk | ruleorder | > | | eee | eeee | eeee | boo.smk | localrules | , | | eee | eeee | eeee | boo.smk | ruleorder | > | + | fff | ffff | ffff | boo.smk | localrules | , | + | fff | ffff | ffff | boo.smk | ruleorder | > | Scenario Outline: Resolve in localrules/ruleorder section for rules from files included in other files Given a snakemake project @@ -220,6 +246,9 @@ Feature: Resolve for rules in localrules and ruleorder """ rule dddd: input: "path/to/input" + + use rule dddd as ffff with: + output: touch("_output_dddd.txt") """ Given a file "soo.smk" with text """ @@ -254,6 +283,8 @@ Feature: Resolve for rules in localrules and ruleorder | ddd | dddd | dddd | boo.smk | ruleorder | > | | eee | eeee | eeee | soo.smk | localrules | , | | eee | eeee | eeee | soo.smk | ruleorder | > | + | fff | ffff | ffff | boo.smk | localrules | , | + | fff | ffff | ffff | boo.smk | ruleorder | > | diff --git a/src/test/resources/features/resolve/uses_and_modules_name_resolve.feature b/src/test/resources/features/resolve/uses_and_modules_name_resolve.feature index 5c9174dd..89c6f24c 100644 --- a/src/test/resources/features/resolve/uses_and_modules_name_resolve.feature +++ b/src/test/resources/features/resolve/uses_and_modules_name_resolve.feature @@ -181,4 +181,21 @@ Feature: Resolve use and module name to its declaration use rule * from MODULE as * """ When I put the caret at MODULE as - Then reference should resolve to "MODULE:" in "foo.smk" \ No newline at end of file + Then reference should resolve to "MODULE:" in "foo.smk" + + Scenario: Resolve to rule imported via 'include' + Given a snakemake project + Given I open a file "boo.smk" with text + """ + rule A: + threads: 1 + """ + Given I open a file "foo.smk" with text + """ + include: "boo.smk" + + use rule A as B with: + threads: 2 + """ + When I put the caret at A + Then reference should resolve to "A" in "boo.smk" \ No newline at end of file