Skip to content

Commit

Permalink
Compute dependency version inlay hints outside of read action
Browse files Browse the repository at this point in the history
  • Loading branch information
InSyncWithFoo committed Nov 20, 2024
1 parent fb57aab commit f8abb94
Show file tree
Hide file tree
Showing 12 changed files with 283 additions and 155 deletions.
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ See [the documentation][0.1.0-a4-1] for more information.

Latest tool versions at the time of release:

* Ruff: [0.7.3][0.1.0-a4-2]
* Ruff: [0.7.4][0.1.0-a4-2]
* uv: [0.5.2][0.1.0-a4-3]
* Rye: [0.42.0][0.1.0-a4-4]

Expand Down Expand Up @@ -45,7 +45,7 @@ Latest tool versions at the time of release:
* Groups can be installed by clicking their corresponding line markers.

* Usages of [`uv.dev-dependencies`][0.1.0-a4-a-4] are now reported.
This field is deprecated as of [uv 0.2.27][0.1.0-a4-a-5];
This field is deprecated as of [uv 0.4.27][0.1.0-a4-a-5];
it should be replaced with `dependency-groups.dev`.

* Dependency specifier strings in `pyproject.toml` and `uv.toml`
Expand Down Expand Up @@ -98,7 +98,7 @@ Latest tool versions at the time of release:


[0.1.0-a4-1]: https://insyncwithfoo.github.io/ryecharm/
[0.1.0-a4-2]: https://github.com/astral-sh/ruff/releases/tag/0.7.3
[0.1.0-a4-2]: https://github.com/astral-sh/ruff/releases/tag/0.7.4
[0.1.0-a4-3]: https://github.com/astral-sh/uv/releases/tag/0.5.2
[0.1.0-a4-4]: https://github.com/astral-sh/rye/releases/tag/0.42.0
[0.1.0-a4-a-1]: https://peps.python.org/pep-0735/
Expand Down
13 changes: 10 additions & 3 deletions docs/other-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,16 @@ with the corresponding interpreter as argument, if any.

!!! note

Due to technical limitations, this feature will cause a
"synchronous execution under read action" exception to be logged.
If you wish <em>not</em> to ignore this error, disable the feature.
On IntelliJ IDEA, flickering might happen during computation.
The cause of this problem is as of yet unknown.

As a workaround, enable the "Retrieve data for computing dependency version
inlay hints in read action" advanced setting in the <i>uv</i> subpanel.

This workaround has the disadvantage of delaying
the computation of other inlay hint providers,
causing a "synchronous execution under read action" exception.
Unless the delay proves to be a problem, you can safely ignore this warning.


### Dependency groups
Expand Down
7 changes: 3 additions & 4 deletions docs/uv/generating.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ as [the default (<i>Pure Python</i>) panel][1].
![](../assets/uv-new-project-panel.png)

!!! note
Due to technical limitations, it is currently
not possible to extend the standard panel.
The <i>uv</i> panel cannot be used with existing framework panels.
This is expected to change in PyCharm 2024.3.
Due to technical limitations, it is not possible to
extend the standard panel. In other words,
uv cannot be used to generate framework-based projects.


## Settings
Expand Down
47 changes: 41 additions & 6 deletions src/main/kotlin/insyncwithfoo/ryecharm/NotificationActions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,34 @@ import com.intellij.notification.BrowseNotificationAction
import com.intellij.notification.Notification
import com.intellij.notification.NotificationAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.fileEditor.OpenFileDescriptor
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.util.ui.EmptyClipboardOwner
import insyncwithfoo.ryecharm.configurations.PanelBasedConfigurable
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection


internal fun Notification.addOpenPluginIssueTrackerAction(): Notification {
val issueTrackerActionText = message("notificationActions.openPluginIssueTracker")
return addAction(BrowseNotificationAction(issueTrackerActionText, RyeCharm.ISSUE_TRACKER))
}
internal fun Notification.addAction(text: String, action: () -> Unit) =
addAction(NotificationAction.createSimple(text, action))


internal fun Notification.addExpiringAction(text: String, action: () -> Unit) =
addAction(NotificationAction.createSimpleExpiring(text, action))


internal fun Notification.addAction(text: String, action: () -> Unit) =
addAction(NotificationAction.createSimple(text, action))
internal fun Notification.addOpenBrowserAction(text: String, link: String) =
addAction(BrowseNotificationAction(text, link))


internal fun Notification.addOpenPluginIssueTrackerAction() {
val text = message("notificationActions.openPluginIssueTracker")
val link = RyeCharm.ISSUE_TRACKER

addOpenBrowserAction(text, link)
}


internal fun Notification.addCopyTextAction(text: String, content: String) {
Expand Down Expand Up @@ -58,6 +66,33 @@ internal fun Notification.addOpenSettingsAction(
}


private class OpenFileAction(text: String, private val path: String) : NotificationAction(text) {

override fun actionPerformed(event: AnActionEvent, notification: Notification) {
val project = event.project ?: return cannotOpenFile()

val fileSystem = LocalFileSystem.getInstance()
val virtualFile = fileSystem.findFileByPath(path) ?: return cannotOpenFile(project)

project.fileEditorManager.openFileEditor(OpenFileDescriptor(project, virtualFile), true)
}

private fun cannotOpenFile(project: Project? = null) {
val title = message("notifications.cannotOpenFile.title")
val content = message("notifications.cannotOpenFile.body", path)

project.somethingIsWrong(title, content)
}

}


internal fun Notification.addOpenFileAction(text: String? = null, path: String) {
addAction(OpenFileAction(text ?: message("notificationActions.openFile"), path))
}



internal class OpenTemporaryFileAction(
text: String,
private val filename: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ internal abstract class AdaptivePanel<S>(val state: S, private val overrides: Ov
toggleOtherCellsBasedOn(checkbox)
}

@Suppress("DialogTitleCapitalization")
fun Panel.advancedSettingsGroup(init: Panel.() -> Unit) {
collapsibleGroup(message("configurations.groups.advanced"), init = init)
}

@Suppress("DialogTitleCapitalization")
fun <E> Panel.timeoutGroup(timeouts: TimeoutMap, entries: List<E>) where E : Commented, E : Keyed {
collapsibleGroup(message("configurations.timeouts.groupName"), indent = true) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ private fun RuffPanel.makeComponent() = panel {
}
}

collapsibleGroup(message("configurations.ruff.groups.advanced")) {
advancedSettingsGroup {
row {
autoRestartServersInput { bindSelected(state::autoRestartServers) }
overrideCheckbox(state::autoRestartServers)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ internal class UVConfigurations : DisplayableState(), HasTimeouts {
var packageManaging by property(true)
var packageManagingNonUVProject by property(false)

var retrieveDependenciesInReadAction by property(false)

override var timeouts by map<SettingName, MillisecondsOrNoLimit>()
}

Expand Down
11 changes: 11 additions & 0 deletions src/main/kotlin/insyncwithfoo/ryecharm/configurations/uv/Panels.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ private fun Row.packageManagingNonUVProjectInput(block: Cell<JBCheckBox>.() -> U
checkBox(message("configurations.uv.packageManagingNonUVProject.label")).apply(block)


private fun Row.retrieveDependenciesInReadActionInput(block: Cell<JBCheckBox>.() -> Unit) =
checkBox(message("configurations.uv.retrieveDependenciesInReadAction.label")).apply(block)


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

Expand Down Expand Up @@ -72,6 +76,13 @@ private fun UVPanel.makeComponent() = panel {
}
}

advancedSettingsGroup {
row {
retrieveDependenciesInReadActionInput { bindSelected(state::retrieveDependenciesInReadAction) }
overrideCheckbox(state::retrieveDependenciesInReadAction)
}
}

timeoutGroup(state.timeouts, UVTimeouts.entries)

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package insyncwithfoo.ryecharm.uv.inlayhints.dependencyversion

import com.intellij.codeInsight.hints.declarative.HintFontSize
import com.intellij.codeInsight.hints.declarative.HintFormat
import com.intellij.codeInsight.hints.declarative.InlayTreeSink
import com.intellij.codeInsight.hints.declarative.InlineInlayPosition
import com.intellij.codeInsight.hints.declarative.OwnBypassCollector
import com.intellij.codeInsight.hints.declarative.SharedBypassCollector
import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.endOffset
import com.jetbrains.python.packaging.common.PythonPackage
import insyncwithfoo.ryecharm.TOMLPath
import insyncwithfoo.ryecharm.absoluteName
import insyncwithfoo.ryecharm.completedAbnormally
import insyncwithfoo.ryecharm.interpreterPath
import insyncwithfoo.ryecharm.isPyprojectToml
import insyncwithfoo.ryecharm.isString
import insyncwithfoo.ryecharm.keyValuePair
import insyncwithfoo.ryecharm.message
import insyncwithfoo.ryecharm.module
import insyncwithfoo.ryecharm.pep508Normalize
import insyncwithfoo.ryecharm.runInBackground
import insyncwithfoo.ryecharm.stringContent
import insyncwithfoo.ryecharm.uv.commands.uv
import insyncwithfoo.ryecharm.uv.inlayhints.dependencyversion.settings.Settings
import insyncwithfoo.ryecharm.uv.inlayhints.dependencyversion.settings.dependencyVersionInlayHintsSettings
import insyncwithfoo.ryecharm.uv.parsePipListOutput
import org.toml.lang.psi.TomlArray
import org.toml.lang.psi.TomlFile
import org.toml.lang.psi.TomlLiteral
import java.nio.file.Path


internal typealias DependencyName = String
internal typealias DependencyVersion = String
internal typealias DependencyMap = Map<DependencyName, DependencyVersion>
internal typealias DependencyList = List<PythonPackage>


// https://peps.python.org/pep-0508/#names
private val dependencySpecifierLookAlike = """(?i)^\s*(?<name>[A-Z0-9](?:[A-Z0-9._-]*[A-Z0-9])?).*""".toRegex()


private const val CONTINUE_PROCESSING = true


private fun DependencyList.toMap(): DependencyMap =
this.associate { it.name.pep508Normalize() to it.version }


internal suspend fun Project.getInstalledDependencies(interpreter: Path?): DependencyMap? {
val uv = this.uv ?: return null
val command = uv.pipList(python = interpreter)

val output = runInBackground(command)

return when {
output.completedAbnormally -> null
else -> parsePipListOutput(output.stdout)?.toMap()
}
}


internal class DependencyVersionInlayHintsCollector(private val dependencies: DependencyMap?) : OwnBypassCollector {

constructor() : this(dependencies = null)

override fun collectHintsForFile(file: PsiFile, sink: InlayTreeSink) {
if (file !is TomlFile) {
return
}

val virtualFile = file.virtualFile ?: return
val dependencies = this.dependencies ?: getInstalledDependencies(file) ?: return
val settings = dependencyVersionInlayHintsSettings

PsiTreeUtil.processElements(file, TomlLiteral::class.java) { element ->
if (shouldShowHint(element, virtualFile, settings)) {
sink.addVersionHint(element, dependencies)
}
CONTINUE_PROCESSING
}
}

@Suppress("UnstableApiUsage")
private fun getInstalledDependencies(file: PsiFile) = runBlockingCancellable {
val (project, module) = Pair(file.project, file.module)
project.getInstalledDependencies(module?.interpreterPath)
}

private fun shouldShowHint(element: TomlLiteral, file: VirtualFile, settings: Settings): Boolean {
val string = element.takeIf { it.isString } ?: return false
val array = string.parent as? TomlArray ?: return false
val keyValuePair = array.keyValuePair ?: return false

val key = keyValuePair.key
val absoluteName = key.absoluteName

return when {
file.isPyprojectToml -> shouldShowHintForPyprojectTomlField(absoluteName, settings)
else -> shouldShowHintForUVField(absoluteName, settings)
}
}

private fun shouldShowHintForPyprojectTomlField(keyName: TOMLPath, settings: Settings): Boolean {
when {
keyName == TOMLPath("project.dependencies") -> return settings.projectDependencies
keyName == TOMLPath("build-system.requires") -> return settings.buildSystemRequires
keyName isChildOf "project.optional-dependencies" -> return settings.projectOptionalDependencies
keyName isChildOf "dependency-groups" -> return settings.dependencyGroups
}

val relativeName = keyName.relativize("tool.uv") ?: return false

return shouldShowHintForUVField(relativeName, settings)
}

private fun shouldShowHintForUVField(keyName: TOMLPath, settings: Settings) =
when (keyName) {
TOMLPath("constraint-dependencies") -> settings.uvConstraintDependencies
TOMLPath("dev-dependencies") -> settings.uvDevDependencies
TOMLPath("override-dependencies") -> settings.uvOverrideDependencies
TOMLPath("upgrade-package") -> settings.uvUpgradePackage
TOMLPath("pip.upgrade-package") -> settings.uvPipUpgradePackage
else -> false
}

private fun InlayTreeSink.addVersionHint(element: TomlLiteral, dependencies: DependencyMap) {
val match = dependencySpecifierLookAlike.matchEntire(element.stringContent!!) ?: return
val dependencyName = match.groups["name"]?.value?.pep508Normalize() ?: return
val dependencyVersion = dependencies[dependencyName] ?: return

addVersionHint(dependencyName, dependencyVersion, element.endOffset)
}

private fun InlayTreeSink.addVersionHint(name: DependencyName, version: DependencyVersion, offset: Int) {
val position = InlineInlayPosition(offset, relatedToPrevious = false)
val hintFormat = HintFormat.default.withFontSize(HintFontSize.ABitSmallerThanInEditor)
val tooltip = message("inlayHints.uv.dependencyVersions.tooltip", name, version)

addPresentation(position, payloads = null, tooltip, hintFormat) {
text(version)
}
}

}
Loading

0 comments on commit f8abb94

Please sign in to comment.