Skip to content

Commit

Permalink
feature: For section keywords show 'since' version and deprecation no…
Browse files Browse the repository at this point in the history
…tice in completion list. Do not suggest already removed keywords. Closes #535
  • Loading branch information
iromeo committed Oct 11, 2024
1 parent 4371645 commit 0fd6926
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 95 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ Released on <not released>
- Inspection: Warn about deprecated or not yet available features based on snakemake language level (see [#508](https://github.com/JetBrains-Research/snakecharm/issues/508)
- Move snakemake API addition/removal info from JAR into `extra/snakemake_api.yaml` in plugin directory
- Inspection: Warn if snakemake section type or API function isn't supported in the current snakemake project based on a snakemake version (see [#500](https://github.com/JetBrains-Research/snakecharm/issues/500)

- Code completion: For section keywords show 'since' version and deprecation notice in the completion list. Do not suggest already removed keywords (see [#535](https://github.com/JetBrains-Research/snakecharm/issues/535)
-
### Fixed
- Improve parser error message when rule/module is declared with name but lacks ':' (see [#515](https://github.com/JetBrains-Research/snakecharm/issues/515))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.intellij.codeInsight.completion.*
import com.intellij.codeInsight.lookup.TailTypeDecorator
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.project.Project
import com.intellij.patterns.PlatformPatterns.psiElement
import com.intellij.patterns.PsiElementPattern
import com.intellij.patterns.StandardPatterns
Expand All @@ -23,6 +24,11 @@ import com.jetbrains.snakecharm.codeInsight.SnakemakeAPI.SUBWORKFLOW_SECTIONS_KE
import com.jetbrains.snakecharm.codeInsight.SnakemakeAPI.TOPLEVEL_ARGS_SECTION_KEYWORDS
import com.jetbrains.snakecharm.codeInsight.SnakemakeAPI.USE_DECLARATION_KEYWORDS
import com.jetbrains.snakecharm.codeInsight.SnakemakeAPI.USE_SECTIONS_KEYWORDS
import com.jetbrains.snakecharm.framework.SmkFrameworkDeprecationProvider
import com.jetbrains.snakecharm.framework.SmkFrameworkDeprecationProvider.Companion.TOP_LEVEL_KEYWORD_TYPE
import com.jetbrains.snakecharm.framework.SmkSupportProjectSettings
import com.jetbrains.snakecharm.framework.UpdateType
import com.jetbrains.snakecharm.lang.SmkLanguageVersion
import com.jetbrains.snakecharm.lang.SnakemakeNames
import com.jetbrains.snakecharm.lang.parser.SmkTokenTypes.RULE_LIKE
import com.jetbrains.snakecharm.lang.parser.SmkTokenTypes.WORKFLOW_TOPLEVEL_DECORATORS_WO_RULE_LIKE
Expand Down Expand Up @@ -117,20 +123,19 @@ object WorkflowTopLevelKeywordsProvider : CompletionProvider<CompletionParameter
val spaceTailKeys = RULE_LIKE.types.map { tt ->
tokenType2Name[tt]!!
}

// top-level
val project = parameters.position.project
listOf(
colonAndWhiteSpaceTailKeys to ColonAndWhiteSpaceTail,
spaceTailKeys to TailTypes.spaceType(),
).forEach { (tokenSet, tail) ->
tokenSet.forEach { s ->
val modifiedTail = if (s == SnakemakeNames.USE_KEYWORD) RuleKeywordTail else tail
result.addElement(
SmkCompletionUtil.createPrioritizedLookupElement(
TailTypeDecorator.withTail(
PythonLookupElement(s, true, null), modifiedTail
),
SmkCompletionUtil.KEYWORDS_PRIORITY
)
)

filterByDeprecationAndAddLookupItems(project, tokenSet, result, isTopLevel=true,
customTailTypes = tokenSet.filter { it == SnakemakeNames.USE_KEYWORD}.associate { it to RuleKeywordTail},
defaultTailType = tail,
priority = SmkCompletionUtil.KEYWORDS_PRIORITY) {
TOP_LEVEL_KEYWORD_TYPE
}
}
}
Expand Down Expand Up @@ -184,17 +189,13 @@ object RuleSectionKeywordsProvider : CompletionProvider<CompletionParameters>()
context: ProcessingContext,
result: CompletionResultSet
) {
RULE_OR_CHECKPOINT_SECTION_KEYWORDS.forEach { s ->

result.addElement(
SmkCompletionUtil.createPrioritizedLookupElement(
TailTypeDecorator.withTail(
PythonLookupElement(s, true, PlatformIcons.PROPERTY_ICON),
ColonAndWhiteSpaceTail
),
priority = SmkCompletionUtil.SECTIONS_KEYS_PRIORITY
)
)
val element = parameters.position
filterByDeprecationAndAddLookupItems(element.project, RULE_OR_CHECKPOINT_SECTION_KEYWORDS, result, priority = SmkCompletionUtil.SECTIONS_KEYS_PRIORITY){
val smkRuleOrCheckpoint = PsiTreeUtil.getParentOfType(element, SmkRuleOrCheckpoint::class.java)
requireNotNull(smkRuleOrCheckpoint) {
"According to CAPTURE should be inside rule or checkpoint: <${element.text}> at ${element.textRange}"
}
smkRuleOrCheckpoint.sectionKeyword
}
}
}
Expand Down Expand Up @@ -240,16 +241,13 @@ object ModuleSectionKeywordsProvider : CompletionProvider<CompletionParameters>(
context: ProcessingContext,
result: CompletionResultSet
) {
MODULE_SECTIONS_KEYWORDS.forEach { s ->
result.addElement(
SmkCompletionUtil.createPrioritizedLookupElement(
TailTypeDecorator.withTail(
PythonLookupElement(s, true, PlatformIcons.PROPERTY_ICON),
ColonAndWhiteSpaceTail
),
priority = SmkCompletionUtil.SECTIONS_KEYS_PRIORITY
)
)
val element = parameters.position
filterByDeprecationAndAddLookupItems(element.project, MODULE_SECTIONS_KEYWORDS, result, priority = SmkCompletionUtil.SECTIONS_KEYS_PRIORITY) {
val smkModule = PsiTreeUtil.getParentOfType(element, SmkModule::class.java)
requireNotNull(smkModule) {
"According to CAPTURE should be inside module: <${element.text}> at ${element.textRange}"
}
smkModule.sectionKeyword
}
}
}
Expand Down Expand Up @@ -280,20 +278,107 @@ object UseSectionKeywordsProvider : CompletionProvider<CompletionParameters>() {
)
}

USE_SECTIONS_KEYWORDS.forEach { s ->
result.addElement(
SmkCompletionUtil.createPrioritizedLookupElement(
TailTypeDecorator.withTail(
PythonLookupElement(s, true, PlatformIcons.PROPERTY_ICON),
ColonAndWhiteSpaceTail
),
priority = SmkCompletionUtil.SECTIONS_KEYS_PRIORITY
)
)
filterByDeprecationAndAddLookupItems(
parameters.position.project,
USE_SECTIONS_KEYWORDS,
result,
priority = SmkCompletionUtil.SECTIONS_KEYS_PRIORITY
) {
SnakemakeNames.USE_KEYWORD
}
}
}

private fun filterByDeprecationAndAddLookupItems(
project: Project,
candidateKeywords: Iterable<String>,
result: CompletionResultSet,
priority: Double,
isTopLevel: Boolean = false,
customTailTypes: Map<String, TailType>? = null,
defaultTailType: TailType = ColonAndWhiteSpaceTail,
parentContextProvider: () -> String?
) {
val deprecationProvider = SmkFrameworkDeprecationProvider.getInstance()
val settings = SmkSupportProjectSettings.getInstance(project)
val contextName = parentContextProvider()

candidateKeywords.forEach { s ->
val versionInfo: String?
if (contextName != null) {
val introducedVersion = when {
isTopLevel -> deprecationProvider.getTopLevelIntroductionVersion(s)
else -> deprecationProvider.getSubSectionIntroductionVersion(s, contextName)
}
val deprecatedVersion = when {
isTopLevel -> deprecationProvider.getTopLevelDeprecationVersion(s)
else -> deprecationProvider.getSubSectionDeprecationVersion(s, contextName)
}
val removedVersion = when {
isTopLevel -> deprecationProvider.getTopLevelRemovedVersion(s)
else -> deprecationProvider.getSubSectionRemovalVersion(s, contextName)
}
val currentVersionString = settings.snakemakeLanguageVersion
val currentVersion = if (currentVersionString == null) null else SmkLanguageVersion(currentVersionString)

val issue = currentVersion?.let {
when {
isTopLevel -> deprecationProvider.getTopLevelCorrection(s, it)
else -> deprecationProvider.getSubsectionCorrection(s, it, contextName)
}
}
if (issue?.updateType == UpdateType.REMOVED) {
// removed in the current version
return@forEach
}

// version info:
val buf = StringBuffer()
if (introducedVersion != null) {
buf.append(">=${introducedVersion}")
}
if (deprecatedVersion != null) {
if (buf.isNotEmpty()) {
buf.append(", ")
}
buf.append("deprecated $deprecatedVersion")
}
if (removedVersion != null) {
if (buf.isNotEmpty()) {
buf.append(", ")
}
buf.append("removed $removedVersion")
}
// if (currentVersion != null) {
// if (issue?.updateType == UpdateType.DEPRECATED) {
// if (buf.isNotEmpty()) {
// buf.append(", ")
// }
// buf.append("deprecated in $currentVersion")
// }
// }
versionInfo = buf.toString()
} else {
versionInfo = null
}

var tailType = defaultTailType;
if (customTailTypes != null && s in customTailTypes.keys) {
tailType = customTailTypes[s]!!
}
result.addElement(
SmkCompletionUtil.createPrioritizedLookupElement(
TailTypeDecorator.withTail(
PythonLookupElement(s, null, versionInfo, true, PlatformIcons.PROPERTY_ICON, null),
tailType
),
priority = priority
)
)
}
}


fun PsiElementPattern.Capture<PsiElement>.insideOneOf(
vararg classes: Class<out PsiElement>
) = inside(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import com.intellij.openapi.components.Service
import com.jetbrains.snakecharm.SnakemakePluginUtil
import com.jetbrains.snakecharm.SnakemakeTestUtil
import com.jetbrains.snakecharm.lang.SmkLanguageVersion
import org.jetbrains.annotations.TestOnly
import org.yaml.snakeyaml.LoaderOptions
import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.constructor.CustomClassLoaderConstructor
import org.yaml.snakeyaml.introspector.BeanAccess
import java.io.InputStream
import java.nio.file.Path
import java.util.*
import kotlin.io.path.exists
import kotlin.io.path.inputStream
Expand All @@ -22,31 +24,43 @@ class SmkFrameworkDeprecationProvider {
private lateinit var subsectionCorrection: Map<Pair<String, String?>, TreeMap<SmkLanguageVersion, SmkKeywordUpdateData>>

private lateinit var subsectionIntroduction: Map<Pair<String, String?>, SmkLanguageVersion>
private lateinit var subsectionRemoval: Map<Pair<String, String?>, SmkLanguageVersion>
private lateinit var subsectionDeprecation: Map<Pair<String, String?>, SmkLanguageVersion>
private lateinit var topLevelIntroduction: Map<String, SmkLanguageVersion>
private lateinit var topLevelRemoval: Map<String, SmkLanguageVersion>
private lateinit var topLevelDeprecation: Map<String, SmkLanguageVersion>

private lateinit var defaultVersion: String

init {
val unitTestMode = ApplicationManager.getApplication().isUnitTestMode

val snakemakeApiFile = if (!unitTestMode) {
val pluginSandboxPath =
SnakemakePluginUtil.getPluginSandboxPath(SmkFrameworkDeprecationProvider::class.java)
pluginSandboxPath.resolve("extra/snakemake_api.yaml")
if (ApplicationManager.getApplication().isUnitTestMode) {
reinitializeInTests()
} else {
SnakemakeTestUtil.getTestDataPath().parent.resolve("snakemake_api.yaml")
val pluginSandboxPath = SnakemakePluginUtil.getPluginSandboxPath(
SmkFrameworkDeprecationProvider::class.java
)
initialize(pluginSandboxPath.resolve("extra/snakemake_api.yaml"))
}
}

@TestOnly
fun reinitializeInTests() {
require(ApplicationManager.getApplication().isUnitTestMode)
initialize(SnakemakeTestUtil.getTestDataPath().parent.resolve("snakemake_api.yaml"))
}

@TestOnly
fun reinitializeInTests(input: InputStream) {
initialize(input)
}

private fun initialize(snakemakeApiFile: Path) {
requireNotNull(!snakemakeApiFile.exists()) {
"Missing wrappers bundle in plugin bundle: '$snakemakeApiFile' doesn't exist"
}
initialize(snakemakeApiFile.inputStream())
}

fun overrideInputFile(input: InputStream) {
initialize(input)
}

private fun initialize(inputStream: InputStream?) {
val yaml = Yaml(CustomClassLoaderConstructor(SmkDeprecationData::class.java.classLoader, LoaderOptions()))
yaml.setBeanAccess(BeanAccess.FIELD) // it must be set to be able to set vals
Expand All @@ -57,7 +71,11 @@ class SmkFrameworkDeprecationProvider {
emptyMap<Pair<String, String?>, TreeMap<SmkLanguageVersion, SmkKeywordUpdateData>>().toMutableMap()

val subsectionIntroductionsMap = emptyMap<Pair<String, String?>, SmkLanguageVersion>().toMutableMap()
val subsectionDeprecationsMap = emptyMap<Pair<String, String?>, SmkLanguageVersion>().toMutableMap()
val subsectionRemovalMap = emptyMap<Pair<String, String?>, SmkLanguageVersion>().toMutableMap()
val topLevelIntroductionsMap = emptyMap<String, SmkLanguageVersion>().toMutableMap()
val topLevelDeprecationsMap = emptyMap<String, SmkLanguageVersion>().toMutableMap()
val topLevelRemovalMap = emptyMap<String, SmkLanguageVersion>().toMutableMap()

defaultVersion = deprecationData.defaultVersion
for (data in deprecationData.changelog) {
Expand Down Expand Up @@ -88,21 +106,9 @@ class SmkFrameworkDeprecationProvider {
putKeywordDataIntoMap(data.deprecated, UpdateType.DEPRECATED, mapGetter)
putKeywordDataIntoMap(data.removed, UpdateType.REMOVED, mapGetter)

for (introduction in data.introduced) {
when (introduction.type) {
TOP_LEVEL_KEYWORD_TYPE -> topLevelIntroductionsMap[introduction.name] = version
SUBSECTION_KEYWORD_TYPE -> {
if (introduction.parent.isEmpty()) {
subsectionIntroductionsMap[introduction.name to null] = version
}
for (parent in introduction.parent) {
subsectionIntroductionsMap[introduction.name to parent] = version
}
}

else -> throw IllegalArgumentException("Type ${introduction.type} introduction is not supported")
}
}
foo(topLevelIntroductionsMap, version, subsectionIntroductionsMap, data.introduced)
foo(topLevelDeprecationsMap, version, subsectionDeprecationsMap, data.deprecated)
foo(topLevelRemovalMap, version, subsectionRemovalMap, data.removed)
}

topLevelCorrection = topLevelData
Expand All @@ -111,7 +117,37 @@ class SmkFrameworkDeprecationProvider {

subsectionIntroduction = subsectionIntroductionsMap
topLevelIntroduction = topLevelIntroductionsMap
subsectionRemoval = subsectionRemovalMap
topLevelRemoval = topLevelRemovalMap
subsectionDeprecation = subsectionDeprecationsMap
topLevelDeprecation = topLevelDeprecationsMap
}

private fun foo(
topLevelEventsMap: MutableMap<String, SmkLanguageVersion>,
version: SmkLanguageVersion,
subsectionEventsMap: MutableMap<Pair<String, String?>, SmkLanguageVersion>,
events: List<SmkDeprecationKeywordData>
)
{
for (event in events) {
when (event.type) {
TOP_LEVEL_KEYWORD_TYPE -> topLevelEventsMap[event.name] = version
SUBSECTION_KEYWORD_TYPE -> {
if (event.parent.isEmpty()) {
subsectionEventsMap[event.name to null] = version
}
for (parent in event.parent) {
subsectionEventsMap[event.name to parent] = version
}
}
FUNCTION_KEYWORD_TYPE -> {
// TODO do nothing, functions processed isn't implemented, issue: ......
}

else -> throw IllegalArgumentException("Type ${event.type} is not supported")
}
}
}

fun getDefaultVersion() = defaultVersion
Expand Down Expand Up @@ -159,11 +195,24 @@ class SmkFrameworkDeprecationProvider {
fun getTopLevelIntroductionVersion(name: String): SmkLanguageVersion? {
return topLevelIntroduction[name]
}
fun getTopLevelRemovedVersion(name: String): SmkLanguageVersion? {
return topLevelRemoval[name]
}
fun getTopLevelDeprecationVersion(name: String): SmkLanguageVersion? {
return topLevelDeprecation[name]
}

fun getSubSectionIntroductionVersion(name: String, parent: String): SmkLanguageVersion? {
return subsectionIntroduction[name to parent] ?: subsectionIntroduction[name to null]
}

fun getSubSectionDeprecationVersion(name: String, parent: String): SmkLanguageVersion? {
return subsectionDeprecation[name to parent] ?: subsectionDeprecation[name to null]
}
fun getSubSectionRemovalVersion(name: String, parent: String): SmkLanguageVersion? {
return subsectionRemoval[name to parent] ?: subsectionRemoval[name to null]
}

private fun getKeywordCorrection(
keywords: TreeMap<SmkLanguageVersion, SmkKeywordUpdateData>?,
version: SmkLanguageVersion,
Expand Down
Loading

0 comments on commit 0fd6926

Please sign in to comment.