Skip to content

Commit

Permalink
Provide sticky scrolling in source editors eclipse-platform#1719
Browse files Browse the repository at this point in the history
With this change, sticky scrolling is introduced into Eclipse text editors. Sticky scrolling will keep certain source code lines visible and in a fixed position on the screen as the user scrolls down the page. This technique improves user experience by keeping information within reach at all times.

The feature can be enabled via the TextEditor settings.

Provides feature for eclipse-platform#1719 and eclipse-jdt/eclipse.jdt.ui#1364
  • Loading branch information
Christopher-Hermann committed Jun 10, 2024
1 parent 385b30b commit 7a93201
Show file tree
Hide file tree
Showing 16 changed files with 1,575 additions and 3 deletions.
1 change: 1 addition & 0 deletions bundles/org.eclipse.ui.editors/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Export-Package:
org.eclipse.ui.internal.editors.text;x-internal:=true,
org.eclipse.ui.internal.editors.text.codemining.annotation;x-internal:=true,
org.eclipse.ui.internal.texteditor;x-internal:=true,
org.eclipse.ui.internal.texteditor.stickyscroll;x-internal:=true,
org.eclipse.ui.texteditor
Require-Bundle:
org.eclipse.core.runtime;bundle-version="[3.29.0,4.0.0)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,9 @@ private OverlayPreferenceStore createOverlayStore() {
overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.INT, AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_LEVEL));
overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.INT, AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_MAX));

overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_STICKY_SCROLLING_ENABLED));
overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.INT, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_STICKY_SCROLLING_MAXIMUM_COUNT));

overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SHOW_LEADING_SPACES));
overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SHOW_ENCLOSED_SPACES));
overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SHOW_TRAILING_SPACES));
Expand Down Expand Up @@ -996,6 +999,16 @@ public void widgetSelected(SelectionEvent e) {
((Combo) showCodeMiningsControls[1]).addSelectionListener(codeMiningsListener);
fMasterSlaveListeners.add(codeMiningsListener);

label= TextEditorMessages.TextEditorDefaultsPreferencePage_stickyScrollingEnabled;
Preference stickyScrollingEnabled= new Preference(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_STICKY_SCROLLING_ENABLED, label, null);
Button stickyScrollingEnabledButton= addCheckBox(appearanceComposite, stickyScrollingEnabled, new BooleanDomain(), 0);

label= TextEditorMessages.TextEditorDefaultsPreferencePage_stickyScrollingMaximumCount;
description= TextEditorMessages.TextEditorDefaultsPreferencePage_stickyScrollingMaximumCount;
Preference stickyScrollingMaximumCount= new Preference(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_STICKY_SCROLLING_MAXIMUM_COUNT, label, description);
final IntegerDomain stickyScrollingMaximumCountDomain= new IntegerDomain(1, 10);
final Control[] stickyScrollingMaximumCountControls= addTextField(appearanceComposite, stickyScrollingMaximumCount, stickyScrollingMaximumCountDomain, 15, 20);
createDependency(stickyScrollingEnabledButton, stickyScrollingEnabled, stickyScrollingMaximumCountControls);

addFiller(appearanceComposite, 2);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ private TextEditorMessages() {
public static String TextEditorDefaultsPreferencePage_showWhitespaceCharactersDialogInvalidInput;
public static String TextEditorDefaultsPreferencePage_showWhitespaceCharactersDialogTitle;
public static String TextEditorDefaultsPreferencePage_space;
public static String TextEditorDefaultsPreferencePage_stickyScrollingEnabled;
public static String TextEditorDefaultsPreferencePage_stickyScrollingMaximumCount;
public static String TextEditorDefaultsPreferencePage_stickyScrollingMaximumCount_description;
public static String TextEditorDefaultsPreferencePage_tab;
public static String TextEditorDefaultsPreferencePage_textDragAndDrop;
public static String TextEditorDefaultsPreferencePage_trailing;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ TextEditorDefaultsPreferencePage_showWhitespaceCharactersLinkText= (<a>configure
TextEditorDefaultsPreferencePage_showWhitespaceCharactersDialogInvalidInput=''{0}'' is not a valid input.
TextEditorDefaultsPreferencePage_showWhitespaceCharactersDialogTitle=Show Whitespace Characters
TextEditorDefaultsPreferencePage_space=Space ( \u00b7 )
TextEditorDefaultsPreferencePage_stickyScrollingEnabled=Enable stic&ky scrolling
TextEditorDefaultsPreferencePage_stickyScrollingMaximumCount=Sticky scroll maximum lines count
TextEditorDefaultsPreferencePage_stickyScrollingMaximumCount_description=Limits the number of shown sticky lines when scrolling
TextEditorDefaultsPreferencePage_tab=Tab ( \u00bb )
TextEditorDefaultsPreferencePage_textDragAndDrop= Enable drag and dro&p of text
TextEditorDefaultsPreferencePage_trailing=Trailing
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*******************************************************************************
* Copyright (c) 2024 SAP SE.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* SAP SE - initial API and implementation
*******************************************************************************/
package org.eclipse.ui.internal.texteditor.stickyscroll;

/**
*
* A record representing a sticky line containing the text to display, and line number. It serves as
* an abstraction to represent sticky line for sticky scrolling.
*
* @param text the text of the corresponding sticky line
* @param lineNumber the specific line number of the sticky line
*/
public record StickyLine(String text, int lineNumber) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*******************************************************************************
* Copyright (c) 2024 SAP SE.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* SAP SE - initial API and implementation
*******************************************************************************/
package org.eclipse.ui.internal.texteditor.stickyscroll;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import org.eclipse.swt.custom.StyledText;

import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.source.ISourceViewer;

/**
* This class provides sticky lines for the given source code in the source viewer. The
* implementation is completely based on indentation and therefore should work by default for
* several languages.
*/
public class StickyLinesProvider {

private final static int IGNORE_INDENTATION= -1;

private final static String TAB= "\t"; //$NON-NLS-1$

private int tabWidth= 4;

/**
* Calculate the sticky lines for the given source code in the source viewer for the given
* vertical offset.
*
* @param verticalOffset The vertical offset line index of the first visible line
* @param sourceViewer The source viewer containing the source code
* @return A list of sticky lines
*/
public List<StickyLine> get(int verticalOffset, ISourceViewer sourceViewer) {
LinkedList<StickyLine> stickyLines= new LinkedList<>();

if (verticalOffset == 0) {
return stickyLines;
}

try {
StyledText textWidget= sourceViewer.getTextWidget();
int startLine= textWidget.getTopIndex();

calculateStickyLinesForLineNumber(stickyLines, sourceViewer, startLine);
calculateStickyLinesUnderStickyLineControl(stickyLines, sourceViewer, startLine);
} catch (IllegalArgumentException e) {
stickyLines.clear();
}

return stickyLines;
}

private void calculateStickyLinesForLineNumber(LinkedList<StickyLine> stickyLines, ISourceViewer sourceViewer, int lineNumber) {
StyledText textWidget= sourceViewer.getTextWidget();
int startIndetation= getStartIndentation(lineNumber, textWidget);

for (int i= lineNumber, previousIndetation= startIndetation; i >= 0; i--) {
String line= textWidget.getLine(i);
int indentation= getIndentation(line);

if (indentation == IGNORE_INDENTATION) {
continue;
}

if (indentation < previousIndetation) {
previousIndetation= indentation;
stickyLines.addFirst(new StickyLine(line, mapLineNumberToSourceViewerLine(i, sourceViewer)));
}
}
}

private void calculateStickyLinesUnderStickyLineControl(LinkedList<StickyLine> stickyLines, ISourceViewer sourceViewer, int startLine) {
int firstBelowControl= startLine + stickyLines.size();
StyledText textWidget= sourceViewer.getTextWidget();
int lineCount= textWidget.getLineCount();

for (int i= startLine; i < firstBelowControl && i < lineCount; i++) {

String line= textWidget.getLine(i);
int indentation= getIndentation(line);
if (indentation == IGNORE_INDENTATION) {
continue;
}

while (!stickyLines.isEmpty() && indentation <= getLastStickyLineIndentation(stickyLines) && i < firstBelowControl) {
stickyLines.removeLast();
firstBelowControl--;
}

String nextContentLine= getNextContentLine(i, textWidget);
if (getIndentation(nextContentLine) > indentation && i < firstBelowControl) {
stickyLines.addLast(new StickyLine(line, mapLineNumberToSourceViewerLine(i, sourceViewer)));
firstBelowControl++;
continue;
}
}
}

private int getLastStickyLineIndentation(LinkedList<StickyLine> stickyLines) {
String text= stickyLines.getLast().text();
return getIndentation(text);
}

private int mapLineNumberToSourceViewerLine(int lineNumber, ISourceViewer sourceViewer) {
if (sourceViewer instanceof ITextViewerExtension5 extension) {
return extension.widgetLine2ModelLine(lineNumber);
}
return lineNumber;
}

private int getStartIndentation(int startFromLine, StyledText styledText) {
int indentation= getIndentation(styledText.getLine(startFromLine));
if (indentation != IGNORE_INDENTATION) {
return indentation;
} else {
int nextContentLine= getIndentation(getNextContentLine(startFromLine, styledText));
int previousContentLine= getIndentation(getPreviousContentLine(startFromLine, styledText));
return Math.max(nextContentLine, previousContentLine);
}
}

private String getNextContentLine(int startFromLine, StyledText styledText) {
for (int i= startFromLine + 1; i < styledText.getLineCount(); i++) {
String line= styledText.getLine(i);
if (!line.isBlank()) {
return line;
}
}
return null;
}

private String getPreviousContentLine(int startFromLine, StyledText styledText) {
for (int i= startFromLine - 1; i >= 0; i--) {
String line= styledText.getLine(i);
if (!line.isBlank()) {
return line;
}
}
return null;
}

private int getIndentation(String line) {
if (line == null || line.isBlank()) {
return IGNORE_INDENTATION;
}
String tabAsSpaces= String.join("", Collections.nCopies(tabWidth, " ")); //$NON-NLS-1$ //$NON-NLS-2$

line= line.replace(TAB, tabAsSpaces);
return line.length() - line.stripLeading().length();
}

/**
* Sets the with in spaces of a tab in the editor.
*
* @param tabWidth The amount of spaces a tab is using.
*/
public void setTabWidth(int tabWidth) {
this.tabWidth= tabWidth;
}

}
Loading

0 comments on commit 7a93201

Please sign in to comment.