mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-449 Sticky lines
GitOrigin-RevId: ffe8c135084017a1a31142ba2bdbce9ece9ba94e
This commit is contained in:
committed by
intellij-monorepo-bot
parent
60972ac2df
commit
3ee9a9f75c
@@ -210,6 +210,9 @@ public interface EditorSettings {
|
||||
|
||||
boolean isInsertParenthesesAutomatically();
|
||||
|
||||
boolean isStickyLinesShown();
|
||||
int getStickyLinesLimit();
|
||||
|
||||
enum LineNumerationType {
|
||||
ABSOLUTE,
|
||||
RELATIVE,
|
||||
|
||||
+61
@@ -82,6 +82,8 @@ public class EditorSettingsExternalizable implements PersistentStateComponent<Ed
|
||||
public boolean IS_FOLDING_ENDINGS_SHOWN = false; //is not used in old UI
|
||||
public boolean SHOW_BREADCRUMBS_ABOVE = false;
|
||||
public boolean SHOW_BREADCRUMBS = true;
|
||||
public boolean SHOW_STICKY_LINES = false;
|
||||
public int STICKY_LINES_LIMIT = 5;
|
||||
public boolean ENABLE_RENDERED_DOC = false;
|
||||
public boolean SHOW_INTENTION_PREVIEW = true;
|
||||
public boolean USE_EDITOR_FONT_IN_INLAYS = false;
|
||||
@@ -128,10 +130,17 @@ public class EditorSettingsExternalizable implements PersistentStateComponent<Ed
|
||||
|
||||
private final Map<String, Boolean> mapLanguageBreadcrumbs = new HashMap<>();
|
||||
|
||||
private final Map<String, Boolean> mapLanguageStickyLines = new HashMap<>();
|
||||
|
||||
public Map<String, Boolean> getLanguageBreadcrumbsMap() {
|
||||
return mapLanguageBreadcrumbs;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Map<String, Boolean> getLanguageStickyLines() {
|
||||
return mapLanguageStickyLines;
|
||||
}
|
||||
|
||||
public OptionSet() {
|
||||
Application application = ApplicationManager.getApplication();
|
||||
if (application != null) {
|
||||
@@ -149,6 +158,14 @@ public class EditorSettingsExternalizable implements PersistentStateComponent<Ed
|
||||
this.mapLanguageBreadcrumbs.putAll(map);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void setLanguageStickyLines(Map<String, Boolean> map) {
|
||||
if (this.mapLanguageStickyLines != map) {
|
||||
this.mapLanguageStickyLines.clear();
|
||||
this.mapLanguageStickyLines.putAll(map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@State(
|
||||
@@ -422,6 +439,47 @@ public class EditorSettingsExternalizable implements PersistentStateComponent<Ed
|
||||
return newValue;
|
||||
}
|
||||
|
||||
public boolean isStickyLinesShown() {
|
||||
return myOptions.SHOW_STICKY_LINES;
|
||||
}
|
||||
|
||||
public boolean setStickyLinesShown(boolean value) {
|
||||
boolean old = myOptions.SHOW_STICKY_LINES;
|
||||
if (old == value) return false;
|
||||
myOptions.SHOW_STICKY_LINES = value;
|
||||
myPropertyChangeSupport.firePropertyChange(PropNames.PROP_SHOW_STICKY_LINES, old, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isStickyLinesShownFor(String languageID) {
|
||||
Boolean visible = myOptions.mapLanguageStickyLines.get(languageID);
|
||||
if (visible == null) {
|
||||
// enabled for all languages by default
|
||||
return true;
|
||||
}
|
||||
return visible;
|
||||
}
|
||||
|
||||
public boolean setStickyLinesShownFor(String languageID, boolean value) {
|
||||
Boolean visible = myOptions.mapLanguageStickyLines.put(languageID, value);
|
||||
boolean newValue = (visible == null || visible) != value;
|
||||
if (newValue) {
|
||||
myPropertyChangeSupport.firePropertyChange(PropNames.PROP_SHOW_STICKY_LINES_PER_LANGUAGE, visible, (Boolean)value);
|
||||
}
|
||||
return newValue;
|
||||
}
|
||||
|
||||
public int getStickyLineLimit() {
|
||||
return myOptions.STICKY_LINES_LIMIT;
|
||||
}
|
||||
|
||||
public void setStickyLineLimit(int limit) {
|
||||
int old = myOptions.STICKY_LINES_LIMIT;
|
||||
if (old == limit) return;
|
||||
myOptions.STICKY_LINES_LIMIT = limit;
|
||||
myPropertyChangeSupport.firePropertyChange(PropNames.PROP_STICKY_LINES_LIMIT, old, limit);
|
||||
}
|
||||
|
||||
public boolean isDocCommentRenderingEnabled() {
|
||||
return myOptions.ENABLE_RENDERED_DOC;
|
||||
}
|
||||
@@ -1046,6 +1104,9 @@ public class EditorSettingsExternalizable implements PersistentStateComponent<Ed
|
||||
public static final @NonNls String PROP_IS_FOLDING_ENDINGS_SHOWN = "isFoldingEndingsShown";
|
||||
public static final @NonNls String PROP_SHOW_BREADCRUMBS_ABOVE = "showBreadcrumbsAbove";
|
||||
public static final @NonNls String PROP_SHOW_BREADCRUMBS = "showBreadcrumbs";
|
||||
public static final @NonNls String PROP_SHOW_STICKY_LINES = "showStickyLines";
|
||||
public static final @NonNls String PROP_SHOW_STICKY_LINES_PER_LANGUAGE = "showStickyLinesPerLanguage";
|
||||
public static final @NonNls String PROP_STICKY_LINES_LIMIT = "stickyLinesLimit";
|
||||
// inconsistent name for backward compatibility because such constants are inlined at compilation stage
|
||||
public static final @NonNls String PROP_ENABLE_RENDERED_DOC = "DocCommentRendering";
|
||||
public static final @NonNls String PROP_SHOW_INTENTION_PREVIEW = "showIntentionPreview";
|
||||
|
||||
@@ -271,6 +271,8 @@ radio.show.breadcrumbs.above=Top
|
||||
radio.show.breadcrumbs.below=Bottom
|
||||
label.breadcrumbs.placement=Placement:
|
||||
label.breadcrumbs.languages=Languages:
|
||||
checkbox.show.sticky.lines=Show sticky lines while scrolling:
|
||||
editbox.sticky.lines.limit=From 1 to 30
|
||||
checkbox.right.margin=Show hard wrap and visual guides (configured in Code Style options)
|
||||
checkbox.use.block.caret=Use block caret
|
||||
checkbox.use.full.line.height.caret=Use full line height caret
|
||||
|
||||
+11
@@ -6,6 +6,7 @@ import com.intellij.codeInsight.daemon.DaemonCodeAnalyzerSettings
|
||||
import com.intellij.codeInsight.documentation.render.DocRenderManager
|
||||
import com.intellij.ide.IdeBundle
|
||||
import com.intellij.ide.ui.LafManager
|
||||
import com.intellij.ide.ui.UINumericRange
|
||||
import com.intellij.ide.ui.UISettings
|
||||
import com.intellij.openapi.application.ApplicationBundle
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
@@ -33,6 +34,7 @@ private val myCbBlockCursor get() = CheckboxDescriptor
|
||||
private val myCbFullLineHeightCursor get() = CheckboxDescriptor(ApplicationBundle.message("checkbox.use.full.line.height.caret"), model::isFullLineHeightCursor, model::setFullLineHeightCursor)
|
||||
private val myCbRightMargin get() = CheckboxDescriptor(ApplicationBundle.message("checkbox.right.margin"), model::isRightMarginShown, model::setRightMarginShown)
|
||||
private val myCbShowLineNumbers get() = CheckboxDescriptor(ApplicationBundle.message("checkbox.show.line.numbers"), model::isLineNumbersShown, model::setLineNumbersShown)
|
||||
private val mbCbShowStickyLines get() = CheckboxDescriptor(ApplicationBundle.message("checkbox.show.sticky.lines"), model::isStickyLinesShown, model::setStickyLinesShown)
|
||||
private val myCbShowMethodSeparators get() = CheckboxDescriptor(if (PlatformUtils.isDataGrip()) ApplicationBundle.message("checkbox.show.method.separators.DataGrip") else ApplicationBundle.message("checkbox.show.method.separators"), DaemonCodeAnalyzerSettings.getInstance()::SHOW_METHOD_SEPARATORS)
|
||||
private val myWhitespacesCheckbox get() = CheckboxDescriptor(ApplicationBundle.message("checkbox.show.whitespaces"), model::isWhitespacesShown, model::setWhitespacesShown)
|
||||
private val myLeadingWhitespacesCheckBox get() = CheckboxDescriptor(ApplicationBundle.message("checkbox.show.leading.whitespaces"), model::isLeadingWhitespacesShown, model::setLeadingWhitespacesShown)
|
||||
@@ -87,6 +89,15 @@ internal class EditorAppearanceConfigurable : BoundCompositeSearchableConfigurab
|
||||
}
|
||||
).bindItem(model::getLineNumeration, model::setLineNumeration)
|
||||
}
|
||||
row {
|
||||
val cbShowSticky = checkBox(mbCbShowStickyLines)
|
||||
.gap(RightGap.SMALL)
|
||||
intTextField(UINumericRange(5, 1, 30).asRange())
|
||||
.bindIntText(model::getStickyLineLimit, model::setStickyLineLimit)
|
||||
.columns(4)
|
||||
.enabledIf(cbShowSticky.selected)
|
||||
comment(ApplicationBundle.message("editbox.sticky.lines.limit"))
|
||||
}
|
||||
row {
|
||||
checkBox(myCbShowMethodSeparators)
|
||||
}
|
||||
|
||||
+4
@@ -19,6 +19,7 @@ import com.intellij.openapi.editor.ex.util.HighlighterIteratorWrapper;
|
||||
import com.intellij.openapi.editor.ex.util.LexerEditorHighlighter;
|
||||
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
|
||||
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
|
||||
import com.intellij.openapi.editor.impl.EditorImpl;
|
||||
import com.intellij.openapi.editor.markup.*;
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager;
|
||||
import com.intellij.openapi.fileTypes.FileType;
|
||||
@@ -343,6 +344,9 @@ public final class BraceHighlightingHandler {
|
||||
Document document = editor.getDocument();
|
||||
int line1 = document.getLineNumber(range.getStartOffset());
|
||||
int line2 = document.getLineNumber(range.getEndOffset());
|
||||
if (editor instanceof EditorImpl editorImpl && editorImpl.shouldSuppressEditorFragmentHint(line1)) {
|
||||
return;
|
||||
}
|
||||
line1 = Math.max(line1, line2 - EditorFragmentComponent.getAvailableVisualLinesAboveEditor(editor) + 1);
|
||||
range = new TextRange(document.getLineStartOffset(line1), range.getEndOffset());
|
||||
LightweightHint hint = EditorFragmentComponent.showEditorFragmentHint(editor, range, true, true);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.codeInsight.stickyLines
|
||||
|
||||
import com.intellij.codeHighlighting.TextEditorHighlightingPass
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.editor.impl.stickyLines.StickyLineInfo
|
||||
import com.intellij.openapi.editor.impl.stickyLines.StickyLinesCollector
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.project.DumbAware
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.util.CachedValue
|
||||
import com.intellij.psi.util.CachedValueProvider
|
||||
import com.intellij.psi.util.CachedValuesManager
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
private val CACHED_LINES_KEY: Key<CachedValue<Runnable>> = Key.create("editor.sticky.lines.cached")
|
||||
|
||||
internal class StickyLinesPass(project: Project, document: Document, private val vFile: VirtualFile, private val psiFile: PsiFile)
|
||||
: TextEditorHighlightingPass(project, document), DumbAware {
|
||||
|
||||
private var updater: Runnable? = null
|
||||
|
||||
override fun doCollectInformation(progress: ProgressIndicator) {
|
||||
updater = getUpdater()
|
||||
}
|
||||
|
||||
override fun doApplyInformationToEditor() {
|
||||
updater?.run()
|
||||
updater = null
|
||||
}
|
||||
|
||||
private fun getUpdater(): Runnable {
|
||||
val cachedUpdater = psiFile.getUserData(CACHED_LINES_KEY)?.upToDateOrNull
|
||||
if (cachedUpdater != null) {
|
||||
return cachedUpdater.get()
|
||||
}
|
||||
return CachedValuesManager.getManager(myProject).getCachedValue(
|
||||
psiFile,
|
||||
CACHED_LINES_KEY,
|
||||
getValueProvider(myProject, myDocument, vFile, dependency = psiFile),
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun getValueProvider(
|
||||
project: Project,
|
||||
document: Document,
|
||||
vFile: VirtualFile,
|
||||
dependency: Any
|
||||
): CachedValueProvider<Runnable> {
|
||||
return CachedValueProvider {
|
||||
val collector = StickyLinesCollector(project, document)
|
||||
val infos: MutableSet<StickyLineInfo> = collector.collectLines(vFile)
|
||||
val alreadyExecuted = AtomicBoolean()
|
||||
val runnable = Runnable {
|
||||
if (alreadyExecuted.compareAndSet(false, true)) {
|
||||
collector.applyLines(infos)
|
||||
}
|
||||
}
|
||||
CachedValueProvider.Result.create(runnable, dependency)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.codeInsight.stickyLines
|
||||
|
||||
import com.intellij.codeHighlighting.TextEditorHighlightingPass
|
||||
import com.intellij.codeHighlighting.TextEditorHighlightingPassFactory
|
||||
import com.intellij.codeHighlighting.TextEditorHighlightingPassFactoryRegistrar
|
||||
import com.intellij.codeHighlighting.TextEditorHighlightingPassRegistrar
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.project.DumbAware
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiFile
|
||||
|
||||
internal class StickyLinesPassFactory : TextEditorHighlightingPassFactoryRegistrar, TextEditorHighlightingPassFactory, DumbAware {
|
||||
|
||||
override fun registerHighlightingPassFactory(registrar: TextEditorHighlightingPassRegistrar, project: Project) {
|
||||
registrar.registerTextEditorHighlightingPass(this, null, null, false, -1)
|
||||
}
|
||||
|
||||
override fun createHighlightingPass(psiFile: PsiFile, editor: Editor): TextEditorHighlightingPass? {
|
||||
val project = editor.project ?: return null
|
||||
val virtualFile = editor.virtualFile ?: return null
|
||||
if (editor.settings.isStickyLinesShown) {
|
||||
return StickyLinesPass(project, editor.document, virtualFile, psiFile)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,8 @@ import com.intellij.openapi.editor.ex.util.EmptyEditorHighlighter;
|
||||
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
|
||||
import com.intellij.openapi.editor.highlighter.HighlighterClient;
|
||||
import com.intellij.openapi.editor.impl.event.MarkupModelListener;
|
||||
import com.intellij.openapi.editor.impl.stickyLines.StickyLinesPanel;
|
||||
import com.intellij.openapi.editor.impl.stickyLines.StickyLinesManager;
|
||||
import com.intellij.openapi.editor.impl.view.EditorView;
|
||||
import com.intellij.openapi.editor.markup.GutterDraggableObject;
|
||||
import com.intellij.openapi.editor.markup.GutterIconRenderer;
|
||||
@@ -155,6 +157,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
|
||||
private final @NotNull MyScrollPane myScrollPane;
|
||||
private final @NotNull EditorComponentImpl myEditorComponent;
|
||||
private final @NotNull EditorGutterComponentImpl myGutterComponent;
|
||||
private final @Nullable StickyLinesPanel myStickyLinesPanel;
|
||||
private final TraceableDisposable myTraceableDisposable = new TraceableDisposable(true);
|
||||
private final FocusModeModel myFocusModeModel;
|
||||
private volatile long myLastTypedActionTimestamp = -1;
|
||||
@@ -232,6 +235,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
|
||||
@MouseSelectionState
|
||||
private int myMouseSelectionState;
|
||||
private @Nullable FoldRegion myMouseSelectedRegion;
|
||||
private PanelWithFloatingToolbar myLayeredPane;
|
||||
|
||||
@MagicConstant(intValues = {MOUSE_SELECTION_STATE_NONE, MOUSE_SELECTION_STATE_LINE_SELECTED, MOUSE_SELECTION_STATE_WORD_SELECTED})
|
||||
private @interface MouseSelectionState {
|
||||
@@ -431,6 +435,13 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
|
||||
myGutterComponent.putClientProperty(ColorKey.FUNCTION_KEY, (Function<ColorKey, Color>)key -> getColorsScheme().getColor(key));
|
||||
initComponent();
|
||||
|
||||
myStickyLinesPanel = createStickyLinesPanel();
|
||||
if (myStickyLinesPanel != null) {
|
||||
// remove the status component from the scroll pane and place it on the layered pane where the sticky panel lives,
|
||||
// otherwise the sticky panel would overlap the status
|
||||
myScrollPane.placeStatusOnTopOfStickyPanel();
|
||||
}
|
||||
|
||||
myView = new EditorView(this);
|
||||
myView.reinitSettings();
|
||||
|
||||
@@ -1105,14 +1116,14 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
|
||||
myEditorComponent.setTransferHandler(new MyTransferHandler());
|
||||
myEditorComponent.setAutoscrolls(false); // we have our own auto-scrolling code
|
||||
|
||||
JLayeredPane layeredPane = new PanelWithFloatingToolbar();
|
||||
layeredPane.add(myScrollPane, JLayeredPane.DEFAULT_LAYER);
|
||||
this.myLayeredPane = new PanelWithFloatingToolbar();
|
||||
myLayeredPane.add(myScrollPane, JLayeredPane.DEFAULT_LAYER);
|
||||
UiNotifyConnector.doWhenFirstShown(myPanel, () -> {
|
||||
if (mayShowToolbar()) {
|
||||
layeredPane.add(new EditorFloatingToolbar(this), JLayeredPane.POPUP_LAYER);
|
||||
myLayeredPane.add(new EditorFloatingToolbar(this), JLayeredPane.POPUP_LAYER);
|
||||
}
|
||||
}, getDisposable());
|
||||
myPanel.add(layeredPane, BorderLayout.CENTER);
|
||||
myPanel.add(myLayeredPane, BorderLayout.CENTER);
|
||||
|
||||
myEditorComponent.addKeyListener(new KeyListener() {
|
||||
@Override
|
||||
@@ -1663,6 +1674,9 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
|
||||
int height = visualLineToYRange(endVisualLine)[1] + 2 - yStart;
|
||||
myEditorComponent.repaintEditorComponent(visibleArea.x, yStart, visibleArea.x + visibleArea.width, height);
|
||||
myGutterComponent.repaint(0, yStart, myGutterComponent.getWidth(), height);
|
||||
if (myStickyLinesPanel != null) {
|
||||
myStickyLinesPanel.repaintLinesInRange(startVisualLine, endVisualLine);
|
||||
}
|
||||
}
|
||||
|
||||
private void bulkUpdateStarted() {
|
||||
@@ -3549,6 +3563,14 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
|
||||
return myVerticalScrollBar;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated try to avoid impl api, probably width management can be implemented in a more transparent way
|
||||
*/
|
||||
@Deprecated
|
||||
public int getStickyLinesPanelWidth() {
|
||||
return myPanel.getWidth() - myVerticalScrollBar.getWidth();
|
||||
}
|
||||
|
||||
@MouseSelectionState
|
||||
private int getMouseSelectionState() {
|
||||
return myMouseSelectionState;
|
||||
@@ -5359,6 +5381,8 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
|
||||
|
||||
@DirtyUI
|
||||
private final class MyScrollPane extends JBScrollPane {
|
||||
private JComponent myStatusComponent;
|
||||
|
||||
private MyScrollPane() {
|
||||
super(0);
|
||||
setupCorners();
|
||||
@@ -5431,6 +5455,42 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
|
||||
super.setupCorners();
|
||||
setBorder(new TablessBorder());
|
||||
}
|
||||
|
||||
void placeStatusOnTopOfStickyPanel() {
|
||||
setStatusComponent(super.getStatusComponent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JComponent getStatusComponent() {
|
||||
if (myStickyLinesPanel == null) {
|
||||
return super.getStatusComponent();
|
||||
}
|
||||
else {
|
||||
return myStatusComponent;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStatusComponent(JComponent statusComponent) {
|
||||
if (myStickyLinesPanel == null) {
|
||||
super.setStatusComponent(statusComponent);
|
||||
}
|
||||
else {
|
||||
JComponent oldStatus = super.getStatusComponent();
|
||||
if (oldStatus != null) {
|
||||
super.setStatusComponent(null);
|
||||
}
|
||||
if (statusComponent != null) {
|
||||
myLayeredPane.add(statusComponent, Integer.valueOf(300));
|
||||
} else if (myStatusComponent != null) {
|
||||
myLayeredPane.remove(myStatusComponent);
|
||||
}
|
||||
firePropertyChange("statusComponent", myStatusComponent, statusComponent);
|
||||
myStatusComponent = statusComponent;
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void adjustGlobalFontSize(float size) {
|
||||
@@ -5549,11 +5609,15 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
|
||||
// Main scroll panel: MyScrollPane
|
||||
c.setBounds(0, 0, r.width, r.height);
|
||||
}
|
||||
else {
|
||||
else if (c instanceof EditorFloatingToolbar){
|
||||
// Floating toolbar: EditorFloatingToolbar
|
||||
Dimension d = c.getPreferredSize();
|
||||
int rightInsets = getVerticalScrollBar().getWidth() + (isMirrored() ? myGutterComponent.getWidth() : 0);
|
||||
c.setBounds(r.width - d.width - rightInsets - 20, 20, d.width, d.height);
|
||||
} else if (!(c instanceof StickyLinesPanel)) {
|
||||
// Status component
|
||||
int statusComponentX = myPanel.getWidth() - c.getWidth();
|
||||
c.setLocation(statusComponentX, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5563,4 +5627,28 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
|
||||
return myScrollPane.getPreferredSize();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable StickyLinesPanel getStickyLinesPanel() {
|
||||
return myStickyLinesPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* There is no point to show the editor fragment hint if the sticky panel shows the same line.
|
||||
*/
|
||||
public boolean shouldSuppressEditorFragmentHint(int hintStartLogicalLine) {
|
||||
if (myStickyLinesPanel != null) {
|
||||
return myStickyLinesPanel.suppressHintForLine(hintStartLogicalLine);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private @Nullable StickyLinesPanel createStickyLinesPanel() {
|
||||
if (myProject != null && myKind == EditorKind.MAIN_EDITOR && !isMirrored()) {
|
||||
StickyLinesManager stickyManager = new StickyLinesManager(this, myDocumentMarkupModel, myDisposable);
|
||||
myLayeredPane.add(stickyManager.getStickyPanel(), Integer.valueOf(200));
|
||||
return stickyManager.getStickyPanel();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +172,18 @@ class EditorSettingsState(private val editor: EditorImpl?,
|
||||
var myIsCamelWords: Boolean by property { EditorSettingsExternalizable.getInstance().isCamelWords }
|
||||
var myLineNumeration: EditorSettings.LineNumerationType by property { EditorSettingsExternalizable.getInstance().lineNumeration }
|
||||
|
||||
var myIsStickyLinesShown: Boolean by property { EditorSettingsExternalizable.getInstance().isStickyLinesShown }
|
||||
var myIsStickyLinesShownForLanguage: Boolean by property {
|
||||
this.language?.let {
|
||||
EditorSettingsExternalizable.getInstance().isStickyLinesShownFor(it.id)
|
||||
}
|
||||
// Return true to avoid late appearance of the sticky panel.
|
||||
// Even if the actual value for the language is false,
|
||||
// the panel won't be shown because breadcrumbs' provider respects the settings
|
||||
?: true
|
||||
}
|
||||
var myStickyLinesLimit: Int by property { EditorSettingsExternalizable.getInstance().stickyLineLimit }
|
||||
|
||||
// These come from AdvancedSettings
|
||||
var showingSpecialCharacters: Boolean by property { AdvancedSettings.getBoolean(EDITOR_SHOW_SPECIAL_CHARS) }
|
||||
|
||||
@@ -237,6 +249,9 @@ class EditorSettingsState(private val editor: EditorImpl?,
|
||||
EditorSettingsExternalizable.PropNames.PROP_SHOW_INTENTION_BULB -> refresh(::myShowIntentionBulb)
|
||||
EditorSettingsExternalizable.PropNames.PROP_IS_CAMEL_WORDS -> refresh(::myIsCamelWords)
|
||||
EditorSettingsExternalizable.PropNames.PROP_LINE_NUMERATION -> refresh(::myLineNumeration)
|
||||
EditorSettingsExternalizable.PropNames.PROP_SHOW_STICKY_LINES -> refresh(::myIsStickyLinesShown)
|
||||
EditorSettingsExternalizable.PropNames.PROP_SHOW_STICKY_LINES_PER_LANGUAGE -> refresh(::myIsStickyLinesShownForLanguage)
|
||||
EditorSettingsExternalizable.PropNames.PROP_STICKY_LINES_LIMIT -> refresh(::myStickyLinesLimit)
|
||||
}
|
||||
}, editor.disposable)
|
||||
|
||||
|
||||
+2
-1
@@ -49,7 +49,8 @@ public class IndentsModelCaretListener implements CaretListener {
|
||||
|
||||
private boolean shouldShowHint(@NotNull IndentGuideDescriptor descriptor) {
|
||||
final Rectangle visibleArea = myEditor.getScrollingModel().getVisibleArea();
|
||||
return myEditor.logicalLineToY(descriptor.startLine) < visibleArea.y;
|
||||
boolean isLineBeforeVisibleArea = myEditor.logicalLineToY(descriptor.startLine) < visibleArea.y;
|
||||
return isLineBeforeVisibleArea && !myEditor.shouldSuppressEditorFragmentHint(descriptor.startLine);
|
||||
}
|
||||
|
||||
protected @Nullable IndentGuideDescriptor getCaretIndentGuide(final @NotNull CaretEvent event) {
|
||||
|
||||
@@ -136,10 +136,29 @@ public final class ScrollingModelImpl implements ScrollingModelEx {
|
||||
private void scrollTo(@NotNull Point targetLocation, @NotNull ScrollType scrollType) {
|
||||
AnimatedScrollingRunnable canceledThread = cancelAnimatedScrolling(false);
|
||||
Rectangle viewRect = canceledThread == null ? getVisibleArea() : canceledThread.getTargetVisibleArea();
|
||||
|
||||
// the model knows nothing about the sticky panel rendering on top of the editor
|
||||
// we should adjust target and view rectangle to avoid hidden caret by the sticky panel
|
||||
targetLocation = stickyPanelAdjust(targetLocation, viewRect);
|
||||
|
||||
Point p = calcOffsetsToScroll(targetLocation, scrollType, viewRect);
|
||||
scroll(p.x, p.y);
|
||||
}
|
||||
|
||||
private @NotNull Point stickyPanelAdjust(@NotNull Point targetLocation, @NotNull Rectangle viewRect) {
|
||||
if (mySupplier.getEditor() instanceof EditorImpl editor) {
|
||||
var panel = editor.getStickyLinesPanel();
|
||||
if (panel != null && panel.isStickyEnabled()) {
|
||||
int height = panel.getHeight();
|
||||
if (height > 0) {
|
||||
viewRect.height -= height;
|
||||
return new Point(targetLocation.x, targetLocation.y - height);
|
||||
}
|
||||
}
|
||||
}
|
||||
return targetLocation;
|
||||
}
|
||||
|
||||
@Override
|
||||
@RequiresEdt
|
||||
public void scrollTo(@NotNull LogicalPosition logicalPosition, @NotNull ScrollType scrollType) {
|
||||
|
||||
@@ -621,6 +621,14 @@ class SettingsImpl internal constructor(private val editor: EditorImpl?, kind: E
|
||||
return EditorSettingsExternalizable.getInstance().isInsertParenthesesAutomatically
|
||||
}
|
||||
|
||||
override fun isStickyLinesShown(): Boolean {
|
||||
return state.myIsStickyLinesShown && state.myIsStickyLinesShownForLanguage
|
||||
}
|
||||
|
||||
override fun getStickyLinesLimit(): Int {
|
||||
return state.myStickyLinesLimit
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
@ApiStatus.Experimental
|
||||
fun getState(): EditorSettingsState {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.editor.impl.stickyLines
|
||||
|
||||
import com.intellij.openapi.util.TextRange
|
||||
|
||||
/**
|
||||
* Represents scope of class or function.
|
||||
*
|
||||
* For example,
|
||||
* ```java
|
||||
* @Override
|
||||
* public void run() { // <- primary line
|
||||
* // ^ navigation offset
|
||||
* // ...
|
||||
* } // <- scope line
|
||||
* ```
|
||||
*/
|
||||
internal interface StickyLine : Comparable<StickyLine> {
|
||||
|
||||
/**
|
||||
* The first logical line of scope used to pin corresponding editor's line on sticky panel.
|
||||
*
|
||||
* Usually it is `lineOf(psiElement.textOffset)`
|
||||
*/
|
||||
fun primaryLine(): Int
|
||||
|
||||
/**
|
||||
* The last logical line of scope used to unpin corresponding editor's line from sticky panel.
|
||||
*
|
||||
* Usually it is `lineOf(psiElement.endOffset)`
|
||||
*/
|
||||
fun scopeLine(): Int
|
||||
|
||||
/**
|
||||
* Offset where editor's caret put on mouse click.
|
||||
*
|
||||
* Usually it is `psiElement.textOffset`
|
||||
*/
|
||||
fun navigateOffset(): Int
|
||||
|
||||
/**
|
||||
* Range between primary line and scope line.
|
||||
*
|
||||
* Usually it is `TextRange(psiElement.textOffset, psiElement.endOffset)`
|
||||
*/
|
||||
fun textRange(): TextRange
|
||||
|
||||
/**
|
||||
* Short text of psi element representing the sticky line.
|
||||
*
|
||||
* Usually it is `psiElement.toString`. Maybe null if debug mode is disabled
|
||||
*/
|
||||
fun debugText(): String?
|
||||
|
||||
/**
|
||||
* Compares lines according scope order
|
||||
*/
|
||||
override fun compareTo(other: StickyLine): Int
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.editor.impl.stickyLines
|
||||
|
||||
import com.intellij.codeInsight.hint.EditorFragmentComponent
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.DefaultActionGroup
|
||||
import com.intellij.openapi.editor.ScrollType
|
||||
import com.intellij.openapi.editor.VisualPosition
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.openapi.editor.ex.EditorGutterComponentEx
|
||||
import com.intellij.openapi.editor.ex.util.EditorUIUtil
|
||||
import com.intellij.openapi.editor.impl.EditorImpl
|
||||
import com.intellij.util.ui.MouseEventAdapter
|
||||
import com.intellij.util.ui.MouseEventHandler
|
||||
import com.intellij.util.ui.StartupUiUtil
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.Graphics
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.MouseListener
|
||||
import java.awt.image.BufferedImage
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPopupMenu
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Represents one editor's line (gutter + line text)
|
||||
*
|
||||
* TODO: mouse events handling
|
||||
* - the line should change background color on mouse hover
|
||||
* - sticky gutter should forward clicks/hovers with adjusted coordinates to the actual gutter component
|
||||
*/
|
||||
internal class StickyLineComponent(private val editor: EditorEx) : JComponent() {
|
||||
var primaryVisualLine: Int = -1
|
||||
var scopeVisualLine: Int = -1
|
||||
private var offsetOnClick: Int = -1
|
||||
|
||||
init {
|
||||
border = null
|
||||
addMouseListener(MyMouseEventHandler())
|
||||
addMouseListener(GutterMouseEventHandler())
|
||||
}
|
||||
|
||||
fun setLine(primaryVisualLine: VisualPosition, scopeVisualLine: VisualPosition, offsetOnClick: Int) {
|
||||
this.primaryVisualLine = primaryVisualLine.line
|
||||
this.scopeVisualLine = scopeVisualLine.line
|
||||
this.offsetOnClick = offsetOnClick
|
||||
}
|
||||
|
||||
fun resetLine() {
|
||||
this.primaryVisualLine = -1
|
||||
this.scopeVisualLine = -1
|
||||
this.offsetOnClick = -1
|
||||
}
|
||||
|
||||
fun isEmpty(): Boolean {
|
||||
return primaryVisualLine == -1 || scopeVisualLine == -1 || offsetOnClick == -1
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
assert(!isEmpty()) { "sticky panel should mark this line as not visible" }
|
||||
val stickyLine = primaryVisualLine
|
||||
val stickyLineY = editor.visualLineToY(stickyLine)
|
||||
val lineHeight = lineHeight()
|
||||
val lineWidth = lineWidth()
|
||||
|
||||
// draw gutter
|
||||
val gutterComp = editor.gutterComponentEx
|
||||
val gutterWidth = max(1, gutterComp.width)
|
||||
val gutterImage: BufferedImage = UIUtil.createImage(editor.component, gutterWidth, lineHeight, BufferedImage.TYPE_INT_RGB)
|
||||
val gutterGraphics = gutterImage.graphics
|
||||
EditorUIUtil.setupAntialiasing(gutterGraphics)
|
||||
|
||||
gutterGraphics.translate(0, -stickyLineY)
|
||||
gutterGraphics.setClip(0, stickyLineY, gutterComp.width, lineHeight)
|
||||
gutterGraphics.color = EditorFragmentComponent.getBackgroundColor(editor)
|
||||
gutterGraphics.fillRect(0, stickyLineY, gutterComp.width, lineHeight)
|
||||
gutterComp.paint(gutterGraphics) // TODO: hide icons (override, annotations, etc)
|
||||
|
||||
// draw text
|
||||
val textWidth = lineWidth - gutterWidth
|
||||
val textImage: BufferedImage = UIUtil.createImage(editor.contentComponent, textWidth, lineHeight, BufferedImage.TYPE_INT_RGB)
|
||||
val textGraphics = textImage.graphics
|
||||
EditorUIUtil.setupAntialiasing(textGraphics)
|
||||
|
||||
textGraphics.translate(0, -stickyLineY)
|
||||
textGraphics.setClip(0, stickyLineY, textWidth, lineHeight)
|
||||
val wasVisible = editor.setCaretVisible(false)
|
||||
try {
|
||||
editor.contentComponent.paint(textGraphics) // TODO: hide caret line background
|
||||
} finally {
|
||||
if (wasVisible) {
|
||||
editor.setCaretVisible(true)
|
||||
}
|
||||
}
|
||||
|
||||
// apply images
|
||||
StartupUiUtil.drawImage(g, gutterImage, 0, 0, null)
|
||||
StartupUiUtil.drawImage(g, textImage, gutterComp.width, 0, null)
|
||||
}
|
||||
|
||||
private fun lineWidth(): Int {
|
||||
return (editor as EditorImpl).stickyLinesPanelWidth
|
||||
}
|
||||
|
||||
private fun lineHeight(): Int {
|
||||
return editor.lineHeight
|
||||
}
|
||||
|
||||
inner class MyMouseEventHandler : MouseEventHandler() {
|
||||
private val popMenu: JPopupMenu
|
||||
private var isPopup = false
|
||||
|
||||
init {
|
||||
val actionManager = ActionManager.getInstance()
|
||||
val actionGroup = actionManager.getAction("EditorStickyLinesSettings") as DefaultActionGroup
|
||||
popMenu = actionManager.createActionPopupMenu("StickyLine", actionGroup).component
|
||||
}
|
||||
|
||||
override fun handle(event: MouseEvent) {
|
||||
if (event.isConsumed) return
|
||||
if (isGutterEvent(event)) {
|
||||
handleGutter(event)
|
||||
} else {
|
||||
handleLine(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLine(event: MouseEvent) {
|
||||
when (event.id) {
|
||||
MouseEvent.MOUSE_PRESSED -> {
|
||||
isPopup = event.isPopupTrigger
|
||||
if (isPopup) {
|
||||
popMenu.show(event.component, event.x, event.y)
|
||||
}
|
||||
}
|
||||
MouseEvent.MOUSE_CLICKED -> {
|
||||
if (!isPopup && !isEmpty()) {
|
||||
editor.caretModel.moveToOffset(offsetOnClick)
|
||||
editor.scrollingModel.scrollToCaret(ScrollType.RELATIVE)
|
||||
}
|
||||
}
|
||||
MouseEvent.MOUSE_RELEASED,
|
||||
MouseEvent.MOUSE_ENTERED,
|
||||
MouseEvent.MOUSE_EXITED,
|
||||
MouseEvent.MOUSE_DRAGGED,
|
||||
MouseEvent.MOUSE_MOVED, -> {}
|
||||
}
|
||||
event.consume()
|
||||
}
|
||||
|
||||
private fun handleGutter(event: MouseEvent) {
|
||||
// TODO: folding region collapsing/expanding
|
||||
}
|
||||
|
||||
private fun isGutterEvent(event: MouseEvent): Boolean {
|
||||
return event.x <= editor.gutterComponentEx.width
|
||||
}
|
||||
}
|
||||
|
||||
inner class GutterMouseEventHandler : MouseEventAdapter<EditorGutterComponentEx>(editor.gutterComponentEx) {
|
||||
override fun convert(event: MouseEvent): MouseEvent {
|
||||
return convert(event, editor.gutterComponentEx)
|
||||
}
|
||||
|
||||
override fun getMouseListener(adapter: EditorGutterComponentEx): MouseListener? {
|
||||
if (adapter is MouseListener && adapter.isShowing) {
|
||||
return adapter
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.editor.impl.stickyLines;
|
||||
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import static org.jetbrains.annotations.ApiStatus.Internal;
|
||||
|
||||
/**
|
||||
* Represents information about sticky line collected by {@link StickyLinesCollector}.
|
||||
* <p>
|
||||
* See also {@link StickyLine}
|
||||
*/
|
||||
@Internal
|
||||
public record StickyLineInfo(int textOffset, int endOffset, @Nullable String debugText) {
|
||||
|
||||
StickyLineInfo(@NotNull TextRange textRange) {
|
||||
this(textRange.getStartOffset(), textRange.getEndOffset(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) return true;
|
||||
if (!(other instanceof StickyLineInfo info)) return false;
|
||||
return endOffset == info.endOffset && textOffset == info.textOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return textOffset + 31 * endOffset;
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.editor.impl.stickyLines
|
||||
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.util.concurrency.ThreadingAssertions
|
||||
import com.intellij.xml.breadcrumbs.PsiFileBreadcrumbsCollector
|
||||
import org.jetbrains.annotations.ApiStatus.Internal
|
||||
|
||||
/**
|
||||
* Responsible for collecting sticky lines based on psi file structure.
|
||||
* The implementation relies on PsiFileBreadcrumbsCollector to collect psi elements at specific document line.
|
||||
*/
|
||||
@Internal
|
||||
class StickyLinesCollector(private val project: Project, private val document: Document) {
|
||||
|
||||
fun collectLines(vFile: VirtualFile): MutableSet<StickyLineInfo> {
|
||||
ThreadingAssertions.assertReadAccess()
|
||||
ThreadingAssertions.assertBackgroundThread()
|
||||
val psiCollector = PsiFileBreadcrumbsCollector(project)
|
||||
val infos: MutableSet<StickyLineInfo> = mutableSetOf()
|
||||
val lineCount: Int = document.getLineCount()
|
||||
for (line in 0 until lineCount) {
|
||||
ProgressManager.checkCanceled()
|
||||
val endOffset: Int = document.getLineEndOffset(line)
|
||||
val psiElements: List<PsiElement> = psiCollector.computePsiElements(vFile, document, endOffset)
|
||||
for (element: PsiElement in psiElements) {
|
||||
if (!isInBanList(element)) {
|
||||
infos.add(StickyLineInfo(
|
||||
element.textOffset,
|
||||
element.textRange.endOffset,
|
||||
debugText(element)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
fun applyLines(linesToAdd: MutableSet<StickyLineInfo>) {
|
||||
ThreadingAssertions.assertEventDispatchThread()
|
||||
val stickyModel: StickyLinesModel = StickyLinesModel.getModel(project, document) ?: return
|
||||
val outdatedLines: List<StickyLine> = findOutdatedLines(stickyModel, linesToAdd) // mutates linesToAdd
|
||||
for (toRemove: StickyLine in outdatedLines) {
|
||||
stickyModel.removeStickyLine(toRemove)
|
||||
}
|
||||
for (toAdd: StickyLineInfo in linesToAdd) {
|
||||
stickyModel.addStickyLine(toAdd.textOffset, toAdd.endOffset, toAdd.debugText)
|
||||
}
|
||||
stickyModel.notifyListeners()
|
||||
}
|
||||
|
||||
private fun findOutdatedLines(stickyModel: StickyLinesModel, linesToAdd: MutableSet<StickyLineInfo>): List<StickyLine> {
|
||||
val outdatedLines: MutableList<StickyLine> = mutableListOf()
|
||||
stickyModel.processStickyLines { line: StickyLine ->
|
||||
val existingLine = StickyLineInfo(line.textRange())
|
||||
val keepExisting = linesToAdd.remove(existingLine)
|
||||
if (!keepExisting) {
|
||||
outdatedLines.add(line)
|
||||
}
|
||||
true
|
||||
}
|
||||
return outdatedLines
|
||||
}
|
||||
|
||||
private fun debugText(element: PsiElement): String? {
|
||||
return if (Registry.`is`("editor.show.sticky.lines.debug")) {
|
||||
element.toString()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isInBanList(element: PsiElement): Boolean {
|
||||
// TODO: provide extension point for suppressing/including psi elements
|
||||
if (element.language.displayName == "Kotlin") {
|
||||
// special handling for kotlin to exclude if/else, try/catch
|
||||
// see org.jetbrains.kotlin.idea.codeInsight.KotlinBreadcrumbsInfoProvider.Holder,
|
||||
// org.jetbrains.kotlin.KtNodeTypes.THEN, org.jetbrains.kotlin.KtNodeTypes.ELSE, etc
|
||||
val debugName = element.toString()
|
||||
return when (debugName) {
|
||||
"THEN", "ELSE", "WHEN", "FINALLY" -> true
|
||||
"BLOCK", "BODY" -> when (element.parent?.toString()) {
|
||||
"TRY", "FOR", "DO_WHILE", "WHEN_ENTRY" -> true
|
||||
else -> false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.editor.impl.stickyLines
|
||||
|
||||
import com.intellij.ide.ui.UISettingsListener
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.editor.event.VisibleAreaEvent
|
||||
import com.intellij.openapi.editor.event.VisibleAreaListener
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.openapi.editor.ex.MarkupModelEx
|
||||
import com.intellij.openapi.fileTypes.FileTypeEvent
|
||||
import com.intellij.openapi.fileTypes.FileTypeListener
|
||||
import com.intellij.openapi.fileTypes.FileTypeManager
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import java.awt.Point
|
||||
import java.awt.Rectangle
|
||||
|
||||
/**
|
||||
* Responsible for updating state of the sticky panel e.g., when the editor is scrolled or resized.
|
||||
*/
|
||||
internal class StickyLinesManager(
|
||||
private val editor: EditorEx,
|
||||
markupModel: MarkupModelEx,
|
||||
parentDisposable: Disposable,
|
||||
) : VisibleAreaListener, Disposable, StickyLinesModel.Listener {
|
||||
|
||||
private val stickyModel: StickyLinesModel = StickyLinesModel.getModel(markupModel)
|
||||
val stickyPanel: StickyLinesPanel = StickyLinesPanel(editor)
|
||||
|
||||
init {
|
||||
Disposer.register(parentDisposable, this)
|
||||
editor.scrollingModel.addVisibleAreaListener(this, this)
|
||||
stickyModel.addListener(this)
|
||||
editor.project!!.messageBus.connect(this).subscribe(
|
||||
UISettingsListener.TOPIC,
|
||||
UISettingsListener { recalculateLinesAndRepaint() }
|
||||
)
|
||||
}
|
||||
|
||||
private var activeVisualLine: Int = -1
|
||||
private var activeEditorY: Int = -1
|
||||
private var activeEditorH: Int = -1
|
||||
|
||||
override fun visibleAreaChanged(event: VisibleAreaEvent) {
|
||||
if (stickyPanel.isStickyEnabled && isAreaChanged(event)) {
|
||||
activeEditorY = event.newRectangle.y
|
||||
activeEditorH = event.newRectangle.height
|
||||
if (event.oldRectangle == null || isLineChanged(event)) {
|
||||
// recalculate sticky lines and repaint
|
||||
stickyPanel.repaintLines(activeEditorY, activeEditorH, getStickyLines(activeEditorY))
|
||||
} else if (isYChanged(event) || isSizeChanged(event)) {
|
||||
// just repaint
|
||||
stickyPanel.repaintLines(activeEditorY, activeEditorH)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun modelChanged() {
|
||||
recalculateLinesAndRepaint()
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
stickyModel.removeListener(this)
|
||||
}
|
||||
|
||||
private fun recalculateLinesAndRepaint() {
|
||||
if (activeVisualLine != -1 && activeEditorY != -1 && activeEditorH != -1) {
|
||||
stickyPanel.repaintLines(activeEditorY, activeEditorH, getStickyLines(activeEditorY))
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAreaChanged(event: VisibleAreaEvent): Boolean {
|
||||
val oldRectangle: Rectangle? = event.oldRectangle
|
||||
return oldRectangle == null ||
|
||||
oldRectangle.y != event.newRectangle.y ||
|
||||
oldRectangle.height != event.newRectangle.height ||
|
||||
oldRectangle.width != event.newRectangle.width
|
||||
}
|
||||
|
||||
private fun isLineChanged(event: VisibleAreaEvent): Boolean {
|
||||
val editorY: Int = event.newRectangle.y
|
||||
val newVisualLine: Int = editor.yToVisualLine(activeY(editorY))
|
||||
if (activeVisualLine != newVisualLine) {
|
||||
activeVisualLine = newVisualLine
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun isYChanged(event: VisibleAreaEvent): Boolean {
|
||||
return event.oldRectangle.y != event.newRectangle.y
|
||||
}
|
||||
|
||||
private fun isSizeChanged(event: VisibleAreaEvent): Boolean {
|
||||
return event.oldRectangle.width != event.newRectangle.width ||
|
||||
event.oldRectangle.height != event.newRectangle.height
|
||||
}
|
||||
|
||||
private fun activeY(editorY: Int): Int {
|
||||
return editorY + stickyPanel.height + /*border*/ 1
|
||||
}
|
||||
|
||||
private fun getStickyLines(editorY: Int): List<StickyLine> {
|
||||
return if (editorY <3) {
|
||||
// special case when the document starts with a sticky line
|
||||
// small visual jump is better than stickied line for good
|
||||
emptyList()
|
||||
} else {
|
||||
val activeY: Int = activeY(editorY)
|
||||
val activeLogicalLine: Int = editor.xyToLogicalPosition(Point(0, activeY)).line
|
||||
val activeOffset: Int = editor.document.getLineEndOffset(activeLogicalLine)
|
||||
collectStickyLines(activeOffset, activeLogicalLine)
|
||||
}
|
||||
}
|
||||
|
||||
private fun collectStickyLines(activeOffset: Int, activeLogicalLine: Int): List<StickyLine> {
|
||||
val visualLinesLimit: Int = editor.settings.stickyLinesLimit
|
||||
// The panel removes visual duplicates so extra logical lines should be collected.
|
||||
// We could collect lines limitless here if it were efficient.
|
||||
// The consequence of this optimization is that in the worst case (when all logical lines are the same visual one),
|
||||
// the sticky panel will consist of only one line instead of a line limit number.
|
||||
// This optimization should be removed if the worst case is not rare.
|
||||
val logicalLinesLimit: Int = visualLinesLimit + 5
|
||||
val stickyLines: MutableList<StickyLine> = ArrayList(logicalLinesLimit)
|
||||
stickyModel.processStickyLines(activeOffset) { stickyLine ->
|
||||
if (activeLogicalLine <= stickyLine.scopeLine()) {
|
||||
stickyLines.add(stickyLine)
|
||||
}
|
||||
return@processStickyLines stickyLines.size < logicalLinesLimit
|
||||
}
|
||||
return stickyLines.sorted()
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.editor.impl.stickyLines;
|
||||
|
||||
import com.intellij.openapi.editor.Document;
|
||||
import com.intellij.openapi.editor.RangeMarker;
|
||||
import com.intellij.openapi.editor.colors.TextAttributesKey;
|
||||
import com.intellij.openapi.editor.ex.MarkupModelEx;
|
||||
import com.intellij.openapi.editor.impl.DocumentMarkupModel;
|
||||
import com.intellij.openapi.editor.markup.HighlighterLayer;
|
||||
import com.intellij.openapi.editor.markup.HighlighterTargetArea;
|
||||
import com.intellij.openapi.editor.markup.MarkupModel;
|
||||
import com.intellij.openapi.editor.markup.RangeHighlighter;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.Key;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.util.Processor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jetbrains.annotations.ApiStatus.Internal;
|
||||
|
||||
/**
|
||||
* Represents a storage of sticky lines.
|
||||
*/
|
||||
@Internal
|
||||
final class StickyLinesModel {
|
||||
|
||||
private static final Key<StickyLinesModel> STICKY_LINES_MODEL_KEY = Key.create("editor.sticky.lines.model");
|
||||
private static final Key<StickyLineImpl> STICKY_LINE_IMPL_KEY = Key.create("editor.sticky.line.impl");
|
||||
private static final TextAttributesKey STICKY_LINE_ATTRIBUTE = TextAttributesKey.createTextAttributesKey(
|
||||
"STICKY_LINE_MARKER"
|
||||
);
|
||||
|
||||
static @Nullable StickyLinesModel getModel(@NotNull Project project, @NotNull Document document) {
|
||||
if (project.isDisposed()) return null;
|
||||
MarkupModel markupModel = DocumentMarkupModel.forDocument(document, project, false);
|
||||
if (markupModel == null) return null;
|
||||
return getModel(markupModel);
|
||||
}
|
||||
|
||||
static @NotNull StickyLinesModel getModel(@NotNull MarkupModel markupModel) {
|
||||
StickyLinesModel stickyModel = markupModel.getUserData(STICKY_LINES_MODEL_KEY);
|
||||
if (stickyModel == null) {
|
||||
stickyModel = new StickyLinesModel((MarkupModelEx) markupModel);
|
||||
markupModel.putUserData(STICKY_LINES_MODEL_KEY, stickyModel);
|
||||
}
|
||||
return stickyModel;
|
||||
}
|
||||
|
||||
private final MarkupModelEx myMarkupModel;
|
||||
private final List<Listener> myListeners;
|
||||
|
||||
private StickyLinesModel(MarkupModelEx markupModel) {
|
||||
myMarkupModel = markupModel;
|
||||
myListeners = new ArrayList<>();
|
||||
}
|
||||
|
||||
void addStickyLine(int textOffset, int endOffset, @Nullable String debugText) {
|
||||
if (textOffset >= endOffset) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"sticky line endOffset %s should be less than textOffset %s", textOffset, endOffset
|
||||
));
|
||||
}
|
||||
RangeHighlighter highlighter = myMarkupModel.addRangeHighlighter(
|
||||
STICKY_LINE_ATTRIBUTE,
|
||||
textOffset,
|
||||
endOffset,
|
||||
HighlighterLayer.SYNTAX,
|
||||
HighlighterTargetArea.EXACT_RANGE
|
||||
);
|
||||
StickyLineImpl stickyLine = new StickyLineImpl(highlighter.getDocument(), highlighter, debugText);
|
||||
highlighter.putUserData(STICKY_LINE_IMPL_KEY, stickyLine);
|
||||
}
|
||||
|
||||
void removeStickyLine(@NotNull StickyLine stickyLine) {
|
||||
RangeMarker rangeMarker = ((StickyLineImpl)stickyLine).rangeMarker();
|
||||
myMarkupModel.removeHighlighter((RangeHighlighter) rangeMarker);
|
||||
}
|
||||
|
||||
void processStickyLines(@NotNull Processor<? super StickyLine> processor) {
|
||||
processStickyLines(myMarkupModel.getDocument().getTextLength(), processor);
|
||||
}
|
||||
|
||||
void processStickyLines(int endOffset, @NotNull Processor<? super StickyLine> processor) {
|
||||
myMarkupModel.processRangeHighlightersOverlappingWith(
|
||||
0,
|
||||
endOffset,
|
||||
highlighter -> {
|
||||
if (highlighter.getTextAttributesKey() == STICKY_LINE_ATTRIBUTE) {
|
||||
StickyLineImpl stickyLine = highlighter.getUserData(STICKY_LINE_IMPL_KEY);
|
||||
if (stickyLine == null) {
|
||||
stickyLine = new StickyLineImpl(highlighter.getDocument(), highlighter, null);
|
||||
}
|
||||
return processor.process(stickyLine);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
void modelChanged();
|
||||
}
|
||||
|
||||
void addListener(Listener listener) {
|
||||
myListeners.add(listener);
|
||||
}
|
||||
|
||||
void removeListener(Listener listener) {
|
||||
myListeners.remove(listener);
|
||||
}
|
||||
|
||||
void notifyListeners() {
|
||||
for (Listener listener : myListeners) {
|
||||
listener.modelChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private record StickyLineImpl(
|
||||
@NotNull Document document,
|
||||
@NotNull RangeMarker rangeMarker,
|
||||
@Nullable String debugText
|
||||
) implements StickyLine {
|
||||
|
||||
@Override
|
||||
public int primaryLine() {
|
||||
return document.getLineNumber(rangeMarker.getStartOffset());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int scopeLine() {
|
||||
return document.getLineNumber(rangeMarker.getEndOffset());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int navigateOffset() {
|
||||
return rangeMarker.getStartOffset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull TextRange textRange() {
|
||||
return rangeMarker.getTextRange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String debugText() {
|
||||
return debugText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) return true;
|
||||
if (!(other instanceof StickyLineImpl impl)) return false;
|
||||
return textRange().equals(impl.textRange());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return textRange().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull StickyLine other) {
|
||||
TextRange range = textRange();
|
||||
TextRange otherRange = other.textRange();
|
||||
int compare = Integer.compare(range.getStartOffset(), otherRange.getStartOffset());
|
||||
if (compare != 0) {
|
||||
return compare;
|
||||
}
|
||||
// reverse order
|
||||
return Integer.compare(otherRange.getEndOffset(), range.getEndOffset());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
String prefix = debugText == null ? "" : debugText;
|
||||
return prefix + "(" + primaryLine() + ", " + scopeLine() + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.editor.impl.stickyLines
|
||||
|
||||
import com.intellij.openapi.editor.LogicalPosition
|
||||
import com.intellij.openapi.editor.VisualPosition
|
||||
import com.intellij.openapi.editor.colors.EditorColors
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.openapi.editor.impl.EditorImpl
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
import com.intellij.ui.ClientProperty
|
||||
import com.intellij.ui.SideBorder
|
||||
import com.intellij.ui.components.JBLayeredPane
|
||||
import com.intellij.ui.components.JBPanel
|
||||
import java.awt.Color
|
||||
import javax.swing.BoxLayout
|
||||
import kotlin.math.min
|
||||
|
||||
internal class StickyLinesPanel(private val editor: EditorEx) : JBPanel<StickyLinesPanel>() {
|
||||
|
||||
// ui
|
||||
private val layeredPane: JBLayeredPane = JBLayeredPane()
|
||||
|
||||
// ui + state
|
||||
private val stickyLinesComp: MutableList<StickyLineComponent> = mutableListOf()
|
||||
|
||||
// state
|
||||
private val stickyLines: MutableList<StickyLine> = mutableListOf()
|
||||
private var editorY: Int = 0
|
||||
private var editorH: Int = 0
|
||||
private var panelW: Int = 0
|
||||
private var panelH: Int = 0
|
||||
|
||||
// settings
|
||||
private val scopeThreshold: Int = 3
|
||||
private val lineLimit: Int get() = editor.settings.stickyLinesLimit
|
||||
val isStickyEnabled: Boolean get() = editor.settings.isStickyLinesShown
|
||||
|
||||
init {
|
||||
border = bottomBorder()
|
||||
layout = BoxLayout(this, BoxLayout.Y_AXIS)
|
||||
layeredPane.layout = null
|
||||
add(layeredPane)
|
||||
}
|
||||
|
||||
// ------------------------------------------- API -------------------------------------------
|
||||
|
||||
fun repaintLinesInRange(startVisualLine: Int, endVisualLine: Int) {
|
||||
if (clearPanelIfDisabled()) return
|
||||
for (lineComp: StickyLineComponent in stickyLinesComp) {
|
||||
if (lineComp.isEmpty()) {
|
||||
break
|
||||
}
|
||||
if (lineComp.primaryVisualLine in startVisualLine..endVisualLine) {
|
||||
lineComp.repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun repaintLines(newEditorY: Int, newEditorH: Int) {
|
||||
if (clearPanelIfDisabled()) return
|
||||
editorY = newEditorY
|
||||
editorH = newEditorH
|
||||
repaintLinesImpl()
|
||||
}
|
||||
|
||||
fun repaintLines(newEditorY: Int, newEditorH: Int, newStickyLines: Collection<StickyLine>) {
|
||||
if (clearPanelIfDisabled()) return
|
||||
editorY = newEditorY
|
||||
editorH = newEditorH
|
||||
stickyLines.clear()
|
||||
stickyLines.addAll(newStickyLines)
|
||||
repaintLinesImpl()
|
||||
}
|
||||
|
||||
fun repaintLines(newStickyLines: Collection<StickyLine>) {
|
||||
if (clearPanelIfDisabled()) return
|
||||
stickyLines.clear()
|
||||
stickyLines.addAll(newStickyLines)
|
||||
repaintLinesImpl()
|
||||
}
|
||||
|
||||
fun suppressHintForLine(logicalLine: Int): Boolean {
|
||||
for (lineComp: StickyLineComponent in stickyLinesComp) {
|
||||
if (lineComp.isEmpty()) {
|
||||
break
|
||||
}
|
||||
val stickyVisualPos = VisualPosition(lineComp.primaryVisualLine, 0)
|
||||
val stickyLogicalLine: Int = editor.visualToLogicalPosition(stickyVisualPos).line
|
||||
if (logicalLine == stickyLogicalLine) {
|
||||
return true
|
||||
}
|
||||
if (logicalLine == stickyLogicalLine - 1) {
|
||||
// case when lineOf(psiElement.startOffset) != lineOf(psiElement.textOffset)
|
||||
// e.g., when a method starts with @Override
|
||||
// "stickyLogicalLine - 1" won't cover all the cases, but the fair approach looks difficult to implement
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ------------------------------------------- Impl -------------------------------------------
|
||||
|
||||
private fun repaintLinesImpl() {
|
||||
val lineLimit: Int = lineLimit // avoid reading settings several times while painting
|
||||
val stickyLines: List<StickyLine> = getStickyLines()
|
||||
lazyAllocateComponents(stickyLines, lineLimit)
|
||||
updateComponentsState(stickyLines, lineLimit)
|
||||
placeComponentsOnPanel()
|
||||
}
|
||||
|
||||
private fun getStickyLines(): List<StickyLine> {
|
||||
return stickyLines
|
||||
// TODO: why are range markers not valid even if the document is not changed?
|
||||
// .filter { it.getRangeMarker().isValid }
|
||||
}
|
||||
|
||||
private fun lazyAllocateComponents(stickyLines: List<StickyLine>, lineLimit: Int) {
|
||||
while (stickyLinesComp.size < min(stickyLines.size, lineLimit)) {
|
||||
val lineComp = StickyLineComponent(editor)
|
||||
layeredPane.add(lineComp, (200 - stickyLinesComp.size) as Any)
|
||||
stickyLinesComp.add(lineComp)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateComponentsState(stickyLines: List<StickyLine>, lineLimit: Int) {
|
||||
var compIndex = 0
|
||||
for (stickyLine: StickyLine in stickyLines) {
|
||||
val primaryVisual: VisualPosition = toVisualPos(stickyLine.primaryLine())
|
||||
val scopeVisual: VisualPosition = toVisualPos(stickyLine.scopeLine())
|
||||
if (isScopeNotNarrow(primaryVisual, scopeVisual) && isLineNotDuplicate(compIndex, primaryVisual)) {
|
||||
stickyLinesComp[compIndex].setLine(primaryVisual, scopeVisual, stickyLine.navigateOffset())
|
||||
compIndex++
|
||||
if (compIndex == lineLimit) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
while (compIndex < stickyLinesComp.size) {
|
||||
stickyLinesComp[compIndex].resetLine()
|
||||
compIndex++
|
||||
}
|
||||
}
|
||||
|
||||
private fun placeComponentsOnPanel() {
|
||||
val lineHeight: Int = editor.lineHeight
|
||||
var panelHeight = 0
|
||||
val panelWidth = (editor as EditorImpl).stickyLinesPanelWidth
|
||||
for (lineComp: StickyLineComponent in stickyLinesComp) {
|
||||
if (lineComp.isEmpty() || isPanelTooBig(panelHeight, lineHeight)) {
|
||||
lineComp.isVisible = false
|
||||
} else {
|
||||
val addedHeight: Int = placeComponentOnPanel(lineComp, panelWidth, panelHeight, lineHeight)
|
||||
// 0 <= addedHeight <= lineHeight
|
||||
panelHeight += addedHeight
|
||||
}
|
||||
}
|
||||
if (isPanelSizeChanged(panelWidth, panelHeight)) {
|
||||
setSize(panelWidth, if (panelHeight == 0) 0 else panelHeight + /*border*/ 1)
|
||||
this.panelW = panelWidth
|
||||
this.panelH = panelHeight
|
||||
layeredPane.setSize(panelWidth, panelHeight)
|
||||
revalidate()
|
||||
}
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun placeComponentOnPanel(
|
||||
lineComp: StickyLineComponent,
|
||||
panelWidth: Int,
|
||||
panelHeight: Int,
|
||||
lineHeight: Int,
|
||||
): Int {
|
||||
val startY1: Int = editor.visualLineToY(lineComp.primaryVisualLine)
|
||||
val startY2: Int = startY1 + lineHeight
|
||||
val endY1: Int = editor.visualLineToY(lineComp.scopeVisualLine)
|
||||
val endY2: Int = endY1 + lineHeight
|
||||
|
||||
val stickyY: Int = editorY + panelHeight + lineHeight
|
||||
if (stickyY in startY2..endY2) {
|
||||
val lineYShift = if (stickyY <= endY1) 0 else stickyY - endY1
|
||||
lineComp.isVisible = true
|
||||
lineComp.setBounds(0, panelHeight - lineYShift, panelWidth, lineHeight)
|
||||
return lineHeight - lineYShift
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------- Utils -------------------------------------------
|
||||
|
||||
private fun toVisualPos(logicalLine: Int): VisualPosition {
|
||||
return editor.logicalToVisualPosition(LogicalPosition(logicalLine, 0))
|
||||
}
|
||||
|
||||
private fun isScopeNotNarrow(primaryVisual: VisualPosition, scopeVisual: VisualPosition): Boolean {
|
||||
return scopeVisual.line - primaryVisual.line + 1 >= scopeThreshold
|
||||
}
|
||||
|
||||
private fun isLineNotDuplicate(compIndex: Int, primaryVisual: VisualPosition): Boolean {
|
||||
if (compIndex == 0) return true
|
||||
val visualLine: Int = primaryVisual.line
|
||||
val prevVisualLine = stickyLinesComp[compIndex - 1].primaryVisualLine
|
||||
assert(prevVisualLine <= visualLine) {
|
||||
"sticky lines list sorted not properly, wrong order of visual lines: $prevVisualLine, $visualLine"
|
||||
}
|
||||
return prevVisualLine != visualLine
|
||||
}
|
||||
|
||||
private fun isPanelTooBig(panelHeight: Int, lineHeight: Int): Boolean {
|
||||
return panelHeight + 2 * lineHeight > editorH / 2
|
||||
}
|
||||
|
||||
private fun isPanelSizeChanged(panelWidth: Int, panelHeight: Int): Boolean {
|
||||
return this.panelW != panelWidth || this.panelH != panelHeight
|
||||
}
|
||||
|
||||
private fun clearPanelIfDisabled(): Boolean {
|
||||
val isEnabled = isStickyEnabled
|
||||
if (!isEnabled && stickyLinesComp.isNotEmpty()) {
|
||||
layeredPane.removeAll()
|
||||
stickyLinesComp.clear()
|
||||
stickyLines.clear()
|
||||
editorY = 0
|
||||
editorH = 0
|
||||
panelW = 0
|
||||
panelH = 0
|
||||
setSize(0, 0)
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
return !isEnabled
|
||||
}
|
||||
|
||||
private fun bottomBorder(): SideBorder {
|
||||
return object : SideBorder(null, BOTTOM) {
|
||||
override fun getLineColor(): Color {
|
||||
val borderColor = ClientProperty.get(
|
||||
this@StickyLinesPanel,
|
||||
FileEditorManager.SEPARATOR_COLOR
|
||||
)
|
||||
if (borderColor != null) {
|
||||
return borderColor
|
||||
}
|
||||
val scheme = EditorColorsManager.getInstance().globalScheme
|
||||
return scheme.getColor(EditorColors.TEARLINE_COLOR) ?: scheme.defaultBackground
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.editor.impl.stickyLines
|
||||
|
||||
import com.intellij.ide.ui.UISettings
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.CommonDataKeys
|
||||
import com.intellij.openapi.actionSystem.ToggleAction
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.ex.EditorSettingsExternalizable
|
||||
import com.intellij.openapi.project.DumbAware
|
||||
import com.intellij.xml.breadcrumbs.BreadcrumbsUtilEx
|
||||
|
||||
// don't show action
|
||||
internal class StickyLinesSettingsHideAction : ToggleAction(), DumbAware {
|
||||
|
||||
override fun isSelected(event: AnActionEvent): Boolean {
|
||||
val settings = EditorSettingsExternalizable.getInstance()
|
||||
// true -> "don't show"
|
||||
return !settings.isStickyLinesShown
|
||||
}
|
||||
|
||||
override fun setSelected(event: AnActionEvent, hide: Boolean) {
|
||||
val settings = EditorSettingsExternalizable.getInstance()
|
||||
val shown = settings.isStickyLinesShown
|
||||
if (shown == hide) {
|
||||
settings.isStickyLinesShown = !shown
|
||||
UISettings.getInstance().fireUISettingsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread {
|
||||
return ActionUpdateThread.BGT
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.intellij.psi.FileViewProvider;
|
||||
import com.intellij.psi.PsiManager;
|
||||
import com.intellij.ui.breadcrumbs.BreadcrumbsProvider;
|
||||
import com.intellij.ui.breadcrumbs.BreadcrumbsUtil;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -66,4 +67,29 @@ public final class BreadcrumbsUtilEx {
|
||||
}
|
||||
return language.getID();
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public static @Nullable BreadcrumbsProvider findProviderForSticky(@NotNull FileViewProvider viewProvider) {
|
||||
EditorSettingsExternalizable settings = EditorSettingsExternalizable.getInstance();
|
||||
if (!settings.isStickyLinesShown()) return null;
|
||||
|
||||
Language baseLang = viewProvider.getBaseLanguage();
|
||||
if (!isStickyLinesShownFor(settings, baseLang)) return null;
|
||||
|
||||
BreadcrumbsProvider provider = BreadcrumbsUtil.getInfoProvider(baseLang);
|
||||
if (provider == null) {
|
||||
for (Language language : viewProvider.getLanguages()) {
|
||||
if (isStickyLinesShownFor(settings, language)) {
|
||||
provider = BreadcrumbsUtil.getInfoProvider(language);
|
||||
if (provider != null) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
private static boolean isStickyLinesShownFor(@NotNull EditorSettingsExternalizable settings, Language language) {
|
||||
String id = findLanguageWithBreadcrumbSettings(language);
|
||||
return settings.isStickyLinesShownFor(id);
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -28,6 +28,7 @@ import com.intellij.ui.breadcrumbs.BreadcrumbsUtil;
|
||||
import com.intellij.ui.components.breadcrumbs.Crumb;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.util.text.CharArrayUtil;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -101,6 +102,24 @@ public final class PsiFileBreadcrumbsCollector extends FileBreadcrumbsCollector
|
||||
return result;
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public @NotNull List<PsiElement> computePsiElements(@NotNull VirtualFile file, @NotNull Document document, int offset) {
|
||||
boolean checkSettings = false;
|
||||
BreadcrumbsProvider defaultInfoProvider = findProvider(file, myProject, true);
|
||||
PsiElement element = findStartElement(document, offset, file, myProject, defaultInfoProvider, checkSettings); // TODO: check settings?
|
||||
if (element == null) return Collections.emptyList();
|
||||
ArrayList<PsiElement> result = new ArrayList<>();
|
||||
while (element != null) {
|
||||
BreadcrumbsProvider provider = findProviderForElement(element, defaultInfoProvider, checkSettings);
|
||||
if (provider != null && provider.acceptElement(element)) {
|
||||
result.add(element);
|
||||
}
|
||||
element = getParent(element, provider);
|
||||
if (element instanceof PsiDirectory) break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CrumbPresentation @Nullable [] getCrumbPresentations(final PsiElement[] elements) {
|
||||
for (BreadcrumbsPresentationProvider provider : BreadcrumbsPresentationProvider.EP_NAME.getExtensionList()) {
|
||||
final CrumbPresentation[] presentations = provider.getCrumbPresentations(elements);
|
||||
|
||||
@@ -1864,6 +1864,11 @@ action.EditorBreadcrumbsShowBelow.description=Show the breadcrumbs bar below the
|
||||
action.EditorBreadcrumbsHideBoth.text=Don't Show
|
||||
action.EditorBreadcrumbsHideBoth.description=Hide the breadcrumbs bar
|
||||
|
||||
group.EditorStickyLinesSettings.text=Sticky Lines
|
||||
group.EditorStickyLinesSettings.description=Toggle sticky lines panel settings
|
||||
action.EditorStickyLinesHide.text=Disable Sticky Lines
|
||||
action.EditorStickyLinesHide.description=Hide sticky lines panel
|
||||
|
||||
group.MarkFileAs.text=Mark File as
|
||||
action.OverrideFileTypeAction.text=Override File Type
|
||||
action.OverrideFileTypeAction.description=Treat current file as a file of different file type
|
||||
|
||||
@@ -1603,6 +1603,7 @@
|
||||
<highlightingPassFactory implementation="com.intellij.codeInsight.daemon.impl.LocalInspectionsPassFactory"/>
|
||||
<highlightingPassFactory implementation="com.intellij.codeInsight.daemon.impl.ShowAutoImportPassFactory"/>
|
||||
<highlightingPassFactory implementation="com.intellij.codeInsight.documentation.render.DocRenderPassFactory"/>
|
||||
<highlightingPassFactory implementation="com.intellij.codeInsight.stickyLines.StickyLinesPassFactory"/>
|
||||
|
||||
<highlightingPassFactory implementation="com.intellij.codeInsight.daemon.impl.WholeFileLocalInspectionsPassFactory"/>
|
||||
<highlightingPassFactory implementation="com.intellij.codeInsight.daemon.impl.ExternalToolPassFactory"/>
|
||||
|
||||
@@ -1586,6 +1586,11 @@
|
||||
<add-to-group group-id="EditorGutterPopupMenu.Appearance" relative-to-action="EditorGutterToggleGlobalIndentLines" anchor="after"/>
|
||||
</group>
|
||||
|
||||
<group id="EditorStickyLinesSettings" popup="true">
|
||||
<action id="EditorStickyLinesHide" class="com.intellij.openapi.editor.impl.stickyLines.StickyLinesSettingsHideAction"/>
|
||||
<add-to-group group-id="EditorGutterPopupMenu.Appearance" relative-to-action="EditorBreadcrumbsSettings" anchor="after"/>
|
||||
</group>
|
||||
|
||||
<action class="com.intellij.ide.plugins.InstallFromDiskAction" id="plugin.InstallFromDiskAction"/>
|
||||
|
||||
<action class="com.intellij.openapi.editor.actions.ShowFontsUsedByEditorAction" id="ShowFontsUsedByEditor"/>
|
||||
|
||||
@@ -2436,5 +2436,8 @@ coroutine.scope.model.description=Use the new experimental coroutine-based model
|
||||
ide.treat.project.modality.as.application=true
|
||||
ide.treat.project.modality.as.application.description=Treat DialogWrapper.IdeModalityType.PROJECT as Dialog.ModalityType.APPLICATION_MODAL
|
||||
|
||||
editor.show.sticky.lines.debug=false
|
||||
editor.show.sticky.lines.debug.description=Show editor sticky lines in debug mode
|
||||
|
||||
# please leave this note as last line
|
||||
# TODO please use EP com.intellij.registryKey for plugin/product specific keys
|
||||
|
||||
Reference in New Issue
Block a user