Skip to content

Commit

Permalink
Show list of rules for selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
InSyncWithFoo committed Feb 7, 2025
1 parent 95924af commit 42ba034
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 51 deletions.
2 changes: 0 additions & 2 deletions src/main/kotlin/insyncwithfoo/ryecharm/Paths.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import java.nio.file.InvalidPathException
import java.nio.file.Path
import kotlin.io.path.div
import kotlin.io.path.getLastModifiedTime
import kotlin.io.path.isDirectory
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.nameWithoutExtension


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@ internal abstract class PanelBasedConfigurable<S : BaseState> : Configurable {


internal val <S : BaseState> PanelBasedConfigurable<S>.projectAndOverrides: Pair<Project?, Overrides?>
get() = try {
(this as ProjectBasedConfigurable)
.run { Pair(project, overrides) }
} catch (_: ClassCastException) {
Pair(null, null)
get() = when (this is ProjectBasedConfigurable) {
true -> Pair(project, overrides)
else -> Pair(null, null)
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ internal val Project.ruffExecutable: Path?


/**
* The UV executable associated with this project, if it exists.
* The uv executable associated with this project, if it exists.
*/
internal val Project.uvExecutable: Path?
get() = uvConfigurations.executable?.toPathIfItExists() ?: UV.detectExecutable()
Expand Down Expand Up @@ -98,8 +98,3 @@ internal fun Project.changeRuffConfigurations(action: RuffConfigurations.() -> U
internal fun Project.changeRuffOverrides(action: Overrides.() -> Unit) {
RuffOverrideService.getInstance(this).state.names.apply(action)
}


internal fun changeGlobalUVConfigurations(action: UVConfigurations.() -> Unit) {
UVGlobalService.getInstance().state.apply(action)
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ private fun Row.dependenciesDataMaxAgeInput(block: Cell<JBIntSpinner>.() -> Unit
spinner(0..1_000_000).apply(block)


@Suppress("DialogTitleCapitalization")
private fun UVPanel.makeComponent() = panel {

row(message("configurations.uv.executable.label")) {
Expand Down
6 changes: 0 additions & 6 deletions src/main/kotlin/insyncwithfoo/ryecharm/ruff/NoqaComment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,6 @@ internal class NoqaComment private constructor(
val separator: String
get() = lastSeparator ?: ", "

val codeListRange: IntRange
get() = when (colon) {
null -> prefix.end..<prefix.end
else -> codes.first().start..<codes.last().end
}

fun findCodeAtOffset(offset: Int) =
codes.firstNotNullOfOrNull { code ->
code.content.takeIf { offset in code.inclusiveRange }
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/insyncwithfoo/ryecharm/ruff/RuffCache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import insyncwithfoo.ryecharm.RootDisposable
import insyncwithfoo.ryecharm.RyeCharm
import insyncwithfoo.ryecharm.parseAsJSON
import insyncwithfoo.ryecharm.propertiesComponent
import insyncwithfoo.ryecharm.ruff.documentation.RuleName
import insyncwithfoo.ryecharm.stringifyToJSON
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty
Expand Down Expand Up @@ -84,7 +85,7 @@ internal class RuffCache(private val project: Project) {
/**
* Store part of the output of `ruff rule --all`.
*/
var ruleNameToCodeMap: CachedResult<Map<String, String>>?
var ruleNameToCodeMap: CachedResult<Map<RuleName, RuleCode>>?
get() = RuffCache::ruleNameToCodeMap.getStoredValue()
set(value) = RuffCache::ruleNameToCodeMap.setStoredValue(value)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,62 @@ import insyncwithfoo.ryecharm.Markdown
import insyncwithfoo.ryecharm.ProgressContext
import insyncwithfoo.ryecharm.completedAbnormally
import insyncwithfoo.ryecharm.isSuccessful
import insyncwithfoo.ryecharm.message
import insyncwithfoo.ryecharm.parseAsJSON
import insyncwithfoo.ryecharm.processTimeout
import insyncwithfoo.ryecharm.ruff.CachedResult
import insyncwithfoo.ryecharm.ruff.RuffCache
import insyncwithfoo.ryecharm.ruff.RuleCode
import insyncwithfoo.ryecharm.ruff.commands.ruff
import insyncwithfoo.ryecharm.ruff.ruleCode
import insyncwithfoo.ryecharm.runInBackground
import insyncwithfoo.ryecharm.toHTML


/**
* Either a full code (e.g., `RUF100`) or a prefix (e.g., `RUF`, `RUF1`, `RUF10`).
*/
internal typealias RuleSelector = String

/**
* A rule name (e.g., `unused-noqa`).
*/
internal typealias RuleName = String

/**
* Either a [RuleSelector] or a [RuleName].
*/
internal typealias RuleSelectorOrName = String


/**
* A fork of [insyncwithfoo.ryecharm.ruff.ruleCode].
*/
private val ruleSelector = """(?<linter>[A-Z]+)(?<number>[0-9]*)""".toRegex()


private val optionsSection = """(?mx)
^\#\#\h*Options\n
(?:[\s\S](?!\n\#))*
""".trimIndent().toRegex()


private val optionNameInListItem = """(?m)(?<prefix>^[-*]\h*\[?)`(?<path>[A-Za-z0-9.-]+)`""".toRegex()
private val ruleLink = """https://docs\.astral\.sh/ruff/rules/(?<rule>[a-z-]+)""".toRegex()
private val ruleLink = """https://docs\.astral\.sh/ruff/rules/(?<rule>[a-z-]+)/?""".toRegex()


internal val String.isRuleSelector: Boolean
get() = ruleSelector.matchEntire(this) != null


private val String.isRuleCode: Boolean
get() = ruleCode.matchEntire(this) != null
private val RuleSelectorOrName.isPylintCodePrefix: Boolean
get() = this in listOf("PLC", "PLE", "PLR", "PLW")


// https://github.com/astral-sh/ruff/issues/14348
/**
* Replace option names with links to specialized URIs.
*/
private fun String.insertOptionLinks() = this.replace(optionsSection) {
private fun Markdown.insertOptionLinks() = this.replace(optionsSection) {
it.value.replace(optionNameInListItem) { match ->
val prefix = match.groups["prefix"]!!.value
val path = match.groups["path"]!!.value
Expand All @@ -49,15 +75,15 @@ private fun String.insertOptionLinks() = this.replace(optionsSection) {
}


private fun String.replaceRuleLinksWithSpecializedURIs() = this.replace(ruleLink) {
private fun Markdown.replaceRuleLinksWithSpecializedURIs() = this.replace(ruleLink) {
val rule = it.groups["rule"]!!.value
val uri = DocumentationURI(RUFF_RULE_HOST, rule)

uri.toString()
}


private suspend fun Project.getNewRuleNameToCodeMap(): Map<RuleCode, String>? {
private suspend fun Project.getNewRuleNameToCodeMap(): Map<RuleName, RuleCode>? {
val ruff = this.ruff ?: return null
val command = ruff.allRules()

Expand All @@ -75,31 +101,26 @@ private suspend fun Project.getNewRuleNameToCodeMap(): Map<RuleCode, String>? {
}


private suspend fun Project.getCodeForRuleName(name: String): String? {
private suspend fun Project.getRuleNameToCodeMap(): Map<RuleName, RuleCode>? {
val ruff = this.ruff ?: return null
val executable = ruff.executable

val cache = RuffCache.getInstance(this)
val cached = cache.ruleNameToCodeMap

if (cached?.matches(executable) == true) {
return cached.result[name]
return cached.result
}

val newData = getNewRuleNameToCodeMap()?.also {
cache.ruleNameToCodeMap = CachedResult(it, executable)
}

return newData?.get(name)
return newData
}


private suspend fun Project.getRuleMarkdownDocumentation(rule: String): Markdown? {
val code = when (rule.isRuleCode) {
true -> rule
else -> getCodeForRuleName(rule) ?: return null
}

private suspend fun Project.getRuleDocumentationByFullCode(code: RuleCode): Markdown? {
val ruff = this.ruff ?: return null
val command = ruff.rule(code)

Expand All @@ -117,16 +138,53 @@ private suspend fun Project.getRuleMarkdownDocumentation(rule: String): Markdown
}

return output.stdout
.insertOptionLinks()
.replaceRuleLinksWithSpecializedURIs()
}


internal suspend fun Project.getRuleDocumentation(rule: String): HTML? {
val markdownDocumentation = getRuleMarkdownDocumentation(rule) ?: return null
private suspend fun Project.getRuleDocumentationByRuleName(name: RuleName) =
getRuleNameToCodeMap()?.get(name)?.let {
getRuleDocumentationByFullCode(it)
}


private suspend fun Project.getRuleListByPrefix(selector: RuleSelector): Markdown? {
val nameToCodeMap = getRuleNameToCodeMap() ?: return null
val ruleList = StringBuilder()

for ((name, code) in nameToCodeMap) {
// TODO: Precise prefix matching
if (code.startsWith(selector)) {
val uri = DocumentationURI(RUFF_RULE_HOST, code)
ruleList.append("\n* [`$name`]($uri) (`$code`)")
}
}

return when (ruleList.isEmpty()) {
true -> message("documentation.popup.ruleList.empty", selector)
else -> message("documentation.popup.ruleList", selector, ruleList)
}
}


internal suspend fun Project.getRuleDocumentationOrList(selectorOrName: RuleSelectorOrName): HTML? {
val match = ruleSelector.matchEntire(selectorOrName)

val markdownDocumentation = when (match == null) {
true -> getRuleDocumentationByRuleName(selectorOrName)

else -> {
val (linter, number) = match.destructured

when (linter.isPylintCodePrefix && number.length == 4 || number.length == 3) {
true -> getRuleDocumentationByFullCode(selectorOrName)
else -> getRuleListByPrefix(selectorOrName)
}
}
}

return readAction {
markdownDocumentation
.insertOptionLinks()
.replaceRuleLinksWithSpecializedURIs()
.toHTML()
return markdownDocumentation?.let {
readAction { it.toHTML() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import insyncwithfoo.ryecharm.isPyprojectToml
import insyncwithfoo.ryecharm.isRuffToml
import insyncwithfoo.ryecharm.isString
import insyncwithfoo.ryecharm.keyValuePair
import insyncwithfoo.ryecharm.ruff.documentation.isRuleSelector
import insyncwithfoo.ryecharm.ruff.documentation.targets.RuffRuleDocumentationTarget
import insyncwithfoo.ryecharm.stringContent
import insyncwithfoo.ryecharm.wrappingTomlLiteral
Expand Down Expand Up @@ -97,7 +98,13 @@ internal class RuffRuleDocumentationTargetProvider : DocumentationTargetProvider
return null
}

return RuffRuleDocumentationTarget(this, stringContent!!)
val selector = stringContent!!

if (!selector.isRuleSelector) {
return null
}

return RuffRuleDocumentationTarget(this, selector)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package insyncwithfoo.ryecharm.ruff.documentation.targets
import com.intellij.platform.backend.documentation.DocumentationResult
import com.intellij.psi.PsiElement
import insyncwithfoo.ryecharm.ruff.NoqaComment
import insyncwithfoo.ryecharm.ruff.documentation.getRuleDocumentation
import insyncwithfoo.ryecharm.ruff.documentation.getRuleDocumentationOrList
import insyncwithfoo.ryecharm.ruff.documentation.providers.NoqaCodeDocumentationTargetProvider
import insyncwithfoo.ryecharm.toDocumentationResult

Expand All @@ -24,7 +24,7 @@ internal class NoqaCodeDocumentationTarget(
val ruleCode = noqaComment.findCodeAtOffset(offset) ?: return null

return DocumentationResult.asyncDocumentation {
project.getRuleDocumentation(ruleCode)?.toDocumentationResult()
project.getRuleDocumentationOrList(ruleCode)?.toDocumentationResult()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package insyncwithfoo.ryecharm.ruff.documentation.targets

import com.intellij.platform.backend.documentation.DocumentationResult
import com.intellij.psi.PsiElement
import insyncwithfoo.ryecharm.ruff.documentation.getRuleDocumentation
import insyncwithfoo.ryecharm.ruff.documentation.RuleSelectorOrName
import insyncwithfoo.ryecharm.ruff.documentation.getRuleDocumentationOrList
import insyncwithfoo.ryecharm.ruff.documentation.providers.RuffRuleDocumentationTargetProvider
import insyncwithfoo.ryecharm.toDocumentationResult

Expand All @@ -12,14 +13,14 @@ import insyncwithfoo.ryecharm.toDocumentationResult
*/
internal class RuffRuleDocumentationTarget(
override val element: PsiElement,
private val rule: String
private val selectorOrName: RuleSelectorOrName
) : RuffDocumentationTarget() {

override fun fromDereferenced(element: PsiElement) =
RuffRuleDocumentationTarget(element, rule)
RuffRuleDocumentationTarget(element, selectorOrName)

override fun computeDocumentation() = DocumentationResult.asyncDocumentation {
element.project.getRuleDocumentation(rule)?.toDocumentationResult()
element.project.getRuleDocumentationOrList(selectorOrName)?.toDocumentationResult()
}

}
5 changes: 5 additions & 0 deletions src/main/resources/messages/ryecharm.properties
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,11 @@ documentation.popup.configGroupInfo = \
\n\
{1}

documentation.popup.ruleList.empty = There are no rules starting with `{0}`.
documentation.popup.ruleList = \
Rules starting with `{0}`:\n\
{1}

############################################################ endregion

############################################################ region # Line markers
Expand Down

0 comments on commit 42ba034

Please sign in to comment.