Skip to content

Commit 650669a

Browse files
author
Aiday Marlen Kyzy
authored
Merge pull request microsoft#158559 from microsoft/aiday/issue157055
Allowing to control/command click on sticky scroll
2 parents 4bf364d + 7afe4e7 commit 650669a

File tree

3 files changed

+142
-29
lines changed

3 files changed

+142
-29
lines changed

src/vs/editor/contrib/gotoSymbol/browser/link/clickLinkGesture.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,16 +106,18 @@ export class ClickLinkGesture extends Disposable {
106106
public readonly onCancel: Event<void> = this._onCancel.event;
107107

108108
private readonly _editor: ICodeEditor;
109+
private readonly _alwaysFireExecuteOnMouseUp?: boolean;
109110
private _opts: ClickLinkOptions;
110111

111112
private _lastMouseMoveEvent: ClickLinkMouseEvent | null;
112113
private _hasTriggerKeyOnMouseDown: boolean;
113114
private _lineNumberOnMouseDown: number;
114115

115-
constructor(editor: ICodeEditor) {
116+
constructor(editor: ICodeEditor, alwaysFireOnMouseUp?: boolean) {
116117
super();
117118

118119
this._editor = editor;
120+
this._alwaysFireExecuteOnMouseUp = alwaysFireOnMouseUp;
119121
this._opts = createOptions(this._editor.getOption(EditorOption.multiCursorModifier));
120122

121123
this._lastMouseMoveEvent = null;
@@ -175,7 +177,7 @@ export class ClickLinkGesture extends Disposable {
175177

176178
private _onEditorMouseUp(mouseEvent: ClickLinkMouseEvent): void {
177179
const currentLineNumber = mouseEvent.target.position ? mouseEvent.target.position.lineNumber : 0;
178-
if (this._hasTriggerKeyOnMouseDown && this._lineNumberOnMouseDown && this._lineNumberOnMouseDown === currentLineNumber) {
180+
if (this._hasTriggerKeyOnMouseDown && this._lineNumberOnMouseDown && this._lineNumberOnMouseDown === currentLineNumber || this._alwaysFireExecuteOnMouseUp) {
179181
this._onExecute.fire(mouseEvent);
180182
}
181183
}

src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/edi
1111
import { StickyScrollWidget, StickyScrollWidgetState } from './stickyScrollWidget';
1212
import { StickyLineCandidateProvider, StickyRange } from './stickyScrollProvider';
1313
import { IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents';
14+
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1415

1516
export class StickyScrollController extends Disposable implements IEditorContribution {
1617

@@ -24,10 +25,11 @@ export class StickyScrollController extends Disposable implements IEditorContrib
2425
constructor(
2526
_editor: ICodeEditor,
2627
@ILanguageFeaturesService _languageFeaturesService: ILanguageFeaturesService,
28+
@IInstantiationService _instaService: IInstantiationService,
2729
) {
2830
super();
2931
this._editor = _editor;
30-
this._stickyScrollWidget = new StickyScrollWidget(this._editor);
32+
this._stickyScrollWidget = new StickyScrollWidget(this._editor, _languageFeaturesService, _instaService);
3133
this._stickyLineCandidateProvider = new StickyLineCandidateProvider(this._editor, _languageFeaturesService);
3234
this._widgetState = new StickyScrollWidgetState([], 0);
3335

src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts

Lines changed: 135 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,30 @@
22
* Copyright (c) Microsoft Corporation. All rights reserved.
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
5-
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
6-
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser';
5+
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
6+
import { IActiveCodeEditor, ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser';
77
import * as dom from 'vs/base/browser/dom';
88
import { EditorLayoutInfo, EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions';
99
import { createStringBuilder } from 'vs/editor/common/core/stringBuilder';
1010
import { RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer';
1111
import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations';
1212
import { Position } from 'vs/editor/common/core/position';
13+
import { ClickLinkGesture } from 'vs/editor/contrib/gotoSymbol/browser/link/clickLinkGesture';
14+
import { getDefinitionsAtPosition } from 'vs/editor/contrib/gotoSymbol/browser/goToSymbol';
15+
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
16+
import { Location } from 'vs/editor/common/languages';
17+
import { goToDefinitionWithLocation } from 'vs/editor/contrib/inlayHints/browser/inlayHintsLocations';
18+
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
19+
import { CancellationTokenSource } from 'vs/base/common/cancellation';
20+
import { IRange, Range } from 'vs/editor/common/core/range';
21+
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
1322
import 'vs/css!./stickyScroll';
1423

24+
interface CustomMouseEvent {
25+
detail: string;
26+
element: HTMLElement;
27+
}
28+
1529
export class StickyScrollWidgetState {
1630
constructor(
1731
public readonly lineNumbers: number[],
@@ -29,24 +43,115 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget {
2943
private _lineHeight: number;
3044
private _lineNumbers: number[];
3145
private _lastLineRelativePosition: number;
46+
private _hoverOnLine: number;
47+
private _hoverOnColumn: number;
48+
private _stickyRangeProjectedOnEditor: IRange | null;
3249

33-
constructor(private readonly editor: ICodeEditor) {
50+
constructor(
51+
private readonly _editor: ICodeEditor,
52+
@ILanguageFeaturesService private readonly _languageFeatureService: ILanguageFeaturesService,
53+
@IInstantiationService private readonly _instaService: IInstantiationService
54+
) {
3455
super();
35-
this._layoutInfo = this.editor.getLayoutInfo();
56+
this._layoutInfo = this._editor.getLayoutInfo();
3657
this._rootDomNode = document.createElement('div');
3758
this._rootDomNode.className = 'sticky-widget';
3859
this._rootDomNode.style.width = `${this._layoutInfo.width - this._layoutInfo.minimap.minimapCanvasOuterWidth - this._layoutInfo.verticalScrollbarWidth}px`;
39-
4060
this._lineNumbers = [];
4161
this._lastLineRelativePosition = 0;
42-
43-
this._lineHeight = this.editor.getOption(EditorOption.lineHeight);
44-
this._register(this.editor.onDidChangeConfiguration(e => {
62+
this._hoverOnLine = -1;
63+
this._hoverOnColumn = -1;
64+
this._stickyRangeProjectedOnEditor = null;
65+
this._lineHeight = this._editor.getOption(EditorOption.lineHeight);
66+
this._register(this._editor.onDidChangeConfiguration(e => {
4567
if (e.hasChanged(EditorOption.lineHeight)) {
46-
this._lineHeight = this.editor.getOption(EditorOption.lineHeight);
68+
this._lineHeight = this._editor.getOption(EditorOption.lineHeight);
69+
}
70+
71+
}));
72+
this._register(this.updateLinkGesture());
73+
}
74+
75+
private updateLinkGesture(): IDisposable {
76+
77+
78+
const linkGestureStore = new DisposableStore();
79+
const sessionStore = new DisposableStore();
80+
linkGestureStore.add(sessionStore);
81+
const gesture = new ClickLinkGesture(this._editor, true);
82+
linkGestureStore.add(gesture);
83+
84+
linkGestureStore.add(gesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, _keyboardEvent]) => {
85+
if (!this._editor.hasModel() || !mouseEvent.hasTriggerModifier) {
86+
sessionStore.clear();
87+
return;
4788
}
89+
const targetMouseEvent = mouseEvent.target as unknown as CustomMouseEvent;
90+
if (targetMouseEvent.detail === this.getId() && targetMouseEvent.element.innerText === targetMouseEvent.element.innerHTML) {
91+
const text = targetMouseEvent.element.innerText;
92+
if (this._hoverOnColumn === -1) {
93+
return;
94+
}
95+
const lineNumber = this._hoverOnLine;
96+
const column = this._hoverOnColumn;
97+
98+
const stickyPositionProjectedOnEditor = new Range(lineNumber, column, lineNumber, column + text.length);
99+
if (!stickyPositionProjectedOnEditor.equalsRange(this._stickyRangeProjectedOnEditor)) {
100+
this._stickyRangeProjectedOnEditor = stickyPositionProjectedOnEditor;
101+
sessionStore.clear();
102+
} else if (targetMouseEvent.element.style.textDecoration === 'underline') {
103+
return;
104+
}
105+
106+
const cancellationToken = new CancellationTokenSource();
107+
sessionStore.add(toDisposable(() => cancellationToken.dispose(true)));
108+
109+
let currentHTMLChild: HTMLElement;
110+
111+
getDefinitionsAtPosition(this._languageFeatureService.definitionProvider, this._editor.getModel(), new Position(lineNumber, column + 1), cancellationToken.token).then((candidateDefinitions => {
112+
if (cancellationToken.token.isCancellationRequested) {
113+
return;
114+
}
115+
if (candidateDefinitions.length !== 0) {
116+
const childHTML: HTMLElement = targetMouseEvent.element;
117+
if (currentHTMLChild !== childHTML) {
118+
sessionStore.clear();
119+
currentHTMLChild = childHTML;
120+
currentHTMLChild.style.textDecoration = 'underline';
121+
sessionStore.add(toDisposable(() => {
122+
currentHTMLChild.style.textDecoration = 'none';
123+
}));
124+
} else if (!currentHTMLChild) {
125+
currentHTMLChild = childHTML;
126+
currentHTMLChild.style.textDecoration = 'underline';
127+
sessionStore.add(toDisposable(() => {
128+
currentHTMLChild.style.textDecoration = 'none';
129+
}));
130+
}
131+
} else {
132+
sessionStore.clear();
133+
}
134+
}));
135+
} else {
136+
sessionStore.clear();
137+
}
138+
}));
139+
linkGestureStore.add(gesture.onCancel(() => {
140+
sessionStore.clear();
48141
}));
142+
linkGestureStore.add(gesture.onExecute(async e => {
143+
if (this._hoverOnLine !== -1) {
144+
if (e.hasTriggerModifier) {
145+
// Control click
146+
this._instaService.invokeFunction(goToDefinitionWithLocation, e, this._editor as IActiveCodeEditor, { uri: this._editor.getModel()!.uri, range: this._stickyRangeProjectedOnEditor } as Location);
147+
} else {
148+
// Normal click
149+
this._editor.revealPosition({ lineNumber: this._hoverOnLine, column: 1 });
150+
}
151+
}
49152

153+
}));
154+
return linkGestureStore;
50155
}
51156

52157

@@ -75,14 +180,14 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget {
75180
private getChildNode(index: number, line: number): HTMLElement {
76181

77182
const child = document.createElement('div');
78-
const viewModel = this.editor._getViewModel();
183+
const viewModel = this._editor._getViewModel();
79184
const viewLineNumber = viewModel!.coordinatesConverter.convertModelPositionToViewPosition(new Position(line, 1)).lineNumber;
80185
const lineRenderingData = viewModel!.getViewLineRenderingData(viewLineNumber);
81-
const layoutInfo = this.editor.getLayoutInfo();
186+
const layoutInfo = this._editor.getLayoutInfo();
82187
const width = layoutInfo.width - layoutInfo.minimap.minimapCanvasOuterWidth - layoutInfo.verticalScrollbarWidth;
83-
const minimapSide = this.editor.getOption(EditorOption.minimap).side;
84-
const lineHeight = this.editor.getOption(EditorOption.lineHeight);
85-
const lineNumberOption = this.editor.getOption(EditorOption.lineNumbers);
188+
const minimapSide = this._editor.getOption(EditorOption.minimap).side;
189+
const lineHeight = this._editor.getOption(EditorOption.lineHeight);
190+
const lineNumberOption = this._editor.getOption(EditorOption.lineNumbers);
86191

87192
let actualInlineDecorations: LineDecoration[];
88193
try {
@@ -112,6 +217,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget {
112217

113218
const lineHTMLNode = document.createElement('span');
114219
lineHTMLNode.className = 'sticky-line';
220+
lineHTMLNode.classList.add(`stickyLine${line}`);
115221
lineHTMLNode.style.lineHeight = `${lineHeight}px`;
116222
lineHTMLNode.innerHTML = newLine as string;
117223

@@ -128,7 +234,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget {
128234
if (lineNumberOption.renderType === RenderLineNumbersType.On || lineNumberOption.renderType === RenderLineNumbersType.Interval && line % 10 === 0) {
129235
innerLineNumberHTML.innerText = line.toString();
130236
} else if (lineNumberOption.renderType === RenderLineNumbersType.Relative) {
131-
innerLineNumberHTML.innerText = Math.abs(line - this.editor.getPosition()!.lineNumber).toString();
237+
innerLineNumberHTML.innerText = Math.abs(line - this._editor.getPosition()!.lineNumber).toString();
132238
}
133239
innerLineNumberHTML.className = 'sticky-line-number';
134240
innerLineNumberHTML.style.lineHeight = `${lineHeight}px`;
@@ -140,8 +246,8 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget {
140246
}
141247
lineNumberHTMLNode.appendChild(innerLineNumberHTML);
142248

143-
this.editor.applyFontInfo(lineHTMLNode);
144-
this.editor.applyFontInfo(innerLineNumberHTML);
249+
this._editor.applyFontInfo(lineHTMLNode);
250+
this._editor.applyFontInfo(innerLineNumberHTML);
145251

146252
child.appendChild(lineNumberHTMLNode);
147253
child.appendChild(lineHTMLNode);
@@ -158,18 +264,22 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget {
158264
child.style.zIndex = '-1';
159265
child.style.top = this._lastLineRelativePosition + 'px';
160266
}
161-
this._disposableStore.add(dom.addDisposableListener(child, 'click', e => {
162-
e.stopPropagation();
163-
e.preventDefault();
164-
this.editor.revealPosition({ lineNumber: line - index, column: 1 });
267+
268+
this._disposableStore.add(dom.addDisposableListener(child, 'mouseover', (e) => {
269+
if (this._editor.hasModel()) {
270+
const mouseOverEvent = new StandardMouseEvent(e);
271+
const text = mouseOverEvent.target.innerText;
272+
this._hoverOnLine = line;
273+
// TODO: workaround to find the column index, perhaps need more solid solution
274+
this._hoverOnColumn = this._editor.getModel().getLineContent(line).indexOf(text) + 1 || -1;
275+
}
165276
}));
166277

167278
return child;
168279
}
169280

170281
private renderRootNode(): void {
171-
172-
if (!this.editor._getViewModel()) {
282+
if (!this._editor._getViewModel()) {
173283
return;
174284
}
175285
for (const [index, line] of this._lineNumbers.entries()) {
@@ -178,9 +288,9 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget {
178288

179289
const widgetHeight: number = this._lineNumbers.length * this._lineHeight + this._lastLineRelativePosition;
180290
this._rootDomNode.style.height = widgetHeight.toString() + 'px';
181-
const minimapSide = this.editor.getOption(EditorOption.minimap).side;
291+
const minimapSide = this._editor.getOption(EditorOption.minimap).side;
182292
if (minimapSide === 'left') {
183-
this._rootDomNode.style.marginLeft = this.editor.getLayoutInfo().minimap.minimapCanvasOuterWidth + 'px';
293+
this._rootDomNode.style.marginLeft = this._editor.getLayoutInfo().minimap.minimapCanvasOuterWidth + 'px';
184294
} else if (minimapSide === 'right') {
185295
this._rootDomNode.style.marginLeft = '0px';
186296
}
@@ -203,6 +313,5 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget {
203313

204314
override dispose(): void {
205315
super.dispose();
206-
this._disposableStore.dispose();
207316
}
208317
}

0 commit comments

Comments
 (0)