Skip to content

Commit ff427d7

Browse files
committed
Add plugin settings and prepare for future updates
1 parent b4fc01a commit ff427d7

9 files changed

Lines changed: 244 additions & 148 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# Changelog
22

33
## [Unreleased]
4+
### Added
5+
- Detail plugin settings that let you disable parameter hints selectively
46

57
## [0.1.4] - 2022-07-20
6-
### Changed
7-
- Lower minimal build requirement to 2021.2
8+
### Changed
9+
- Lower minimal build requirement to 2021.2
810

9-
### Fixed
11+
### Fixed
1012
- Hints not showing when there's one positional parameter with `**kwargs`
1113

1214
## [0.1.3] - 2022-07-19

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Pretty much any expression is supported:
2929
* dataclass and class instantiation
3030
* lambdas
3131
* decorators
32-
* function calls
32+
* function and method calls
3333
* etc.
3434

3535
<!-- Plugin description end -->
@@ -38,10 +38,12 @@ Pretty much any expression is supported:
3838

3939
## Settings
4040

41-
The plugin can be disabled anytime in IDE settings:
41+
The plugin can be disabled entirely in the IDE settings:
4242

4343
<kbd>Settings</kbd> -> <kbd>Editor</kbd> -> <kbd>Inlay Hints</kbd> -> <kbd>Parameter names</kbd> -> <kbd>Python</kbd>
4444

45+
There are also more detailed settings in the same section that let you control the behavior of the plugin.
46+
4547
## Demo Screenshot
4648

4749
![](.github/readme/plugin_demo.png)

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
pluginGroup = space.whitememory.pythoninlayparams
22
pluginName = Python Inlay Params
3-
pluginVersion = 0.1.4
3+
pluginVersion = 0.2.0
44

55
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
66
# for insight into build numbers and IntelliJ Platform versions.
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package space.whitememory.pythoninlayparams
2+
3+
import com.intellij.codeInsight.hints.FactoryInlayHintsCollector
4+
import com.intellij.codeInsight.hints.InlayHintsSink
5+
import com.intellij.codeInsight.hints.InlayInfo
6+
import com.intellij.openapi.editor.Editor
7+
import com.intellij.psi.PsiElement
8+
import com.intellij.psi.util.PsiTreeUtil
9+
import com.jetbrains.python.psi.*
10+
import com.jetbrains.python.psi.types.TypeEvalContext
11+
12+
@Suppress("UnstableApiUsage")
13+
class PythonInlayHintsCollector(
14+
editor: Editor, private val settings: PythonInlayHintsProvider.Settings
15+
) : FactoryInlayHintsCollector(editor) {
16+
17+
private val forbiddenBuiltinFiles = setOf("builtins.pyi", "typing.pyi")
18+
19+
override fun collect(element: PsiElement, editor: Editor, sink: InlayHintsSink): Boolean {
20+
// This method gets every element in the editor,
21+
// so we have to verify it's a Python call expression
22+
if (element !is PyCallExpression || element is PyDecorator) {
23+
return true
24+
}
25+
26+
// Don't show hints if there's no arguments
27+
// Or the only argument is unpacking (*list, **dict)
28+
if (element.arguments.isEmpty() || (element.arguments.size == 1 && element.arguments[0] is PyStarArgument)) {
29+
return true
30+
}
31+
32+
// Try to resolve the object that made this call
33+
var resolved = element.callee?.reference?.resolve() ?: return true
34+
if (isForbiddenBuiltinElement(resolved)) {
35+
return true
36+
}
37+
38+
var classAttributes = listOf<PyTargetExpression>()
39+
if (resolved is PyTargetExpression && settings.lambdaHints) {
40+
// TODO: Handle cases other than lambda expressions
41+
// Use the target to find the lambda expression object, and assign it to get its parameters up ahead
42+
resolved = PsiTreeUtil.getNextSiblingOfType(resolved, PyLambdaExpression::class.java) ?: return true
43+
} else if (resolved is PyClass && settings.classHints) {
44+
// This call is made by a class (initialization), so we want to find the parameters it takes.
45+
// In order to do so, we first have to check for an init method, and if not found,
46+
// We will use the class attributes instead. (Handle dataclasses, attrs, etc.)
47+
val evalContext = TypeEvalContext.codeAnalysis(element.project, element.containingFile)
48+
val entryMethod = resolved.findInitOrNew(true, evalContext)
49+
50+
resolved = if (entryMethod != null && entryMethod.containingClass == resolved) {
51+
entryMethod
52+
} else {
53+
// Use the class attributes if there's no init with params in the parent classes
54+
// TODO: Make sure wrong attributes are not used
55+
classAttributes = resolved.classAttributes
56+
entryMethod ?: resolved
57+
}
58+
} else if (!settings.functionHints) {
59+
return true
60+
}
61+
62+
val resolvedParameters = getElementFilteredParameters(resolved)
63+
val finalParameters = if (resolvedParameters.isEmpty() && classAttributes.isNotEmpty()) {
64+
// If there's no parameters in the object,
65+
// we use the class attributes instead,
66+
// in case this is a class
67+
classAttributes
68+
} else if (resolvedParameters.isEmpty()) {
69+
return true
70+
} else {
71+
resolvedParameters
72+
}
73+
74+
if (finalParameters.size == 1) {
75+
// Don't need a hint if there's only one parameter,
76+
// Make an exception for *args
77+
finalParameters[0].let {
78+
if (it !is PyNamedParameter || !it.isPositionalContainer) return true
79+
}
80+
}
81+
82+
getInlayInfos(finalParameters, element.arguments).forEach {
83+
val hintText = factory.smallText("${it.text}:")
84+
val presentation = factory.roundWithBackground(hintText)
85+
86+
sink.addInlineElement(it.offset, false, presentation, false)
87+
}
88+
89+
return true
90+
}
91+
92+
/**
93+
* Gets the list of [InlayInfo] that represents parameters worth showing along with their offset.
94+
*/
95+
private fun getInlayInfos(parameters: List<PyElement>, arguments: Array<PyExpression>): MutableList<InlayInfo> {
96+
val inlayInfos = mutableListOf<InlayInfo>()
97+
98+
parameters.zip(arguments).forEach { (param, arg) ->
99+
val paramName = param.name ?: return@forEach
100+
if (arg is PyStarArgument || arg is PyKeywordArgument) {
101+
// It's a keyword argument or unpacking,
102+
// we don't need to show hits after this
103+
return inlayInfos
104+
}
105+
106+
if (param is PyNamedParameter) {
107+
if (param.isPositionalContainer) {
108+
// This is an *args parameter that takes more than one argument
109+
// So we show it and stop the further processing of this call expression
110+
inlayInfos.add(InlayInfo("...$paramName", arg.textOffset))
111+
return inlayInfos
112+
} else if (param.isKeywordContainer) {
113+
// We don't want to show `kwargs` as a hint by accident
114+
return inlayInfos
115+
}
116+
}
117+
118+
if (isHintNameValid(paramName, arg)) {
119+
inlayInfos.add(InlayInfo(paramName, arg.textOffset))
120+
}
121+
}
122+
123+
return inlayInfos
124+
}
125+
126+
/**
127+
* Checks if the given parameter name is valid for the given argument.
128+
* This is used to skip parameters that start with __, or are the same as the argument.
129+
*/
130+
private fun isHintNameValid(name: String, argument: PyExpression): Boolean {
131+
// TODO: More filters
132+
return name != argument.name?.lowercase() && !name.startsWith("__") && name.length > 1
133+
}
134+
135+
/**
136+
* Get the parameters of the element, but filter out the ones that are not needed.
137+
* For example, if the element is a class method, we don't want to show the __self__ parameter.
138+
*/
139+
private fun getElementFilteredParameters(element: PsiElement): List<PyParameter> {
140+
element.children.forEach {
141+
if (it is PyParameterList) {
142+
return it.parameters.filter { param -> !param.isSelf }
143+
}
144+
}
145+
return emptyList()
146+
}
147+
148+
/**
149+
* Checks if the element is part of the standard library that isn't relevant for these hints.
150+
*/
151+
private fun isForbiddenBuiltinElement(element: PsiElement): Boolean {
152+
// TODO: Implement using PyType.isBuiltin (?),
153+
// although we still want some builtins like datetime.datetime
154+
return element.containingFile.name in forbiddenBuiltinFiles
155+
}
156+
}

0 commit comments

Comments
 (0)