Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/gotoSymbol/browser/link/clickLinkGesture.ts
5347 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { IKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';
7
import { Emitter, Event } from '../../../../../base/common/event.js';
8
import { KeyCode } from '../../../../../base/common/keyCodes.js';
9
import { Disposable } from '../../../../../base/common/lifecycle.js';
10
import * as platform from '../../../../../base/common/platform.js';
11
import { ICodeEditor, IEditorMouseEvent, IMouseTarget } from '../../../../browser/editorBrowser.js';
12
import { EditorOption, MouseMiddleClickAction } from '../../../../common/config/editorOptions.js';
13
import { ICursorSelectionChangedEvent } from '../../../../common/cursorEvents.js';
14
15
function hasModifier(e: { ctrlKey: boolean; shiftKey: boolean; altKey: boolean; metaKey: boolean }, modifier: 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey'): boolean {
16
return !!e[modifier];
17
}
18
19
/**
20
* An event that encapsulates the various trigger modifiers logic needed for go to definition.
21
*/
22
export class ClickLinkMouseEvent {
23
24
public readonly target: IMouseTarget;
25
public readonly hasTriggerModifier: boolean;
26
public readonly hasSideBySideModifier: boolean;
27
public readonly isNoneOrSingleMouseDown: boolean;
28
public readonly isLeftClick: boolean;
29
public readonly isMiddleClick: boolean;
30
public readonly isRightClick: boolean;
31
32
public readonly mouseMiddleClickAction: MouseMiddleClickAction;
33
34
constructor(source: IEditorMouseEvent, opts: ClickLinkOptions) {
35
this.target = source.target;
36
this.isLeftClick = source.event.leftButton;
37
this.isMiddleClick = source.event.middleButton;
38
this.isRightClick = source.event.rightButton;
39
this.mouseMiddleClickAction = opts.mouseMiddleClickAction;
40
this.hasTriggerModifier = hasModifier(source.event, opts.triggerModifier);
41
42
if (this.isMiddleClick && opts.mouseMiddleClickAction === 'ctrlLeftClick') {
43
// Redirect middle click to left click with modifier
44
this.isMiddleClick = false;
45
this.isLeftClick = true;
46
this.hasTriggerModifier = true;
47
}
48
this.hasSideBySideModifier = hasModifier(source.event, opts.triggerSideBySideModifier);
49
this.isNoneOrSingleMouseDown = (source.event.detail <= 1);
50
}
51
}
52
53
/**
54
* An event that encapsulates the various trigger modifiers logic needed for go to definition.
55
*/
56
export class ClickLinkKeyboardEvent {
57
58
public readonly keyCodeIsTriggerKey: boolean;
59
public readonly keyCodeIsSideBySideKey: boolean;
60
public readonly hasTriggerModifier: boolean;
61
62
constructor(source: IKeyboardEvent, opts: ClickLinkOptions) {
63
this.keyCodeIsTriggerKey = (source.keyCode === opts.triggerKey);
64
this.keyCodeIsSideBySideKey = (source.keyCode === opts.triggerSideBySideKey);
65
this.hasTriggerModifier = hasModifier(source, opts.triggerModifier);
66
}
67
}
68
export type TriggerModifier = 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey';
69
70
export class ClickLinkOptions {
71
72
public readonly triggerKey: KeyCode;
73
public readonly triggerModifier: TriggerModifier;
74
public readonly triggerSideBySideKey: KeyCode;
75
public readonly triggerSideBySideModifier: TriggerModifier;
76
77
constructor(
78
triggerKey: KeyCode,
79
triggerModifier: TriggerModifier,
80
triggerSideBySideKey: KeyCode,
81
triggerSideBySideModifier: TriggerModifier,
82
public readonly mouseMiddleClickAction: MouseMiddleClickAction,
83
) {
84
this.triggerKey = triggerKey;
85
this.triggerModifier = triggerModifier;
86
this.triggerSideBySideKey = triggerSideBySideKey;
87
this.triggerSideBySideModifier = triggerSideBySideModifier;
88
}
89
90
public equals(other: ClickLinkOptions): boolean {
91
return (
92
this.triggerKey === other.triggerKey
93
&& this.triggerModifier === other.triggerModifier
94
&& this.triggerSideBySideKey === other.triggerSideBySideKey
95
&& this.triggerSideBySideModifier === other.triggerSideBySideModifier
96
&& this.mouseMiddleClickAction === other.mouseMiddleClickAction
97
);
98
}
99
}
100
101
function createOptions(multiCursorModifier: 'altKey' | 'ctrlKey' | 'metaKey', mouseMiddleClickAction: MouseMiddleClickAction): ClickLinkOptions {
102
if (multiCursorModifier === 'altKey') {
103
if (platform.isMacintosh) {
104
return new ClickLinkOptions(KeyCode.Meta, 'metaKey', KeyCode.Alt, 'altKey', mouseMiddleClickAction);
105
}
106
return new ClickLinkOptions(KeyCode.Ctrl, 'ctrlKey', KeyCode.Alt, 'altKey', mouseMiddleClickAction);
107
}
108
109
if (platform.isMacintosh) {
110
return new ClickLinkOptions(KeyCode.Alt, 'altKey', KeyCode.Meta, 'metaKey', mouseMiddleClickAction);
111
}
112
return new ClickLinkOptions(KeyCode.Alt, 'altKey', KeyCode.Ctrl, 'ctrlKey', mouseMiddleClickAction);
113
}
114
115
export interface IClickLinkGestureOptions {
116
/**
117
* Return 0 if the mouse event should not be considered.
118
*/
119
extractLineNumberFromMouseEvent?: (e: ClickLinkMouseEvent) => number;
120
}
121
122
export class ClickLinkGesture extends Disposable {
123
124
private readonly _onMouseMoveOrRelevantKeyDown: Emitter<[ClickLinkMouseEvent, ClickLinkKeyboardEvent | null]> = this._register(new Emitter<[ClickLinkMouseEvent, ClickLinkKeyboardEvent | null]>());
125
public readonly onMouseMoveOrRelevantKeyDown: Event<[ClickLinkMouseEvent, ClickLinkKeyboardEvent | null]> = this._onMouseMoveOrRelevantKeyDown.event;
126
127
private readonly _onExecute: Emitter<ClickLinkMouseEvent> = this._register(new Emitter<ClickLinkMouseEvent>());
128
public readonly onExecute: Event<ClickLinkMouseEvent> = this._onExecute.event;
129
130
private readonly _onCancel: Emitter<void> = this._register(new Emitter<void>());
131
public readonly onCancel: Event<void> = this._onCancel.event;
132
133
private readonly _editor: ICodeEditor;
134
private readonly _extractLineNumberFromMouseEvent: (e: ClickLinkMouseEvent) => number;
135
private _opts: ClickLinkOptions;
136
137
private _lastMouseMoveEvent: ClickLinkMouseEvent | null;
138
private _hasTriggerKeyOnMouseDown: boolean;
139
private _lineNumberOnMouseDown: number;
140
141
constructor(editor: ICodeEditor, opts?: IClickLinkGestureOptions) {
142
super();
143
144
this._editor = editor;
145
this._extractLineNumberFromMouseEvent = opts?.extractLineNumberFromMouseEvent ?? ((e) => e.target.position ? e.target.position.lineNumber : 0);
146
this._opts = createOptions(this._editor.getOption(EditorOption.multiCursorModifier), this._editor.getOption(EditorOption.mouseMiddleClickAction));
147
148
this._lastMouseMoveEvent = null;
149
this._hasTriggerKeyOnMouseDown = false;
150
this._lineNumberOnMouseDown = 0;
151
152
this._register(this._editor.onDidChangeConfiguration((e) => {
153
if (e.hasChanged(EditorOption.multiCursorModifier) || e.hasChanged(EditorOption.mouseMiddleClickAction)) {
154
const newOpts = createOptions(this._editor.getOption(EditorOption.multiCursorModifier), this._editor.getOption(EditorOption.mouseMiddleClickAction));
155
if (this._opts.equals(newOpts)) {
156
return;
157
}
158
this._opts = newOpts;
159
this._lastMouseMoveEvent = null;
160
this._hasTriggerKeyOnMouseDown = false;
161
this._lineNumberOnMouseDown = 0;
162
this._onCancel.fire();
163
}
164
}));
165
this._register(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(new ClickLinkMouseEvent(e, this._opts))));
166
this._register(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(new ClickLinkMouseEvent(e, this._opts))));
167
this._register(this._editor.onMouseUp((e: IEditorMouseEvent) => this._onEditorMouseUp(new ClickLinkMouseEvent(e, this._opts))));
168
this._register(this._editor.onKeyDown((e: IKeyboardEvent) => this._onEditorKeyDown(new ClickLinkKeyboardEvent(e, this._opts))));
169
this._register(this._editor.onKeyUp((e: IKeyboardEvent) => this._onEditorKeyUp(new ClickLinkKeyboardEvent(e, this._opts))));
170
this._register(this._editor.onMouseDrag(() => this._resetHandler()));
171
172
this._register(this._editor.onDidChangeCursorSelection((e) => this._onDidChangeCursorSelection(e)));
173
this._register(this._editor.onDidChangeModel((e) => this._resetHandler()));
174
this._register(this._editor.onDidChangeModelContent(() => this._resetHandler()));
175
this._register(this._editor.onDidScrollChange((e) => {
176
if (e.scrollTopChanged || e.scrollLeftChanged) {
177
this._resetHandler();
178
}
179
}));
180
}
181
182
private _onDidChangeCursorSelection(e: ICursorSelectionChangedEvent): void {
183
if (e.selection && e.selection.startColumn !== e.selection.endColumn) {
184
this._resetHandler(); // immediately stop this feature if the user starts to select (https://github.com/microsoft/vscode/issues/7827)
185
}
186
}
187
188
private _onEditorMouseMove(mouseEvent: ClickLinkMouseEvent): void {
189
this._lastMouseMoveEvent = mouseEvent;
190
191
this._onMouseMoveOrRelevantKeyDown.fire([mouseEvent, null]);
192
}
193
194
private _onEditorMouseDown(mouseEvent: ClickLinkMouseEvent): void {
195
// We need to record if we had the trigger key on mouse down because someone might select something in the editor
196
// holding the mouse down and then while mouse is down start to press Ctrl/Cmd to start a copy operation and then
197
// release the mouse button without wanting to do the navigation.
198
// With this flag we prevent goto definition if the mouse was down before the trigger key was pressed.
199
this._hasTriggerKeyOnMouseDown = mouseEvent.hasTriggerModifier;
200
this._lineNumberOnMouseDown = this._extractLineNumberFromMouseEvent(mouseEvent);
201
}
202
203
private _onEditorMouseUp(mouseEvent: ClickLinkMouseEvent): void {
204
const currentLineNumber = this._extractLineNumberFromMouseEvent(mouseEvent);
205
const lineNumbersCorrect = !!this._lineNumberOnMouseDown && this._lineNumberOnMouseDown === currentLineNumber;
206
if (lineNumbersCorrect && (this._hasTriggerKeyOnMouseDown || (mouseEvent.isMiddleClick && mouseEvent.mouseMiddleClickAction === 'openLink'))) {
207
this._onExecute.fire(mouseEvent);
208
}
209
}
210
211
private _onEditorKeyDown(e: ClickLinkKeyboardEvent): void {
212
if (
213
this._lastMouseMoveEvent
214
&& (
215
e.keyCodeIsTriggerKey // User just pressed Ctrl/Cmd (normal goto definition)
216
|| (e.keyCodeIsSideBySideKey && e.hasTriggerModifier) // User pressed Ctrl/Cmd+Alt (goto definition to the side)
217
)
218
) {
219
this._onMouseMoveOrRelevantKeyDown.fire([this._lastMouseMoveEvent, e]);
220
} else if (e.hasTriggerModifier) {
221
this._onCancel.fire(); // remove decorations if user holds another key with ctrl/cmd to prevent accident goto declaration
222
}
223
}
224
225
private _onEditorKeyUp(e: ClickLinkKeyboardEvent): void {
226
if (e.keyCodeIsTriggerKey) {
227
this._onCancel.fire();
228
}
229
}
230
231
private _resetHandler(): void {
232
this._lastMouseMoveEvent = null;
233
this._hasTriggerKeyOnMouseDown = false;
234
this._onCancel.fire();
235
}
236
}
237
238