mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-44026 Always match the first component of a qualified name in suggestions
For instance, "numpy.foo" should not match "pkg.test_numpy.foo". GitOrigin-RevId: b3f3dfb17b0705eb3f93368eb281ba662ea92e3c
This commit is contained in:
committed by
intellij-monorepo-bot
parent
e40791ec88
commit
9ad52f13a4
+2
-2
@@ -104,7 +104,7 @@ class PyUnresolvedModuleAttributeCompletionContributor : CompletionContributor()
|
||||
val suggestedQualifiedNames = HashSet<String>()
|
||||
|
||||
ProgressManager.checkCanceled()
|
||||
val resultMatchingCompleteReference = result.withPrefixMatcher(QualifiedNameMatcher(qualifierString, attribute))
|
||||
val resultMatchingCompleteReference = result.withPrefixMatcher(QualifiedNameMatcher(qualifier.append(attribute)))
|
||||
PyModuleNameIndex.find(qualifier.lastComponent!!, project, true).asSequence()
|
||||
.filter { QualifiedNameFinder.findShortestImportableQName(it) == qualifier }
|
||||
.flatMap { it.iterateNames().asSequence() }
|
||||
@@ -128,7 +128,7 @@ class PyUnresolvedModuleAttributeCompletionContributor : CompletionContributor()
|
||||
}
|
||||
val scope = PySearchUtilBase.excludeSdkTestsScope(project)
|
||||
PyQualifiedNameCompletionMatcher.processMatchingExportedNames(
|
||||
qualifierString, attribute, parameters.originalFile, scope,
|
||||
qualifier, attribute, parameters.originalFile, scope,
|
||||
Processor {
|
||||
ProgressManager.checkCanceled()
|
||||
if (suggestedQualifiedNames.add(it.qualifiedName.toString())) {
|
||||
|
||||
+40
-41
@@ -7,7 +7,6 @@ import com.intellij.openapi.progress.ProcessCanceledException;
|
||||
import com.intellij.openapi.progress.ProgressManager;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.roots.ProjectFileIndex;
|
||||
import com.intellij.openapi.util.Couple;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.openapi.vfs.VfsUtilCore;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
@@ -37,18 +36,17 @@ public class PyQualifiedNameCompletionMatcher {
|
||||
private PyQualifiedNameCompletionMatcher() {
|
||||
}
|
||||
|
||||
public static void processMatchingExportedNames(@NotNull String qualifierPattern,
|
||||
public static void processMatchingExportedNames(@NotNull QualifiedName qualifierPattern,
|
||||
@NotNull String attributePattern,
|
||||
@NotNull PsiFile currentFile,
|
||||
@NotNull GlobalSearchScope scope,
|
||||
@NotNull Processor<? super ExportedName> processor) {
|
||||
if (attributePattern.isEmpty() || qualifierPattern.isEmpty()) return;
|
||||
PrefixMatcher attributeMatcher = createNameMatcher(attributePattern);
|
||||
PrefixMatcher qualifierMatcher = createQualifierMatcher(qualifierPattern);
|
||||
if (attributePattern.isEmpty() || qualifierPattern.getComponentCount() == 0) return;
|
||||
QualifiedNameMatcher matcher = new QualifiedNameMatcher(qualifierPattern.append(attributePattern));
|
||||
StubIndex stubIndex = StubIndex.getInstance();
|
||||
Project project = Objects.requireNonNull(scope.getProject());
|
||||
|
||||
GlobalSearchScope moduleMatchingScope = new ModuleQualifiedNameMatchingScope(scope, qualifierMatcher, project);
|
||||
GlobalSearchScope moduleMatchingScope = new ModuleQualifiedNameMatchingScope(scope, matcher, project);
|
||||
Set<QualifiedName> alreadySuggestedAttributes = new HashSet<>();
|
||||
IndexLookupStats stats = new IndexLookupStats();
|
||||
try {
|
||||
@@ -56,7 +54,7 @@ public class PyQualifiedNameCompletionMatcher {
|
||||
stubIndex.processAllKeys(PyExportedModuleAttributeIndex.KEY, attributeName -> {
|
||||
ProgressManager.checkCanceled();
|
||||
stats.scannedKeys++;
|
||||
if (!attributeMatcher.isStartMatch(attributeName)) return true;
|
||||
if (!matcher.attributeMatches(attributeName)) return true;
|
||||
stats.matchingKeys++;
|
||||
return stubIndex.processElements(PyExportedModuleAttributeIndex.KEY,
|
||||
attributeName, project, moduleMatchingScope, idFilter, PyElement.class, element -> {
|
||||
@@ -66,7 +64,7 @@ public class PyQualifiedNameCompletionMatcher {
|
||||
assert moduleQualifiedName != null : vFile;
|
||||
QualifiedName canonicalImportPath = findCanonicalImportPath(element, moduleQualifiedName, currentFile);
|
||||
QualifiedName importPath;
|
||||
if (canonicalImportPath != null && qualifierMatcher.prefixMatches(canonicalImportPath.toString())) {
|
||||
if (canonicalImportPath != null && matcher.qualifierMatches(canonicalImportPath)) {
|
||||
importPath = canonicalImportPath;
|
||||
}
|
||||
else {
|
||||
@@ -108,16 +106,6 @@ public class PyQualifiedNameCompletionMatcher {
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static CamelHumpMatcher createQualifierMatcher(@NotNull String pattern) {
|
||||
return new CamelHumpMatcher(pattern, false);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static PrefixMatcher createNameMatcher(@NotNull String pattern) {
|
||||
return new CamelHumpMatcher(pattern, false);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static QualifiedName findCanonicalImportPath(@NotNull PyElement element,
|
||||
@NotNull QualifiedName moduleQualifiedName,
|
||||
@@ -153,43 +141,54 @@ public class PyQualifiedNameCompletionMatcher {
|
||||
}
|
||||
|
||||
public static final class QualifiedNameMatcher extends PrefixMatcher {
|
||||
final PrefixMatcher myLastNameMatcher;
|
||||
final PrefixMatcher myQualifierMatcher;
|
||||
private final PrefixMatcher myQualifierFirstComponentMatcher;
|
||||
private final PrefixMatcher myQualifierRemainderMatcher;
|
||||
private final PrefixMatcher myLastComponentMatcher;
|
||||
|
||||
public QualifiedNameMatcher(@NotNull String qualifierPattern, @NotNull String namePattern) {
|
||||
super(qualifierPattern + "." + namePattern);
|
||||
myLastNameMatcher = createNameMatcher(namePattern);
|
||||
myQualifierMatcher = createQualifierMatcher(qualifierPattern);
|
||||
public QualifiedNameMatcher(@NotNull QualifiedName qualifiedName) {
|
||||
super(qualifiedName.toString());
|
||||
if (qualifiedName.getComponentCount() < 2) {
|
||||
throw new IllegalArgumentException("Qualified name should have at least two components, but was '" + qualifiedName + "'");
|
||||
}
|
||||
myLastComponentMatcher = new CamelHumpMatcher(qualifiedName.getLastComponent(), false);
|
||||
QualifiedName qualifier = qualifiedName.removeLastComponent();
|
||||
myQualifierFirstComponentMatcher = new CamelHumpMatcher(qualifier.getFirstComponent(), false);
|
||||
myQualifierRemainderMatcher = new CamelHumpMatcher(qualifier.removeHead(1).toString(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prefixMatches(@NotNull String qualifiedName) {
|
||||
Couple<String> qualifierAndName = splitByLastDot(qualifiedName);
|
||||
return myQualifierMatcher.prefixMatches(qualifierAndName.getFirst()) && myLastNameMatcher.prefixMatches(qualifierAndName.getSecond());
|
||||
public boolean prefixMatches(@NotNull String name) {
|
||||
QualifiedName qualifiedName = QualifiedName.fromDottedString(name);
|
||||
if (qualifiedName.getComponentCount() == 0) return false;
|
||||
if (!attributeMatches(qualifiedName.getLastComponent())) return false;
|
||||
if (!qualifierMatches(qualifiedName.removeLastComponent())) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean attributeMatches(@Nullable String attribute) {
|
||||
return myLastComponentMatcher.isStartMatch(attribute);
|
||||
}
|
||||
|
||||
private boolean qualifierMatches(@NotNull QualifiedName qualifier) {
|
||||
String firstComponent = Objects.requireNonNullElse(qualifier.getFirstComponent(), "");
|
||||
if (!myQualifierFirstComponentMatcher.prefixMatches(firstComponent)) return false;
|
||||
String remainder = qualifier.getComponentCount() == 0 ? "" : qualifier.removeHead(1).toString();
|
||||
if (!myQualifierRemainderMatcher.prefixMatches(remainder)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull PrefixMatcher cloneWithPrefix(@NotNull String prefix) {
|
||||
Couple<String> qualifierAndName = splitByLastDot(prefix);
|
||||
return new QualifiedNameMatcher(qualifierAndName.getFirst(), qualifierAndName.getSecond());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Couple<String> splitByLastDot(@NotNull String qualifiedName) {
|
||||
int lastDotIndex = qualifiedName.lastIndexOf(".");
|
||||
if (lastDotIndex < 0) {
|
||||
return Couple.of("", qualifiedName);
|
||||
}
|
||||
return Couple.of(qualifiedName.substring(0, lastDotIndex), qualifiedName.substring(lastDotIndex + 1));
|
||||
return new QualifiedNameMatcher(QualifiedName.fromDottedString(prefix));
|
||||
}
|
||||
}
|
||||
|
||||
private static class ModuleQualifiedNameMatchingScope extends DelegatingGlobalSearchScope {
|
||||
private final PrefixMatcher myQualifiedNameMatcher;
|
||||
private final QualifiedNameMatcher myQualifiedNameMatcher;
|
||||
private final Project myProject;
|
||||
|
||||
ModuleQualifiedNameMatchingScope(@NotNull GlobalSearchScope baseScope,
|
||||
@NotNull PrefixMatcher qualifiedNameMatcher,
|
||||
@NotNull QualifiedNameMatcher qualifiedNameMatcher,
|
||||
@NotNull Project project) {
|
||||
super(baseScope);
|
||||
myQualifiedNameMatcher = qualifiedNameMatcher;
|
||||
@@ -201,7 +200,7 @@ public class PyQualifiedNameCompletionMatcher {
|
||||
if (!super.contains(file)) return false;
|
||||
QualifiedName qualifiedName = restoreModuleQualifiedName(file, myProject);
|
||||
if (qualifiedName == null) return false;
|
||||
return myQualifiedNameMatcher.prefixMatches(qualifiedName.toString());
|
||||
return myQualifiedNameMatcher.qualifierMatches(qualifiedName);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def func():
|
||||
pass
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def func():
|
||||
pass
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
def func():
|
||||
pass
|
||||
+1
@@ -0,0 +1 @@
|
||||
ba.fu<caret>
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.jetbrains.python;
|
||||
|
||||
import com.intellij.psi.util.QualifiedName;
|
||||
import com.jetbrains.python.fixtures.PyTestCase;
|
||||
import com.jetbrains.python.psi.stubs.PyQualifiedNameCompletionMatcher.QualifiedNameMatcher;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PyNotImportedQualifiedNameCompletionTest extends PyTestCase {
|
||||
public void testQualifiedNamesAlwaysMatchedByFirstComponentOfQualifier() {
|
||||
myFixture.copyDirectoryToProject(getTestName(false), "");
|
||||
myFixture.configureByFile("main.py");
|
||||
myFixture.completeBasic();
|
||||
List<String> variants = myFixture.getLookupElementStrings();
|
||||
assertNotNull(variants);
|
||||
assertDoesntContain(variants, "foo.bar.func");
|
||||
assertContainsElements(variants, "bar.func", "baz.quux.func");
|
||||
}
|
||||
|
||||
public void testQualifiedNameMatcherTest() {
|
||||
QualifiedNameMatcher matcher = new QualifiedNameMatcher(QualifiedName.fromDottedString("foo.bar.baz"));
|
||||
assertTrue(matcher.prefixMatches("foo.bar.baz"));
|
||||
assertTrue(matcher.prefixMatches("foo.bar.baz"));
|
||||
assertTrue(matcher.prefixMatches("fooExtra.bar.baz"));
|
||||
assertTrue(matcher.prefixMatches("foo.barExtra.baz"));
|
||||
assertTrue(matcher.prefixMatches("foo.bar.bazExtra"));
|
||||
assertFalse(matcher.prefixMatches(""));
|
||||
assertFalse(matcher.prefixMatches("baz"));
|
||||
assertFalse(matcher.prefixMatches("foo.bar"));
|
||||
assertFalse(matcher.prefixMatches("bar.baz"));
|
||||
assertTrue(matcher.prefixMatches("foo.xxx.bar.baz"));
|
||||
assertTrue(matcher.prefixMatches("foo.bar.xxx.baz"));
|
||||
assertFalse(matcher.prefixMatches("foo.bar.baz.xxx"));
|
||||
assertFalse(matcher.prefixMatches("xxx.foo.bar.baz"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTestDataPath() {
|
||||
return super.getTestDataPath() + "/completion/notImportedQualifiedName/";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user