diff --git a/platform/lang-api/src/com/intellij/codeInsight/hints/InlayParameterHintsProvider.kt b/platform/lang-api/src/com/intellij/codeInsight/hints/InlayParameterHintsProvider.kt index 82699590d861..9cf1b2dc530d 100644 --- a/platform/lang-api/src/com/intellij/codeInsight/hints/InlayParameterHintsProvider.kt +++ b/platform/lang-api/src/com/intellij/codeInsight/hints/InlayParameterHintsProvider.kt @@ -22,7 +22,29 @@ import com.intellij.lang.LanguageExtension object InlayParameterHintsExtension: LanguageExtension("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 { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/ParameterHintsPresentationManager.java b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/ParameterHintsPresentationManager.java index c1f2cb5d5c05..338638061153 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/ParameterHintsPresentationManager.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/ParameterHintsPresentationManager.java @@ -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() { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/ParameterHintsPassFactory.java b/platform/lang-impl/src/com/intellij/codeInsight/hints/ParameterHintsPassFactory.java index aca1d04682fc..23fdc7f71b8b 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/ParameterHintsPassFactory.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/ParameterHintsPassFactory.java @@ -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 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 myAnnotations = new HashMap<>(); + private final Map myHints = new HashMap<>(); + private final Map 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 removedHints = new HashSet<>(); - TIntObjectHashMap 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 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 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 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)); } } } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/ParameterHintsUpdater.java b/platform/lang-impl/src/com/intellij/codeInsight/hints/ParameterHintsUpdater.java new file mode 100644 index 000000000000..04c19851d306 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/ParameterHintsUpdater.java @@ -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 REPEATED_PASS = Key.create("RepeatedParameterHintsPass"); + + private final ParameterHintsPresentationManager myHintsManager; + private final TIntObjectHashMap myCaretMap; + + private final Map myExistingHints; + private final Map myNewHints; + private final Map myHintsToPreserve; + + private final Editor myEditor; + private final List myUpdateList; + + public ParameterHintsUpdater(@NotNull Editor editor, + @NotNull Map existingHints, + @NotNull Map newHints, + @NotNull Map hintsToPreserve) { + myEditor = editor; + myExistingHints = existingHints; + myNewHints = newHints; + myHintsToPreserve = hintsToPreserve; + myHintsManager = ParameterHintsPresentationManager.getInstance(); + + myCaretMap = new TIntObjectHashMap<>(); + List allCarets = myEditor.getCaretModel().getAllCarets(); + allCarets.forEach((caret) -> myCaretMap.put(caret.getOffset(), caret)); + + Set toUpdate = getOffsetsToUpdate(); + myUpdateList = getSortedByOffsetUpdateList(toUpdate); + } + + + private Set getOffsetsToUpdate() { + Set 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 getSortedByOffsetUpdateList(Set 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 getInfosNear(int index) { + List 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; + } + } +} \ No newline at end of file