API for handling selected completion item actually works; added test

IDEA-CR-31904
This commit is contained in:
Dmitry Jemerov
2018-04-19 15:17:22 +02:00
parent 9e232912cd
commit e5ddee0216
10 changed files with 146 additions and 89 deletions

View File

@@ -0,0 +1,20 @@
class A extends Base{
int _field;
void _method(){}
void foo(){
int _local1;
int _local2;
_field
int _local3;
}
}
class Base{
void _baseMethod(){}
int _baseField;
private void privateMethod(){}
private int privateField;
}

View File

@@ -0,0 +1,42 @@
// Copyright 2000-2018 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.intellij.java.codeInsight.completion;
import com.intellij.JavaTestUtil;
import com.intellij.codeInsight.completion.CompletionLookupArranger;
import com.intellij.codeInsight.completion.CompletionParameters;
import com.intellij.codeInsight.completion.CompletionService;
import com.intellij.codeInsight.completion.CompletionType;
import com.intellij.codeInsight.completion.impl.CompletionServiceImpl;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.util.Pair;
import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase;
import com.intellij.util.containers.ContainerUtil;
import java.util.List;
/**
* @author yole
*/
public class CompletionServiceTest extends LightCodeInsightFixtureTestCase {
@Override
protected String getBasePath() {
return JavaTestUtil.getRelativeJavaTestDataPath() + "/codeInsight/completion/normal/";
}
public void testCompletionServiceSimple() {
myFixture.configureByFile("Simple.java");
CompletionServiceImpl service = (CompletionServiceImpl)CompletionService.getCompletionService();
CompletionParameters parameters = service.createCompletionParameters(getProject(),
myFixture.getEditor(),
myFixture.getEditor().getCaretModel().getPrimaryCaret(),
1,
CompletionType.BASIC);
CompletionLookupArranger arranger = service.createLookupArranger(parameters);
service.performCompletion(parameters, result -> arranger.addElement(result));
Pair<List<LookupElement>, Integer> items = arranger.arrangeItems();
LookupElement element = ContainerUtil.find(items.first, item -> item.getLookupString().equals("_field"));
WriteCommandAction.runWriteCommandAction(getProject(), () -> service.handleCompletionItemSelected(parameters, element, arranger.itemMatcher(element), '\n'));
myFixture.checkResultByFile("Simple_afterCompletionService.java");
}
}

View File

@@ -20,6 +20,7 @@ class CompletionInitializationContextImpl extends CompletionInitializationContex
myHostOffsets = new OffsetsInFile(file, getOffsetMap()).toTopLevelFile();
}
@NotNull
OffsetsInFile getHostOffsets() {
return myHostOffsets;
}

View File

@@ -4,6 +4,7 @@ package com.intellij.codeInsight.completion;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementPresentation;
import com.intellij.openapi.util.Pair;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@@ -17,10 +18,20 @@ public interface CompletionLookupArranger {
* Adds an element to be arranged.
* @param presentation The presentation of the element (rendered with {@link LookupElement#renderElement(LookupElementPresentation)}
*/
void addElement(LookupElement element,
CompletionSorter sorter,
PrefixMatcher prefixMatcher,
LookupElementPresentation presentation);
void addElement(@NotNull LookupElement element,
@NotNull CompletionSorter sorter,
@NotNull PrefixMatcher prefixMatcher,
@NotNull LookupElementPresentation presentation);
/**
* Adds an element to be arranged, along with its prefix matcher.
*/
void addElement(@NotNull CompletionResult result);
/**
* Returns the prefix matcher registered for the specified element.
*/
PrefixMatcher itemMatcher(@NotNull LookupElement item);
/**
* Returns the items in the appropriate order and the initial selection.

View File

@@ -137,15 +137,22 @@ public class CompletionLookupArrangerImpl extends LookupArranger implements Comp
}
@Override
public void addElement(LookupElement element,
CompletionSorter sorter,
PrefixMatcher prefixMatcher,
LookupElementPresentation presentation) {
public void addElement(@NotNull LookupElement element,
@NotNull CompletionSorter sorter,
@NotNull PrefixMatcher prefixMatcher,
@NotNull LookupElementPresentation presentation) {
registerMatcher(element, prefixMatcher);
associateSorter(element, (CompletionSorterImpl)sorter);
addElement(element, presentation);
}
@Override
public void addElement(@NotNull CompletionResult result) {
LookupElementPresentation presentation = new LookupElementPresentation();
result.getLookupElement().renderElement(presentation);
addElement(result.getLookupElement(), result.getSorter(), result.getPrefixMatcher(), presentation);
}
@Override
public void addElement(LookupElement element, LookupElementPresentation presentation) {
StatisticsWeigher.clearBaseStatisticsInfo(element);

View File

@@ -23,7 +23,9 @@ public class CompletionProcessBase implements CompletionProcessEx, Disposable {
protected final Object myLock = new String("CompletionProgressIndicator");
protected OffsetsInFile myHostOffsets;
private CompletionParameters myParameters;
@NotNull
private final Caret myCaret;
@NotNull
private final OffsetMap myOffsetMap;
public CompletionProcessBase(CompletionInitializationContext context) {
@@ -33,21 +35,25 @@ public class CompletionProcessBase implements CompletionProcessEx, Disposable {
myOffsetMap = context.getOffsetMap();
}
@NotNull
@Override
public Project getProject() {
return myParameters.getOriginalFile().getProject();
}
@NotNull
@Override
public Editor getEditor() {
return myParameters.getEditor();
}
@NotNull
@Override
public Caret getCaret() {
return myCaret;
}
@NotNull
@Override
public OffsetMap getOffsetMap() {
return myOffsetMap;
@@ -64,6 +70,7 @@ public class CompletionProcessBase implements CompletionProcessEx, Disposable {
return myInvocationCount == 0;
}
@NotNull
@Override
public OffsetsInFile getHostOffsets() {
return myHostOffsets;

View File

@@ -18,10 +18,19 @@ import java.util.function.Supplier;
* @author yole
*/
interface CompletionProcessEx extends CompletionProcess {
@NotNull
Project getProject();
@NotNull
Editor getEditor();
@NotNull
Caret getCaret();
@NotNull
OffsetMap getOffsetMap();
@NotNull
OffsetsInFile getHostOffsets();
@Nullable

View File

@@ -98,7 +98,7 @@ public class CompletionProgressIndicator extends ProgressIndicatorBase implement
};
private final Semaphore myFreezeSemaphore = new Semaphore(1);
private final Semaphore myFinishSemaphore = new Semaphore(1);
private final OffsetMap myOffsetMap;
@NotNull private final OffsetMap myOffsetMap;
private final Set<Pair<Integer, ElementPattern<String>>> myRestartingPrefixConditions = ContainerUtil.newConcurrentSet();
private final LookupAdapter myLookupListener = new LookupAdapter() {
@Override
@@ -125,7 +125,7 @@ public class CompletionProgressIndicator extends ProgressIndicatorBase implement
private final Object myLock = new String("CompletionProgressIndicator");
CompletionProgressIndicator(Editor editor, @NotNull Caret caret, int invocationCount,
CodeCompletionHandlerBase handler, OffsetMap offsetMap, OffsetsInFile hostOffsets,
CodeCompletionHandlerBase handler, @NotNull OffsetMap offsetMap, @NotNull OffsetsInFile hostOffsets,
boolean hasModifiers, LookupImpl lookup) {
myEditor = editor;
myCaret = caret;
@@ -174,11 +174,13 @@ public class CompletionProgressIndicator extends ProgressIndicatorBase implement
myHandler.lookupItemSelected(this, lookupItem, completionChar, myLookup.getItems());
}
@NotNull
@SuppressWarnings("WeakerAccess")
public OffsetMap getOffsetMap() {
return myOffsetMap;
}
@NotNull
@Override
public OffsetsInFile getHostOffsets() {
return myHostOffsets;
@@ -621,10 +623,12 @@ public class CompletionProgressIndicator extends ProgressIndicatorBase implement
return reused ? Math.max(myInvocationCount + 1, 2) : invocation;
}
@NotNull
public Editor getEditor() {
return myEditor;
}
@Override
@NotNull
public Caret getCaret() {
return myCaret;

View File

@@ -3,6 +3,7 @@ package com.intellij.codeInsight.completion.impl;
import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.lookup.*;
import com.intellij.codeInsight.lookup.impl.LookupImpl;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Caret;
@@ -288,7 +289,15 @@ public final class CompletionServiceImpl extends CompletionService {
}
@SuppressWarnings("unused")
public void handleCompletionItemSelected(CompletionParameters parameters, LookupElement lookupElement, char completionChar) {
public void handleCompletionItemSelected(CompletionParameters parameters,
LookupElement lookupElement,
PrefixMatcher prefixMatcher,
char completionChar) {
LookupImpl.insertLookupString(parameters.getPosition().getProject(),
parameters.getEditor(),
lookupElement,
prefixMatcher, prefixMatcher.getPrefix(), prefixMatcher.getPrefix().length());
CodeCompletionHandlerBase handler =
CodeCompletionHandlerBase.createHandler(parameters.getCompletionType(), true, parameters.isAutoPopup(), true);
handler.handleCompletionElementSelected(parameters, lookupElement, completionChar);

View File

@@ -527,40 +527,49 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable,
}
protected void insertLookupString(LookupElement item, final int prefix) {
final String lookupString = getCaseCorrectedLookupString(item);
insertLookupString(myProject, getTopLevelEditor(), item, itemMatcher(item), itemPattern(item), prefix);
}
final Editor hostEditor = getTopLevelEditor();
public static void insertLookupString(final Project project,
Editor editor, LookupElement item,
PrefixMatcher matcher, String itemPattern, final int prefixLength) {
final String lookupString = getCaseCorrectedLookupString(item, matcher, itemPattern);
final Editor hostEditor = editor;
hostEditor.getCaretModel().runForEachCaret(new CaretAction() {
@Override
public void perform(Caret caret) {
EditorModificationUtil.deleteSelectedText(hostEditor);
final int caretOffset = hostEditor.getCaretModel().getOffset();
int offset = insertLookupInDocumentWindowIfNeeded(caretOffset, prefix, lookupString);
int offset = insertLookupInDocumentWindowIfNeeded(project, editor, caretOffset, prefixLength, lookupString);
hostEditor.getCaretModel().moveToOffset(offset);
hostEditor.getSelectionModel().removeSelection();
}
});
myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
}
private int insertLookupInDocumentWindowIfNeeded(int caretOffset, int prefix, String lookupString) {
DocumentWindow document = getInjectedDocument(caretOffset);
if (document == null) return insertLookupInDocument(caretOffset, myEditor.getDocument(), prefix, lookupString);
PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
private static int insertLookupInDocumentWindowIfNeeded(Project project,
Editor editor, int caretOffset,
int prefix,
String lookupString) {
DocumentWindow document = getInjectedDocument(project, editor, caretOffset);
if (document == null) return insertLookupInDocument(caretOffset, editor.getDocument(), prefix, lookupString);
PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(document);
int offset = document.hostToInjected(caretOffset);
int lookupStart = Math.min(offset, Math.max(offset - prefix, 0));
int diff = -1;
if (file != null) {
List<TextRange> ranges = InjectedLanguageManager.getInstance(myProject)
List<TextRange> ranges = InjectedLanguageManager.getInstance(project)
.intersectWithAllEditableFragments(file, TextRange.create(lookupStart, offset));
if (!ranges.isEmpty()) {
diff = ranges.get(0).getStartOffset() - lookupStart;
if (ranges.size() == 1 && diff == 0) diff = -1;
}
}
if (diff == -1) return insertLookupInDocument(caretOffset, myEditor.getDocument(), prefix, lookupString);
if (diff == -1) return insertLookupInDocument(caretOffset, editor.getDocument(), prefix, lookupString);
return document.injectedToHost(
insertLookupInDocument(offset, document, prefix - diff, diff == 0 ? lookupString : lookupString.substring(diff))
);
@@ -576,15 +585,14 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable,
return lookupStart + lookupString.length();
}
private String getCaseCorrectedLookupString(LookupElement item) {
private static String getCaseCorrectedLookupString(LookupElement item, PrefixMatcher prefixMatcher, String prefix) {
String lookupString = item.getLookupString();
if (item.isCaseSensitive()) {
return lookupString;
}
final String prefix = itemPattern(item);
final int length = prefix.length();
if (length == 0 || !itemMatcher(item).prefixMatches(prefix)) return lookupString;
if (length == 0 || !prefixMatcher.prefixMatches(prefix)) return lookupString;
boolean isAllLower = true;
boolean isAllUpper = true;
boolean sameCase = true;
@@ -872,67 +880,6 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable,
myPreview.updatePreview(currentItem);
}
public boolean fillInCommonPrefix(boolean explicitlyInvoked) {
if (explicitlyInvoked) {
setFocusDegree(FocusDegree.FOCUSED);
}
if (explicitlyInvoked && myCalculating) return false;
if (!explicitlyInvoked && mySelectionTouched) return false;
ListModel listModel = getListModel();
if (listModel.getSize() <= 1) return false;
if (listModel.getSize() == 0) return false;
final LookupElement firstItem = (LookupElement)listModel.getElementAt(0);
if (listModel.getSize() == 1 && firstItem instanceof EmptyLookupItem) return false;
final PrefixMatcher firstItemMatcher = itemMatcher(firstItem);
final String oldPrefix = firstItemMatcher.getPrefix();
final String presentPrefix = oldPrefix + getAdditionalPrefix();
String commonPrefix = getCaseCorrectedLookupString(firstItem);
for (int i = 1; i < listModel.getSize(); i++) {
LookupElement item = (LookupElement)listModel.getElementAt(i);
if (item instanceof EmptyLookupItem) return false;
if (!oldPrefix.equals(itemMatcher(item).getPrefix())) return false;
final String lookupString = getCaseCorrectedLookupString(item);
final int length = Math.min(commonPrefix.length(), lookupString.length());
if (length < commonPrefix.length()) {
commonPrefix = commonPrefix.substring(0, length);
}
for (int j = 0; j < length; j++) {
if (commonPrefix.charAt(j) != lookupString.charAt(j)) {
commonPrefix = lookupString.substring(0, j);
break;
}
}
if (commonPrefix.length() == 0 || commonPrefix.length() < presentPrefix.length()) {
return false;
}
}
if (commonPrefix.equals(presentPrefix)) {
return false;
}
for (int i = 0; i < listModel.getSize(); i++) {
LookupElement item = (LookupElement)listModel.getElementAt(i);
if (!itemMatcher(item).cloneWithPrefix(commonPrefix).prefixMatches(item)) {
return false;
}
}
myOffsets.setInitialPrefix(presentPrefix, explicitlyInvoked);
replacePrefix(presentPrefix, commonPrefix);
return true;
}
public void replacePrefix(final String presentPrefix, final String newPrefix) {
if (!performGuardedChange(() -> {
EditorModificationUtil.deleteSelectedText(myEditor);
@@ -978,11 +925,11 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable,
}
@Nullable
private DocumentWindow getInjectedDocument(int offset) {
PsiFile hostFile = PsiDocumentManager.getInstance(myProject).getPsiFile(myEditor.getDocument());
private static DocumentWindow getInjectedDocument(Project project, Editor editor, int offset) {
PsiFile hostFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
if (hostFile != null) {
// inspired by com.intellij.codeInsight.editorActions.TypedHandler.injectedEditorIfCharTypedIsSignificant()
List<DocumentWindow> injected = InjectedLanguageManager.getInstance(myProject).getCachedInjectedDocumentsInRange(hostFile, TextRange.create(offset, offset));
List<DocumentWindow> injected = InjectedLanguageManager.getInstance(project).getCachedInjectedDocumentsInRange(hostFile, TextRange.create(offset, offset));
for (DocumentWindow documentWindow : injected ) {
if (documentWindow.isValid() && documentWindow.containsRange(offset, offset)) {
return documentWindow;
@@ -995,7 +942,7 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable,
@Override
@NotNull
public Editor getEditor() {
DocumentWindow documentWindow = getInjectedDocument(myEditor.getCaretModel().getOffset());
DocumentWindow documentWindow = getInjectedDocument(myProject, myEditor, myEditor.getCaretModel().getOffset());
if (documentWindow != null) {
PsiFile injectedFile = PsiDocumentManager.getInstance(myProject).getPsiFile(documentWindow);
return InjectedLanguageUtil.getInjectedEditorForInjectedFile(myEditor, injectedFile);