[param hints] inlays update refactoring, added ability for clients to preserve hints if they existed before update

This commit is contained in:
Yaroslav Lepenkin
2017-03-28 10:57:00 +03:00
parent 95ccefcaff
commit 1c8ee7eb03
4 changed files with 241 additions and 68 deletions
@@ -22,7 +22,29 @@ import com.intellij.lang.LanguageExtension
object InlayParameterHintsExtension: LanguageExtension<InlayParameterHintsProvider>("com.intellij.codeInsight.parameterNameHints")
data class InlayInfo(val text: String, val offset: Int)
class InlayInfo(val text: String, val offset: Int, val isShowOnlyIfExistedBefore: Boolean) {
constructor(text: String, offset: Int): this(text, offset, false)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
other as InlayInfo
if (text != other.text) return false
if (offset != other.offset) return false
return true
}
override fun hashCode(): Int {
var result = text.hashCode()
result = 31 * result + offset
return result
}
}
sealed class HintInfo {
@@ -87,19 +87,21 @@ public class ParameterHintsPresentationManager implements Disposable {
return inlay;
}
public void deleteHint(@NotNull Editor editor, @NotNull Inlay hint) {
updateRenderer(editor, hint, null);
public void deleteHint(@NotNull Editor editor, @NotNull Inlay hint, boolean useAnimation) {
updateRenderer(editor, hint, null, useAnimation);
}
public void replaceHint(@NotNull Editor editor, @NotNull Inlay hint, @NotNull String newText) {
updateRenderer(editor, hint, newText);
updateRenderer(editor, hint, newText, true);
}
private void updateRenderer(@NotNull Editor editor, @NotNull Inlay hint, @Nullable String newText) {
private void updateRenderer(@NotNull Editor editor, @NotNull Inlay hint, @Nullable String newText, boolean useAnimation) {
MyRenderer renderer = (MyRenderer)hint.getRenderer();
renderer.update(editor, newText);
renderer.update(editor, newText, useAnimation);
hint.updateSize();
scheduleRendererUpdate(editor, hint);
if (useAnimation) {
scheduleRendererUpdate(editor, hint);
}
}
@Override
@@ -173,16 +175,15 @@ public class ParameterHintsPresentationManager implements Disposable {
private int step;
private MyRenderer(Editor editor, String text, boolean animated) {
updateState(editor, text);
if (!animated) step = steps + 1;
updateState(editor, text, animated);
}
private String getText() {
return myText;
}
public void update(Editor editor, String newText) {
updateState(editor, newText);
public void update(Editor editor, String newText, boolean animated) {
updateState(editor, newText, animated);
}
@Nullable
@@ -191,13 +192,13 @@ public class ParameterHintsPresentationManager implements Disposable {
return "ParameterNameHints";
}
private void updateState(Editor editor, String text) {
private void updateState(Editor editor, String text, boolean animated) {
FontMetrics metrics = getFontMetrics(editor).metrics;
startWidth = doCalcWidth(myText, metrics);
myText = text;
int endWidth = doCalcWidth(myText, metrics);
step = 1;
steps = Math.max(1, Math.abs(endWidth - startWidth) / metrics.charWidth('a') / ANIMATION_CHARS_PER_STEP);
step = animated ? 1 : steps + 1;
}
public boolean nextStep() {
@@ -26,29 +26,26 @@ import com.intellij.codeInsight.hints.settings.Diff;
import com.intellij.codeInsight.hints.settings.ParameterNameHintsSettings;
import com.intellij.lang.Language;
import com.intellij.openapi.components.AbstractProjectComponent;
import com.intellij.openapi.editor.Caret;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.Inlay;
import com.intellij.openapi.editor.VisualPosition;
import com.intellij.openapi.editor.ex.EditorSettingsExternalizable;
import com.intellij.openapi.editor.ex.util.CaretVisualPositionKeeper;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.SyntaxTraverser;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashSet;
import gnu.trove.TIntObjectHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class ParameterHintsPassFactory extends AbstractProjectComponent implements TextEditorHighlightingPassFactory {
private static final Key<Boolean> REPEATED_PASS = Key.create("RepeatedParameterHintsPass");
public ParameterHintsPassFactory(Project project, TextEditorHighlightingPassRegistrar registrar) {
super(project);
@@ -63,7 +60,8 @@ public class ParameterHintsPassFactory extends AbstractProjectComponent implemen
}
private static class ParameterHintsPass extends EditorBoundHighlightingPass {
private final Map<Integer, String> myAnnotations = new HashMap<>();
private final Map<Integer, String> myHints = new HashMap<>();
private final Map<Integer, String> myShowOnlyIfExistedBeforeHints = new HashMap<>();
private ParameterHintsPass(@NotNull PsiFile file, @NotNull Editor editor) {
super(editor, file, true);
@@ -72,7 +70,7 @@ public class ParameterHintsPassFactory extends AbstractProjectComponent implemen
@Override
public void doCollectInformation(@NotNull ProgressIndicator progress) {
assert myDocument != null;
myAnnotations.clear();
myHints.clear();
if (!isEnabled()) return;
Language language = myFile.getLanguage();
@@ -90,7 +88,7 @@ public class ParameterHintsPassFactory extends AbstractProjectComponent implemen
.map((item) -> MatcherConstructor.INSTANCE.createMatcher(item))
.filter((e) -> e != null)
.collect(Collectors.toList());
SyntaxTraverser.psiTraverser(myFile).forEach(element -> process(element, provider, matchers));
}
@@ -123,61 +121,33 @@ public class ParameterHintsPassFactory extends AbstractProjectComponent implemen
if (info == null || !isMatchedByAny(info, blackListMatchers)) {
hints.forEach((hint) -> {
String presentation = provider.getInlayPresentation(hint.getText());
myAnnotations.put(hint.getOffset(), presentation);
int offset = hint.getOffset();
if (hint.isShowOnlyIfExistedBefore()) {
myShowOnlyIfExistedBeforeHints.put(offset, presentation);
}
else {
myHints.put(offset, presentation);
}
});
}
}
@Override
public void doApplyInformationToEditor() {
assert myDocument != null;
boolean firstTime = myEditor.getUserData(REPEATED_PASS) == null;
ParameterHintsPresentationManager presentationManager = ParameterHintsPresentationManager.getInstance();
Set<String> removedHints = new HashSet<>();
TIntObjectHashMap<Caret> caretMap = new TIntObjectHashMap<>();
CaretVisualPositionKeeper keeper = new CaretVisualPositionKeeper(myEditor);
for (Caret caret : myEditor.getCaretModel().getAllCarets()) {
caretMap.put(caret.getOffset(), caret);
}
for (Inlay inlay : myEditor.getInlayModel().getInlineElementsInRange(0, myDocument.getTextLength())) {
if (!presentationManager.isParameterHint(inlay)) continue;
int offset = inlay.getOffset();
String newText = myAnnotations.remove(offset);
String oldText = presentationManager.getHintText(inlay);
if (delayRemoval(inlay, caretMap) || presentationManager.isPinned(inlay)) continue;
if (!Objects.equals(newText, oldText)) {
if (newText == null) {
removedHints.add(oldText);
presentationManager.deleteHint(myEditor, inlay);
}
else {
presentationManager.replaceHint(myEditor, inlay, newText);
}
}
}
for (Map.Entry<Integer, String> e : myAnnotations.entrySet()) {
int offset = e.getKey();
String text = e.getValue();
presentationManager.addHint(myEditor, offset, text, !firstTime && !removedHints.contains(text), false);
}
ParameterHintsUpdater updater = new ParameterHintsUpdater(myEditor, getAllHints(), myHints, myShowOnlyIfExistedBeforeHints);
updater.update();
keeper.restoreOriginalLocation();
myEditor.putUserData(REPEATED_PASS, Boolean.TRUE);
}
private boolean delayRemoval(Inlay inlay, TIntObjectHashMap<Caret> caretMap) {
int offset = inlay.getOffset();
Caret caret = caretMap.get(offset);
if (caret == null) return false;
CharSequence text = myEditor.getDocument().getImmutableCharSequence();
if (offset >= text.length()) return false;
char afterCaret = text.charAt(offset);
if (afterCaret != ',' && afterCaret != ')') return false;
VisualPosition afterInlayPosition = myEditor.offsetToVisualPosition(offset, true, false);
// check whether caret is to the right of inlay
if (!caret.getVisualPosition().equals(afterInlayPosition)) return false;
return true;
private Map<Integer, Inlay> getAllHints() {
assert myDocument != null;
ParameterHintsPresentationManager manager = ParameterHintsPresentationManager.getInstance();
return myEditor.getInlayModel()
.getInlineElementsInRange(0, myDocument.getTextLength())
.stream()
.filter((inlay) -> manager.isParameterHint(inlay))
.collect(Collectors.toMap((inlay) -> inlay.getOffset(), (inlay) -> inlay));
}
}
}
@@ -0,0 +1,180 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInsight.hints;
import com.intellij.codeInsight.daemon.impl.ParameterHintsPresentationManager;
import com.intellij.openapi.editor.Caret;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.Inlay;
import com.intellij.openapi.editor.VisualPosition;
import com.intellij.openapi.util.Key;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.TIntObjectHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ParameterHintsUpdater {
private static final Key<Boolean> REPEATED_PASS = Key.create("RepeatedParameterHintsPass");
private final ParameterHintsPresentationManager myHintsManager;
private final TIntObjectHashMap<Caret> myCaretMap;
private final Map<Integer, Inlay> myExistingHints;
private final Map<Integer, String> myNewHints;
private final Map<Integer, String> myHintsToPreserve;
private final Editor myEditor;
private final List<InlayUpdateInfo> myUpdateList;
public ParameterHintsUpdater(@NotNull Editor editor,
@NotNull Map<Integer, Inlay> existingHints,
@NotNull Map<Integer, String> newHints,
@NotNull Map<Integer, String> hintsToPreserve) {
myEditor = editor;
myExistingHints = existingHints;
myNewHints = newHints;
myHintsToPreserve = hintsToPreserve;
myHintsManager = ParameterHintsPresentationManager.getInstance();
myCaretMap = new TIntObjectHashMap<>();
List<Caret> allCarets = myEditor.getCaretModel().getAllCarets();
allCarets.forEach((caret) -> myCaretMap.put(caret.getOffset(), caret));
Set<Integer> toUpdate = getOffsetsToUpdate();
myUpdateList = getSortedByOffsetUpdateList(toUpdate);
}
private Set<Integer> getOffsetsToUpdate() {
Set<Integer> offsets = ContainerUtil.newHashSet(myExistingHints.keySet());
offsets.addAll(myNewHints.keySet());
for (Inlay hint : myExistingHints.values()) {
if (delayRemoval(hint) || myHintsManager.isPinned(hint) || isPreserveHint(hint)) {
offsets.remove(hint.getOffset());
}
}
return offsets;
}
private boolean isPreserveHint(Inlay inlay) {
int offset = inlay.getOffset();
String newText = myNewHints.get(offset);
if (newText == null) {
newText = myHintsToPreserve.get(offset);
}
String oldText = myHintsManager.getHintText(inlay);
return Objects.equals(newText, oldText);
}
private List<InlayUpdateInfo> getSortedByOffsetUpdateList(Set<Integer> offsetsToUpdate) {
return offsetsToUpdate.stream()
.sorted()
.map((offset) -> new InlayUpdateInfo(offset, myExistingHints.get(offset), myNewHints.get(offset)))
.collect(Collectors.toList());
}
public void update() {
boolean firstTime = myEditor.getUserData(REPEATED_PASS) == null;
for (int infoIndex = 0; infoIndex < myUpdateList.size(); infoIndex++) {
InlayUpdateInfo info = myUpdateList.get(infoIndex);
String oldText = info.oldText;
String newText = info.newText;
if (oldText == null) {
boolean useAnimation = !firstTime && !isSameHintRemovedNear(newText, infoIndex);
myHintsManager.addHint(myEditor, info.offset, newText, useAnimation, false);
}
else if (newText == null) {
boolean useAnimation = !isSameHintAddedNear(oldText, infoIndex);
myHintsManager.deleteHint(myEditor, info.inlay, useAnimation);
}
else {
myHintsManager.replaceHint(myEditor, info.inlay, newText);
}
}
myEditor.putUserData(REPEATED_PASS, Boolean.TRUE);
}
private boolean isSameHintRemovedNear(@NotNull String text, int index) {
return getInfosNear(index).anyMatch((info) -> text.equals(info.oldText));
}
private boolean isSameHintAddedNear(@NotNull String text, int index) {
return getInfosNear(index).anyMatch((info) -> text.equals(info.newText));
}
private Stream<InlayUpdateInfo> getInfosNear(int index) {
List<InlayUpdateInfo> result = ContainerUtil.newArrayList();
if (index > 0) {
result.add(myUpdateList.get(index - 1));
}
if (index + 1 < myUpdateList.size()) {
result.add(myUpdateList.get(index + 1));
}
return result.stream();
}
private boolean delayRemoval(Inlay inlay) {
int offset = inlay.getOffset();
Caret caret = myCaretMap.get(offset);
if (caret == null) return false;
CharSequence text = myEditor.getDocument().getImmutableCharSequence();
if (offset >= text.length()) return false;
char afterCaret = text.charAt(offset);
if (afterCaret != ',' && afterCaret != ')') return false;
VisualPosition afterInlayPosition = myEditor.offsetToVisualPosition(offset, true, false);
// check whether caret is to the right of inlay
if (!caret.getVisualPosition().equals(afterInlayPosition)) return false;
return true;
}
private static class InlayUpdateInfo {
public final int offset;
public final Inlay inlay;
public final String newText;
public final String oldText;
public InlayUpdateInfo(int offset, @Nullable Inlay current, @Nullable String newText) {
this.offset = offset;
this.inlay = current;
this.newText = newText;
this.oldText = getHintText();
}
@Nullable
private String getHintText() {
return inlay != null ? ParameterHintsPresentationManager.getInstance().getHintText(inlay) : null;
}
}
}