Skip to content
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
0d3f4c7
preliminary code written in order to be able to define the different …
aiday-mar Feb 14, 2023
874e0af
changed the code in order to allow either the outline model or the fo…
aiday-mar Feb 21, 2023
b2c117f
removing console
aiday-mar Feb 21, 2023
8dcb642
adding the changes so as to allow choosing between outline model, fol…
aiday-mar Feb 27, 2023
b8412be
added some changes
aiday-mar Feb 28, 2023
6be9024
added changes in order to enable the usage of the folding model even …
aiday-mar Feb 28, 2023
68a3076
removing some code
aiday-mar Mar 1, 2023
ed2c747
bdinding the functions to this instance
aiday-mar Mar 1, 2023
64107a2
changed to editorModel, was throwing errors before
aiday-mar Mar 1, 2023
38196bb
writing more concise code for the registration and unregistration of …
aiday-mar Mar 1, 2023
8752fdd
making the logic more robust
aiday-mar Mar 1, 2023
b6dfd31
work in progress
aiday-mar Mar 1, 2023
c611c18
resetting the folding.ts file to what it was
aiday-mar Mar 1, 2023
13fe993
changing the code so as not to use the folding controller
aiday-mar Mar 1, 2023
2359ad4
injecting two more services into the sticky scroll controller
aiday-mar Mar 1, 2023
1aa24e6
adding the finishing touches
aiday-mar Mar 1, 2023
f664892
changing the sticky lines provider in the test file too
aiday-mar Mar 1, 2023
1792851
removing some console logs
aiday-mar Mar 1, 2023
500b635
reoving an import
aiday-mar Mar 1, 2023
3c85f5b
cleaning the code
aiday-mar Mar 1, 2023
85bea40
merging main
aiday-mar Mar 1, 2023
364425a
adding changes
aiday-mar Mar 1, 2023
fc18845
updating the tests in order to include the default model
aiday-mar Mar 1, 2023
e97e98d
removing useless space
aiday-mar Mar 2, 2023
bccdbf8
Changing the name used for debugging
aiday-mar Mar 2, 2023
ad0fe31
adding an empty line in order to retrigger the checks
aiday-mar Mar 2, 2023
954b785
changing the editor options so that they are lower-case and camel-case
aiday-mar Mar 3, 2023
1231465
changing the code so that the folding regions are used directly witho…
aiday-mar Mar 3, 2023
bcc9cb8
removing some useless comments
aiday-mar Mar 3, 2023
f4612b2
adding the changes from the review
aiday-mar Mar 6, 2023
2acb9c2
changing the location where the update scheduler is used, placing the…
aiday-mar Mar 6, 2023
e6f9832
Using the parent index from the folding regions instead of using a wh…
aiday-mar Mar 6, 2023
c9badac
Return false not undefined when there is no valid syntax range provid…
aiday-mar Mar 6, 2023
ac8092d
adding await in front of the update scheduler call
aiday-mar Mar 6, 2023
c648c2a
removing the check on the size of the folding regions
aiday-mar Mar 8, 2023
11d2cfc
Using a promise on the outline model
aiday-mar Mar 8, 2023
d2b25cc
cleaning the code
aiday-mar Mar 8, 2023
a481b9a
work in progress, close to finishing
aiday-mar Mar 8, 2023
8202cf9
cleaning the code, polishing the refactoring
aiday-mar Mar 8, 2023
8016c2e
cleaning the code
aiday-mar Mar 8, 2023
e37453b
cleaning the code
aiday-mar Mar 8, 2023
051c06a
Changing createModel to createModelFromProvider
aiday-mar Mar 8, 2023
c19a662
Separating the sticky element, sticky range and sticky model into a s…
aiday-mar Mar 8, 2023
d89641e
retriggering the tests
aiday-mar Mar 9, 2023
73b37e6
cleaning the code
aiday-mar Mar 9, 2023
105013a
removing useless function in then
aiday-mar Mar 9, 2023
e5bade7
making the method implementation protected as they are in the interface
aiday-mar Mar 9, 2023
e92ade3
making the method implementation protected as they are in the interface
aiday-mar Mar 9, 2023
a53d6a4
Cleaning the code, adding JS docs, making the createStickyModel retur…
aiday-mar Mar 9, 2023
df76aa6
Adding type to status
aiday-mar Mar 9, 2023
0afb059
moving the fromOutlineModel function into the stickyScrollModelProvid…
aiday-mar Mar 9, 2023
56a8156
Doing the debounce calculation after the call to computeStickyModel
aiday-mar Mar 9, 2023
001c28b
using generic types and making one method private
aiday-mar Mar 10, 2023
743be44
Making the computeStickyScroll return a status as well as a model pro…
aiday-mar Mar 10, 2023
0715697
specifying the feature registry provider when there is a provider
aiday-mar Mar 10, 2023
70158bf
changing the order of the services injected
aiday-mar Mar 12, 2023
660ab67
Merge branch 'main' into aiday/issue162904
aiday-mar Mar 13, 2023
d8d1112
Adding changes from the review
aiday-mar Mar 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/vs/editor/common/config/editorOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2654,7 +2654,10 @@ export interface IEditorStickyScrollOptions {
* Maximum number of sticky lines to show
*/
maxLineCount?: number;

/**
* Model to choose for sticky scroll by default
*/
defaultModel?: 'outlineModel' | 'foldingProviderModel' | 'indentationModel';
}

/**
Expand All @@ -2665,21 +2668,27 @@ export type EditorStickyScrollOptions = Readonly<Required<IEditorStickyScrollOpt
class EditorStickyScroll extends BaseEditorOption<EditorOption.stickyScroll, IEditorStickyScrollOptions, EditorStickyScrollOptions> {

constructor() {
const defaults: EditorStickyScrollOptions = { enabled: false, maxLineCount: 5 };
const defaults: EditorStickyScrollOptions = { enabled: false, maxLineCount: 5, defaultModel: 'outlineModel' };
super(
EditorOption.stickyScroll, 'stickyScroll', defaults,
{
'editor.stickyScroll.enabled': {
type: 'boolean',
default: defaults.enabled,
description: nls.localize('editor.stickyScroll', "Shows the nested current scopes during the scroll at the top of the editor.")
description: nls.localize('editor.stickyScroll.enabled', "Shows the nested current scopes during the scroll at the top of the editor.")
},
'editor.stickyScroll.maxLineCount': {
type: 'number',
default: defaults.maxLineCount,
minimum: 1,
maximum: 10,
description: nls.localize('editor.stickyScroll.', "Defines the maximum number of sticky lines to show.")
description: nls.localize('editor.stickyScroll.maxLineCount', "Defines the maximum number of sticky lines to show.")
},
'editor.stickyScroll.defaultModel': {
type: 'string',
enum: ['outlineModel', 'foldingProviderModel', 'indentationModel'],
default: defaults.defaultModel,
description: nls.localize('editor.stickyScroll.defaultModel', "Defines the model to use for determining which lines to stick. If the outline model does not exist, it will fall back on the folding provider model which falls back on the indentation model. This order is respected in all three cases.")
},
}
);
Expand All @@ -2693,6 +2702,7 @@ class EditorStickyScroll extends BaseEditorOption<EditorOption.stickyScroll, IEd
return {
enabled: boolean(input.enabled, this.defaultValue.enabled),
maxLineCount: EditorIntOption.clampedInt(input.maxLineCount, this.defaultValue.maxLineCount, 1, 10),
defaultModel: stringSet<'outlineModel' | 'foldingProviderModel' | 'indentationModel'>(input.defaultModel, this.defaultValue.defaultModel, ['outlineModel', 'foldingProviderModel', 'indentationModel']),
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions';
import { StickyScrollWidget, StickyScrollWidgetState } from './stickyScrollWidget';
import { IStickyLineCandidateProvider, StickyLineCandidateProvider, StickyRange } from './stickyScrollProvider';
import { IStickyLineCandidateProvider, StickyLineCandidateProvider } from './stickyScrollProvider';
import { IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
Expand All @@ -22,7 +22,10 @@ import { getDefinitionsAtPosition } from 'vs/editor/contrib/gotoSymbol/browser/g
import { goToDefinitionWithLocation } from 'vs/editor/contrib/inlayHints/browser/inlayHintsLocations';
import { Position } from 'vs/editor/common/core/position';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce';
import * as dom from 'vs/base/browser/dom';
import { StickyRange } from 'vs/editor/contrib/stickyScroll/browser/stickyScrollElement';

interface CustomMouseEvent {
detail: string;
Expand Down Expand Up @@ -67,46 +70,35 @@ export class StickyScrollController extends Disposable implements IEditorContrib
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@IInstantiationService private readonly _instaService: IInstantiationService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@ILanguageConfigurationService _languageConfigurationService: ILanguageConfigurationService,
@ILanguageFeatureDebounceService _languageFeatureDebounceService: ILanguageFeatureDebounceService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService
) {
super();
this._stickyScrollWidget = new StickyScrollWidget(this._editor);
this._stickyLineCandidateProvider = new StickyLineCandidateProvider(this._editor, this._languageFeaturesService);
this._stickyLineCandidateProvider = new StickyLineCandidateProvider(this._editor, _languageFeaturesService, _languageConfigurationService, _languageFeatureDebounceService);
this._register(this._stickyScrollWidget);
this._register(this._stickyLineCandidateProvider);

// The line numbers to stick
this._widgetState = new StickyScrollWidgetState([], 0);

// Read the initial configuration to check if sticky scroll is enabled
this._readConfiguration();
// If the editor option for sticky scroll has changed, read the configuration again
this._register(this._editor.onDidChangeConfiguration(e => {
if (e.hasChanged(EditorOption.stickyScroll)) {
this._readConfiguration();
}
}));

this._register(dom.addDisposableListener(this._stickyScrollWidget.getDomNode(), dom.EventType.CONTEXT_MENU, async (event: MouseEvent) => {
this._onContextMenu(event);
}));

// Context key which indicates if the sticky scroll is focused or not
this._stickyScrollFocusedContextKey = EditorContextKeys.stickyScrollFocused.bindTo(this._contextKeyService);
this._stickyScrollVisibleContextKey = EditorContextKeys.stickyScrollVisible.bindTo(this._contextKeyService);

// Create a focus tracker to track the focus on the sticky scroll widget
const focusTracker = this._register(dom.trackFocus(this._stickyScrollWidget.getDomNode()));
// Focus placed elsewhere in the window
this._register(focusTracker.onDidBlur(_ => {
this._disposeFocusStickyScrollStore();
}));
// Focusing for example through tabbing in the window
this._register(focusTracker.onDidFocus(_ => {
this.focus();
}));

// Register the click link gesture to allow the user to click on the sticky scroll widget to go to the definition
this._register(this._createClickLinkGesture());
}

Expand All @@ -118,7 +110,6 @@ export class StickyScrollController extends Disposable implements IEditorContrib
return this._widgetState;
}

// Get function to allow actions to use the sticky scroll controller
public static get(editor: ICodeEditor): IStickyScrollController | null {
return editor.getContribution<StickyScrollController>(StickyScrollController.ID);
}
Expand All @@ -132,14 +123,11 @@ export class StickyScrollController extends Disposable implements IEditorContrib
public focus(): void {
const focusState = this._stickyScrollFocusedContextKey.get();
if (focusState === true) {
// If already focused or no line to focus on, return
return;
}
this._focused = true;
this._focusDisposableStore = new DisposableStore();
this._stickyScrollFocusedContextKey.set(true);

// The first line focused is the bottom most line
const rootNode = this._stickyScrollWidget.getDomNode();
(rootNode.lastElementChild! as HTMLDivElement).focus();
this._stickyElements = rootNode.children;
Expand Down Expand Up @@ -280,16 +268,17 @@ export class StickyScrollController extends Disposable implements IEditorContrib
this._editor.removeOverlayWidget(this._stickyScrollWidget);
this._sessionStore.clear();
return;
} else {
this._editor.addOverlayWidget(this._stickyScrollWidget);
this._sessionStore.add(this._editor.onDidScrollChange(() => this._renderStickyScroll()));
this._sessionStore.add(this._editor.onDidLayoutChange(() => this._onDidResize()));
this._sessionStore.add(this._editor.onDidChangeModelTokens((e) => this._onTokensChange(e)));
this._sessionStore.add(this._stickyLineCandidateProvider.onDidChangeStickyScroll(() => this._renderStickyScroll()));
const lineNumberOption = this._editor.getOption(EditorOption.lineNumbers);
if (lineNumberOption.renderType === RenderLineNumbersType.Relative) {
this._sessionStore.add(this._editor.onDidChangeCursorPosition(() => this._renderStickyScroll()));
}
}

this._editor.addOverlayWidget(this._stickyScrollWidget);
this._sessionStore.add(this._editor.onDidScrollChange(() => this._renderStickyScroll()));
this._sessionStore.add(this._editor.onDidLayoutChange(() => this._onDidResize()));
this._sessionStore.add(this._editor.onDidChangeModelTokens((e) => this._onTokensChange(e)));
this._sessionStore.add(this._stickyLineCandidateProvider.onDidChangeStickyScroll(() => this._renderStickyScroll()));
const lineNumberOption = this._editor.getOption(EditorOption.lineNumbers);

if (lineNumberOption.renderType === RenderLineNumbersType.Relative) {
this._sessionStore.add(this._editor.onDidChangeCursorPosition(() => this._renderStickyScroll()));
}
}

Expand All @@ -315,7 +304,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib
const layoutInfo = this._editor.getLayoutInfo();
const width = layoutInfo.width - layoutInfo.minimap.minimapCanvasOuterWidth - layoutInfo.verticalScrollbarWidth;
this._stickyScrollWidget.getDomNode().style.width = `${width}px`;
// make sure sticky scroll doesn't take up more than 25% of the editor
// Make sure sticky scroll doesn't take up more than 25% of the editor
const theoreticalLines = layoutInfo.height / this._editor.getOption(EditorOption.lineHeight);
this._maxStickyLines = Math.round(theoreticalLines * .25);
}
Expand All @@ -330,28 +319,23 @@ export class StickyScrollController extends Disposable implements IEditorContrib
this._widgetState = this.findScrollWidgetState();
this._stickyScrollVisibleContextKey.set(!(this._widgetState.lineNumbers.length === 0));

// If the sticky scroll widget is focused then rerender the focus
if (!this._focused) {
this._stickyScrollWidget.setState(this._widgetState);
} else {
this._stickyElements = this._stickyScrollWidget.getDomNode().children;
// When there are no more sticky line, lose focus
if (this._stickyElements.length === 0) {
this._disposeFocusStickyScrollStore();
this._stickyScrollWidget.setState(this._widgetState);
} else {
// Finding the focused line number before rendering
const focusedStickyElementLineNumber = this._stickyScrollWidget.lineNumbers[this._focusedStickyElementIndex];
// Rendering
this._stickyScrollWidget.setState(this._widgetState);
// Checking if after rendering the line number is still in the sticky scroll widget
const previousFocusedLineNumberExists = this._stickyScrollWidget.lineNumbers.includes(focusedStickyElementLineNumber);

// If the line number is still there, do not change anything
// If the line number is not there, set the new focused line to be the last line
if (!previousFocusedLineNumberExists) {
this._focusedStickyElementIndex = this._stickyElements.length - 1;
}
// Focus the line
(this._stickyElements.item(this._focusedStickyElementIndex) as HTMLDivElement).focus();
}
}
Expand Down
160 changes: 160 additions & 0 deletions src/vs/editor/contrib/stickyScroll/browser/stickyScrollElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Iterable } from 'vs/base/common/iterator';
import { URI } from 'vs/base/common/uri';
import { OutlineElement, OutlineGroup, OutlineModel } from 'vs/editor/contrib/documentSymbols/browser/outlineModel';
import { FoldingRegions } from 'vs/editor/contrib/folding/browser/foldingRanges';

export class StickyRange {
constructor(
public readonly startLineNumber: number,
public readonly endLineNumber: number
) { }
}

export class StickyElement {

private static comparator(range1: StickyRange, range2: StickyRange): number {
if (range1.startLineNumber !== range2.startLineNumber) {
return range1.startLineNumber - range2.startLineNumber;
} else {
return range2.endLineNumber - range1.endLineNumber;
}
}

public static fromOutlineElement(outlineElement: OutlineElement, previousStartLine: number): StickyElement {
const children: StickyElement[] = [];
for (const child of outlineElement.children.values()) {
if (child.symbol.selectionRange.startLineNumber !== child.symbol.range.endLineNumber) {
if (child.symbol.selectionRange.startLineNumber !== previousStartLine) {
children.push(StickyElement.fromOutlineElement(child, child.symbol.selectionRange.startLineNumber));
} else {
for (const subchild of child.children.values()) {
children.push(StickyElement.fromOutlineElement(subchild, child.symbol.selectionRange.startLineNumber));
}
}
}
}
children.sort((child1, child2) => this.comparator(child1.range!, child2.range!));
const range = new StickyRange(outlineElement.symbol.selectionRange.startLineNumber, outlineElement.symbol.range.endLineNumber);
return new StickyElement(range, children, undefined);
}

public static fromOutlineModel(outlineModel: OutlineModel, preferredProvider: string | undefined): { stickyOutlineElement: StickyElement; providerID: string | undefined } {

let outlineElements: Map<string, OutlineElement>;
// When several possible outline providers
if (Iterable.first(outlineModel.children.values()) instanceof OutlineGroup) {
const provider = Iterable.find(outlineModel.children.values(), outlineGroupOfModel => outlineGroupOfModel.id === preferredProvider);
if (provider) {
outlineElements = provider.children;
} else {
let tempID = '';
let maxTotalSumOfRanges = -1;
let optimalOutlineGroup = undefined;
for (const [_key, outlineGroup] of outlineModel.children.entries()) {
const totalSumRanges = StickyElement.findSumOfRangesOfGroup(outlineGroup);
if (totalSumRanges > maxTotalSumOfRanges) {
optimalOutlineGroup = outlineGroup;
maxTotalSumOfRanges = totalSumRanges;
tempID = outlineGroup.id;
}
}
preferredProvider = tempID;
outlineElements = optimalOutlineGroup!.children;
}
} else {
outlineElements = outlineModel.children as Map<string, OutlineElement>;
}
const stickyChildren: StickyElement[] = [];
const outlineElementsArray = Array.from(outlineElements.values()).sort((element1, element2) => {
const range1: StickyRange = new StickyRange(element1.symbol.range.startLineNumber, element1.symbol.range.endLineNumber);
const range2: StickyRange = new StickyRange(element2.symbol.range.startLineNumber, element2.symbol.range.endLineNumber);
return this.comparator(range1, range2);
});
for (const outlineElement of outlineElementsArray) {
stickyChildren.push(StickyElement.fromOutlineElement(outlineElement, outlineElement.symbol.selectionRange.startLineNumber));
}
const stickyOutlineElement = new StickyElement(undefined, stickyChildren, undefined);

return {
stickyOutlineElement: stickyOutlineElement,
providerID: preferredProvider
};
}

private static findSumOfRangesOfGroup(outline: OutlineGroup | OutlineElement): number {
let res = 0;
for (const child of outline.children.values()) {
res += this.findSumOfRangesOfGroup(child);
}
if (outline instanceof OutlineElement) {
return res + outline.symbol.range.endLineNumber - outline.symbol.selectionRange.startLineNumber;
} else {
return res;
}
}

public static fromFoldingRegions(foldingRegions: FoldingRegions): StickyElement {
const length = foldingRegions.length;
const orderedStickyElements: StickyElement[] = [];

// The root sticky outline element
const stickyOutlineElement = new StickyElement(
undefined,
[],
undefined
);

for (let i = 0; i < length; i++) {
// Finding the parent index of the current range
const parentIndex = foldingRegions.getParentIndex(i);

let parentNode;
if (parentIndex !== -1) {
// Access the reference of the parent node
parentNode = orderedStickyElements[parentIndex];
} else {
// In that case the parent node is the root node
parentNode = stickyOutlineElement;
}

const child = new StickyElement(
new StickyRange(foldingRegions.getStartLineNumber(i), foldingRegions.getEndLineNumber(i) + 1),
[],
parentNode
);
parentNode.children.push(child);
orderedStickyElements.push(child);
}
return stickyOutlineElement;
}

constructor(
/**
* Range of line numbers spanned by the current scope
*/
public readonly range: StickyRange | undefined,
/**
* Must be sorted by start line number
*/
public readonly children: StickyElement[],
/**
* Parent sticky outline element
*/
public readonly parent: StickyElement | undefined
) {
}
}

export class StickyModel {
constructor(
readonly uri: URI,
readonly version: number,
readonly element: StickyElement | undefined,
readonly outlineProviderId: string | undefined
) { }
}
Loading