PY-62208 Include importable names in basic completion results

Previously, such names were visible only on so-called "extended" completion,
activated when the hotkey for the basic completion was hit twice. The main reason
was that collecting such variants from indexes was a slow process, and we
didn't want to harm the responsiveness of completion for basic names.
Now it becomes possible thanks to a number of performance optimizations:

* Instead of using three separate indexes for classes, functions and variables,
we use one -- PyExportedModuleAttributeIndex. By definition, it includes only top-level
"importable" names, so we additionally save time by not filtering out irrelevant
entries. Also, it doesn't contain private definitions starting with an underscore.
It might bother some users, but given that the previous completion was used
extremely rarely, and the new one is going to be visible everywhere, it seems
that pruning unlikely entries as much as possible is a fare tradeoff. In the future,
we might enable them back on the "extended" completion if there is a demand.
Also, this index binds its keys to the project (`traceKeyHashToVirtualFileMapping`),
further eliminating useless index lookups.

* Thanks to the recent fixes in the platform (IJPL-265), it's now possible to
simultaneously iterate over all keys in an index and request values for a given key
without deadlocks, which is much faster than eagerly fetching all keys first.

* While scanning through all matching entries from indexes, we terminate
the lookup if the number of items exceeds the size of the lookup list.
We can further reduce this number by adjusting the "ide.completion.variant.limit"
registry value.

* Calculating expensive "canonical" import paths (e.g. "pkg.private.Name" is importable as
"pkg.Name") is offloaded to a background thread thanks to the `withExpensiveRenderer` API.
We still calculate these paths synchronously, though, for names whose raw qualified names
contain components starting with an underscore to decide whether these private names are
publicly re-exported and, hence, should be displayed.

The rest of the work has been put into reducing the number of entries on the list, e.g.

* The prefix under caret is now matched from the beginning of a name, e.g. `Bar<caret>`
matches `BarBaz`, but not `FooBar`.
* We don't suggest imported names clashing with those already available in scope.
* Some kinds of definitions are not suggested in specific contexts, e.g.
functions and variables are not suggested inside patterns and type hints.
* Nothing is suggested at the top-level of a class body, where dangling
reference expressions or calls are not normally expected.

Additionally, we don't suggest names from .pyi stubs at the moment, because
it pollutes the suggestion list with entries coming from the stubs for
third-party packages in Typeshed. We should probably enable them back once
we are able to properly disable Typeshed entries for not installed packages.

Some legacy forms of completion are left in the extended mode. In particular,
qualified names of classes are offered inside string literals only in this mode.
Also, module and package names are suggested only in the extended mode, because
top-level packages and modules are already suggested for the basic completion
by PyModuleNameCompletionContributor.

A few tests in PyClassNameCompletionTest were updated or removed entirely because
* we no longer suggest private names
* we no longer suggest names from private modules not re-exported in a public module
* we no longer suggest names clashing with those already available in scope
* prefix matching policy was changed to start at the beginning of an identifier

The whole feature can be disabled with the option "Suggest importable classes,
functions and variables in basic completion" in settings.

GitOrigin-RevId: 0787d42ce337b73b01a60f0bb7aa434fee43e659
This commit is contained in:
Mikhail Golubev
2024-07-02 14:08:48 +00:00
committed by intellij-monorepo-bot
parent 7efc1bb1f9
commit 52850e21d8
63 changed files with 490 additions and 159 deletions
@@ -0,0 +1,3 @@
from mod import MyClass
MyClass
@@ -0,0 +1,3 @@
from mod import MyClass
MyCla<caret>
@@ -0,0 +1,2 @@
class MyClass:
pass
@@ -0,0 +1,5 @@
from mod import MyClass
def f(p):
match p:
case MyClass
@@ -0,0 +1,5 @@
from mod import MyClass
def f(p):
match p:
case MyCla<caret>
@@ -0,0 +1,2 @@
class MyClass:
pass
@@ -1,3 +1,3 @@
path = "something"
path1 = "something"
pat<caret>
@@ -1,2 +1,2 @@
def my_func(*args, **kwargs):
def func(*args, **kwargs):
pass
@@ -2,10 +2,10 @@ from typing import overload
@overload
def my_func(p: int):
def func(p: int):
pass
@overload
def my_func(p1: str, p2: int):
def func(p1: str, p2: int):
pass
@@ -1,2 +0,0 @@
def __foo__():
return "private"
@@ -1,2 +0,0 @@
def _foo():
return "private"
@@ -1,2 +0,0 @@
def foo():
return "public"
@@ -1 +0,0 @@
foo = "private"
@@ -1 +0,0 @@
foo = "non-private"
@@ -2,5 +2,5 @@ class Foo:
pass
class Bar:
class UniqueBar:
pass
@@ -1,6 +1,6 @@
from module import (
Foo,
Bar,
UniqueBar,
)
print(Foo(), Bar)
print(Foo(), UniqueBar)
@@ -1,3 +1,3 @@
from module import Foo
print(Foo(), Ba<caret>)
print(Foo(), UniqueBa<caret>)
@@ -1,2 +1,2 @@
def func():
def test_func():
pass
@@ -1 +1 @@
fun<caret>
test_fun<caret>
@@ -1,2 +1,2 @@
def func():
def test_func():
pass
@@ -0,0 +1,5 @@
from mod import unique_class
match "foo":
case unique_classfd
@@ -0,0 +1,2 @@
match "foo":
case unique_<caret>
@@ -0,0 +1,7 @@
def unique_func():
pass
unique_var = 42
class unique_class:
pass
@@ -0,0 +1,3 @@
from mod import unique_class
x: unique_class
@@ -0,0 +1,7 @@
def unique_func():
pass
unique_var = 42
class unique_class:
pass
@@ -0,0 +1,5 @@
from typing import Final
class C:
attr: Final(<caret>)
@@ -0,0 +1,2 @@
class C:
attr: Fina<caret>
@@ -0,0 +1,22 @@
@_SpecialForm
def Final(self, parameters):
"""Special typing construct to indicate final names to type checkers.
A final name cannot be re-assigned or overridden in a subclass.
For example::
MAX_SIZE: Final = 9000
MAX_SIZE += 1 # Error reported by type checker
class Connection:
TIMEOUT: Final[int] = 10
class FastConnector(Connection):
TIMEOUT = 1 # Error reported by type checker
There is no runtime checking of these properties.
"""
item = _type_check(parameters, f'{self} accepts only single type.')
return _GenericAlias(self, (item,))
@@ -0,0 +1,2 @@
class C:
unique_<caret>
@@ -0,0 +1,2 @@
class C:
unique_<caret>
@@ -0,0 +1,2 @@
match "foo":
cas<caret>
@@ -0,0 +1,2 @@
def case_fold(s):
...
@@ -0,0 +1,5 @@
from mod import unique_var
class C:
attr = unique_var
@@ -0,0 +1,2 @@
class C:
attr = unique_<caret>
@@ -0,0 +1,3 @@
from typing import Tuple
attr: Tuple<caret>
@@ -0,0 +1 @@
attr: Tup<caret>
@@ -0,0 +1,12 @@
Tuple = _TupleType(tuple, -1, inst=False, name='Tuple')
Tuple.__doc__ = \
"""Deprecated alias to builtins.tuple.
Tuple[X, Y] is the cross-product type of X and Y.
Example: Tuple[T1, T2] is a tuple of two elements corresponding
to type variables T1 and T2. Tuple[int, float, str] is a tuple
of an int, a float and a string.
To specify a variable-length tuple of homogeneous type, use Tuple[T, ...].
"""
@@ -0,0 +1 @@
unique_<caret>
@@ -0,0 +1,3 @@
from pkg import unique_var
unique_var
@@ -0,0 +1 @@
unique_<caret>
@@ -0,0 +1 @@
from ._mod import unique_var
@@ -0,0 +1 @@
unique_var = 42
@@ -0,0 +1 @@
mai<caret>
@@ -0,0 +1 @@
mai<caret>
@@ -0,0 +1,2 @@
def main():
pass