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
3296 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 } 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
constructor(source: IEditorMouseEvent, opts: ClickLinkOptions) {
33
this.target = source.target;
34
this.isLeftClick = source.event.leftButton;
35
this.isMiddleClick = source.event.middleButton;
36
this.isRightClick = source.event.rightButton;
37
this.hasTriggerModifier = hasModifier(source.event, opts.triggerModifier);
38
this.hasSideBySideModifier = hasModifier(source.event, opts.triggerSideBySideModifier);
39
this.isNoneOrSingleMouseDown = (source.event.detail <= 1);
40
}
41
}
42
43
/**
44
* An event that encapsulates the various trigger modifiers logic needed for go to definition.
45
*/
46
export class ClickLinkKeyboardEvent {
47
48
public readonly keyCodeIsTriggerKey: boolean;
49
public readonly keyCodeIsSideBySideKey: boolean;
50
public readonly hasTriggerModifier: boolean;
51
52
constructor(source: IKeyboardEvent, opts: ClickLinkOptions) {
53
this.keyCodeIsTriggerKey = (source.keyCode === opts.triggerKey);
54
this.keyCodeIsSideBySideKey = (source.keyCode === opts.triggerSideBySideKey);
55
this.hasTriggerModifier = hasModifier(source, opts.triggerModifier);
56
}
57
}
58
export type TriggerModifier = 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey';
59
60
export class ClickLinkOptions {
61
62
public readonly triggerKey: KeyCode;
63
public readonly triggerModifier: TriggerModifier;
64
public readonly triggerSideBySideKey: KeyCode;
65
public readonly triggerSideBySideModifier: TriggerModifier;
66
67
constructor(
68
triggerKey: KeyCode,
69
triggerModifier: TriggerModifier,
70
triggerSideBySideKey: KeyCode,
71
triggerSideBySideModifier: TriggerModifier
72
) {
73
this.triggerKey = triggerKey;
74
this.triggerModifier = triggerModifier;
75
this.triggerSideBySideKey = triggerSideBySideKey;
76
this.triggerSideBySideModifier = triggerSideBySideModifier;
77
}
78
79
public equals(other: ClickLinkOptions): boolean {
80
return (
81
this.triggerKey === other.triggerKey
82
&& this.triggerModifier === other.triggerModifier
83
&& this.triggerSideBySideKey === other.triggerSideBySideKey
84
&& this.triggerSideBySideModifier === other.triggerSideBySideModifier
85
);
86
}
87
}
88
89
function createOptions(multiCursorModifier: 'altKey' | 'ctrlKey' | 'metaKey'): ClickLinkOptions {
90
if (multiCursorModifier === 'altKey') {
91
if (platform.isMacintosh) {
92
return new ClickLinkOptions(KeyCode.Meta, 'metaKey', KeyCode.Alt, 'altKey');
93
}
94
return new ClickLinkOptions(KeyCode.Ctrl, 'ctrlKey', KeyCode.Alt, 'altKey');
95
}
96
97
if (platform.isMacintosh) {
98
return new ClickLinkOptions(KeyCode.Alt, 'altKey', KeyCode.Meta, 'metaKey');
99
}
100
return new ClickLinkOptions(KeyCode.Alt, 'altKey', KeyCode.Ctrl, 'ctrlKey');
101
}
102
103
export interface IClickLinkGestureOptions {
104
/**
105
* Return 0 if the mouse event should not be considered.
106
*/
107
extractLineNumberFromMouseEvent?: (e: ClickLinkMouseEvent) => number;
108
}
109
110
export class ClickLinkGesture extends Disposable {
111
112
private readonly _onMouseMoveOrRelevantKeyDown: Emitter<[ClickLinkMouseEvent, ClickLinkKeyboardEvent | null]> = this._register(new Emitter<[ClickLinkMouseEvent, ClickLinkKeyboardEvent | null]>());
113
public readonly onMouseMoveOrRelevantKeyDown: Event<[ClickLinkMouseEvent, ClickLinkKeyboardEvent | null]> = this._onMouseMoveOrRelevantKeyDown.event;
114
115
private readonly _onExecute: Emitter<ClickLinkMouseEvent> = this._register(new Emitter<ClickLinkMouseEvent>());
116
public readonly onExecute: Event<ClickLinkMouseEvent> = this._onExecute.event;
117
118
private readonly _onCancel: Emitter<void> = this._register(new Emitter<void>());
119
public readonly onCancel: Event<void> = this._onCancel.event;
120
121
private readonly _editor: ICodeEditor;
122
private readonly _extractLineNumberFromMouseEvent: (e: ClickLinkMouseEvent) => number;
123
private _opts: ClickLinkOptions;
124
125
private _lastMouseMoveEvent: ClickLinkMouseEvent | null;
126
private _hasTriggerKeyOnMouseDown: boolean;
127
private _lineNumberOnMouseDown: number;
128
129
constructor(editor: ICodeEditor, opts?: IClickLinkGestureOptions) {
130
super();
131
132
this._editor = editor;
133
this._extractLineNumberFromMouseEvent = opts?.extractLineNumberFromMouseEvent ?? ((e) => e.target.position ? e.target.position.lineNumber : 0);
134
this._opts = createOptions(this._editor.getOption(EditorOption.multiCursorModifier));
135
136
this._lastMouseMoveEvent = null;
137
this._hasTriggerKeyOnMouseDown = false;
138
this._lineNumberOnMouseDown = 0;
139
140
this._register(this._editor.onDidChangeConfiguration((e) => {
141
if (e.hasChanged(EditorOption.multiCursorModifier)) {
142
const newOpts = createOptions(this._editor.getOption(EditorOption.multiCursorModifier));
143
if (this._opts.equals(newOpts)) {
144
return;
145
}
146
this._opts = newOpts;
147
this._lastMouseMoveEvent = null;
148
this._hasTriggerKeyOnMouseDown = false;
149
this._lineNumberOnMouseDown = 0;
150
this._onCancel.fire();
151
}
152
}));
153
this._register(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(new ClickLinkMouseEvent(e, this._opts))));
154
this._register(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(new ClickLinkMouseEvent(e, this._opts))));
155
this._register(this._editor.onMouseUp((e: IEditorMouseEvent) => this._onEditorMouseUp(new ClickLinkMouseEvent(e, this._opts))));
156
this._register(this._editor.onKeyDown((e: IKeyboardEvent) => this._onEditorKeyDown(new ClickLinkKeyboardEvent(e, this._opts))));
157
this._register(this._editor.onKeyUp((e: IKeyboardEvent) => this._onEditorKeyUp(new ClickLinkKeyboardEvent(e, this._opts))));
158
this._register(this._editor.onMouseDrag(() => this._resetHandler()));
159
160
this._register(this._editor.onDidChangeCursorSelection((e) => this._onDidChangeCursorSelection(e)));
161
this._register(this._editor.onDidChangeModel((e) => this._resetHandler()));
162
this._register(this._editor.onDidChangeModelContent(() => this._resetHandler()));
163
this._register(this._editor.onDidScrollChange((e) => {
164
if (e.scrollTopChanged || e.scrollLeftChanged) {
165
this._resetHandler();
166
}
167
}));
168
}
169
170
private _onDidChangeCursorSelection(e: ICursorSelectionChangedEvent): void {
171
if (e.selection && e.selection.startColumn !== e.selection.endColumn) {
172
this._resetHandler(); // immediately stop this feature if the user starts to select (https://github.com/microsoft/vscode/issues/7827)
173
}
174
}
175
176
private _onEditorMouseMove(mouseEvent: ClickLinkMouseEvent): void {
177
this._lastMouseMoveEvent = mouseEvent;
178
179
this._onMouseMoveOrRelevantKeyDown.fire([mouseEvent, null]);
180
}
181
182
private _onEditorMouseDown(mouseEvent: ClickLinkMouseEvent): void {
183
// We need to record if we had the trigger key on mouse down because someone might select something in the editor
184
// holding the mouse down and then while mouse is down start to press Ctrl/Cmd to start a copy operation and then
185
// release the mouse button without wanting to do the navigation.
186
// With this flag we prevent goto definition if the mouse was down before the trigger key was pressed.
187
this._hasTriggerKeyOnMouseDown = mouseEvent.hasTriggerModifier;
188
this._lineNumberOnMouseDown = this._extractLineNumberFromMouseEvent(mouseEvent);
189
}
190
191
private _onEditorMouseUp(mouseEvent: ClickLinkMouseEvent): void {
192
const currentLineNumber = this._extractLineNumberFromMouseEvent(mouseEvent);
193
if (this._hasTriggerKeyOnMouseDown && this._lineNumberOnMouseDown && this._lineNumberOnMouseDown === currentLineNumber) {
194
this._onExecute.fire(mouseEvent);
195
}
196
}
197
198
private _onEditorKeyDown(e: ClickLinkKeyboardEvent): void {
199
if (
200
this._lastMouseMoveEvent
201
&& (
202
e.keyCodeIsTriggerKey // User just pressed Ctrl/Cmd (normal goto definition)
203
|| (e.keyCodeIsSideBySideKey && e.hasTriggerModifier) // User pressed Ctrl/Cmd+Alt (goto definition to the side)
204
)
205
) {
206
this._onMouseMoveOrRelevantKeyDown.fire([this._lastMouseMoveEvent, e]);
207
} else if (e.hasTriggerModifier) {
208
this._onCancel.fire(); // remove decorations if user holds another key with ctrl/cmd to prevent accident goto declaration
209
}
210
}
211
212
private _onEditorKeyUp(e: ClickLinkKeyboardEvent): void {
213
if (e.keyCodeIsTriggerKey) {
214
this._onCancel.fire();
215
}
216
}
217
218
private _resetHandler(): void {
219
this._lastMouseMoveEvent = null;
220
this._hasTriggerKeyOnMouseDown = false;
221
this._onCancel.fire();
222
}
223
}
224
225