Path: blob/main/src/vs/editor/contrib/gotoSymbol/browser/link/clickLinkGesture.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { IKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';6import { Emitter, Event } from '../../../../../base/common/event.js';7import { KeyCode } from '../../../../../base/common/keyCodes.js';8import { Disposable } from '../../../../../base/common/lifecycle.js';9import * as platform from '../../../../../base/common/platform.js';10import { ICodeEditor, IEditorMouseEvent, IMouseTarget } from '../../../../browser/editorBrowser.js';11import { EditorOption } from '../../../../common/config/editorOptions.js';12import { ICursorSelectionChangedEvent } from '../../../../common/cursorEvents.js';1314function hasModifier(e: { ctrlKey: boolean; shiftKey: boolean; altKey: boolean; metaKey: boolean }, modifier: 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey'): boolean {15return !!e[modifier];16}1718/**19* An event that encapsulates the various trigger modifiers logic needed for go to definition.20*/21export class ClickLinkMouseEvent {2223public readonly target: IMouseTarget;24public readonly hasTriggerModifier: boolean;25public readonly hasSideBySideModifier: boolean;26public readonly isNoneOrSingleMouseDown: boolean;27public readonly isLeftClick: boolean;28public readonly isMiddleClick: boolean;29public readonly isRightClick: boolean;3031constructor(source: IEditorMouseEvent, opts: ClickLinkOptions) {32this.target = source.target;33this.isLeftClick = source.event.leftButton;34this.isMiddleClick = source.event.middleButton;35this.isRightClick = source.event.rightButton;36this.hasTriggerModifier = hasModifier(source.event, opts.triggerModifier);37this.hasSideBySideModifier = hasModifier(source.event, opts.triggerSideBySideModifier);38this.isNoneOrSingleMouseDown = (source.event.detail <= 1);39}40}4142/**43* An event that encapsulates the various trigger modifiers logic needed for go to definition.44*/45export class ClickLinkKeyboardEvent {4647public readonly keyCodeIsTriggerKey: boolean;48public readonly keyCodeIsSideBySideKey: boolean;49public readonly hasTriggerModifier: boolean;5051constructor(source: IKeyboardEvent, opts: ClickLinkOptions) {52this.keyCodeIsTriggerKey = (source.keyCode === opts.triggerKey);53this.keyCodeIsSideBySideKey = (source.keyCode === opts.triggerSideBySideKey);54this.hasTriggerModifier = hasModifier(source, opts.triggerModifier);55}56}57export type TriggerModifier = 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey';5859export class ClickLinkOptions {6061public readonly triggerKey: KeyCode;62public readonly triggerModifier: TriggerModifier;63public readonly triggerSideBySideKey: KeyCode;64public readonly triggerSideBySideModifier: TriggerModifier;6566constructor(67triggerKey: KeyCode,68triggerModifier: TriggerModifier,69triggerSideBySideKey: KeyCode,70triggerSideBySideModifier: TriggerModifier71) {72this.triggerKey = triggerKey;73this.triggerModifier = triggerModifier;74this.triggerSideBySideKey = triggerSideBySideKey;75this.triggerSideBySideModifier = triggerSideBySideModifier;76}7778public equals(other: ClickLinkOptions): boolean {79return (80this.triggerKey === other.triggerKey81&& this.triggerModifier === other.triggerModifier82&& this.triggerSideBySideKey === other.triggerSideBySideKey83&& this.triggerSideBySideModifier === other.triggerSideBySideModifier84);85}86}8788function createOptions(multiCursorModifier: 'altKey' | 'ctrlKey' | 'metaKey'): ClickLinkOptions {89if (multiCursorModifier === 'altKey') {90if (platform.isMacintosh) {91return new ClickLinkOptions(KeyCode.Meta, 'metaKey', KeyCode.Alt, 'altKey');92}93return new ClickLinkOptions(KeyCode.Ctrl, 'ctrlKey', KeyCode.Alt, 'altKey');94}9596if (platform.isMacintosh) {97return new ClickLinkOptions(KeyCode.Alt, 'altKey', KeyCode.Meta, 'metaKey');98}99return new ClickLinkOptions(KeyCode.Alt, 'altKey', KeyCode.Ctrl, 'ctrlKey');100}101102export interface IClickLinkGestureOptions {103/**104* Return 0 if the mouse event should not be considered.105*/106extractLineNumberFromMouseEvent?: (e: ClickLinkMouseEvent) => number;107}108109export class ClickLinkGesture extends Disposable {110111private readonly _onMouseMoveOrRelevantKeyDown: Emitter<[ClickLinkMouseEvent, ClickLinkKeyboardEvent | null]> = this._register(new Emitter<[ClickLinkMouseEvent, ClickLinkKeyboardEvent | null]>());112public readonly onMouseMoveOrRelevantKeyDown: Event<[ClickLinkMouseEvent, ClickLinkKeyboardEvent | null]> = this._onMouseMoveOrRelevantKeyDown.event;113114private readonly _onExecute: Emitter<ClickLinkMouseEvent> = this._register(new Emitter<ClickLinkMouseEvent>());115public readonly onExecute: Event<ClickLinkMouseEvent> = this._onExecute.event;116117private readonly _onCancel: Emitter<void> = this._register(new Emitter<void>());118public readonly onCancel: Event<void> = this._onCancel.event;119120private readonly _editor: ICodeEditor;121private readonly _extractLineNumberFromMouseEvent: (e: ClickLinkMouseEvent) => number;122private _opts: ClickLinkOptions;123124private _lastMouseMoveEvent: ClickLinkMouseEvent | null;125private _hasTriggerKeyOnMouseDown: boolean;126private _lineNumberOnMouseDown: number;127128constructor(editor: ICodeEditor, opts?: IClickLinkGestureOptions) {129super();130131this._editor = editor;132this._extractLineNumberFromMouseEvent = opts?.extractLineNumberFromMouseEvent ?? ((e) => e.target.position ? e.target.position.lineNumber : 0);133this._opts = createOptions(this._editor.getOption(EditorOption.multiCursorModifier));134135this._lastMouseMoveEvent = null;136this._hasTriggerKeyOnMouseDown = false;137this._lineNumberOnMouseDown = 0;138139this._register(this._editor.onDidChangeConfiguration((e) => {140if (e.hasChanged(EditorOption.multiCursorModifier)) {141const newOpts = createOptions(this._editor.getOption(EditorOption.multiCursorModifier));142if (this._opts.equals(newOpts)) {143return;144}145this._opts = newOpts;146this._lastMouseMoveEvent = null;147this._hasTriggerKeyOnMouseDown = false;148this._lineNumberOnMouseDown = 0;149this._onCancel.fire();150}151}));152this._register(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(new ClickLinkMouseEvent(e, this._opts))));153this._register(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(new ClickLinkMouseEvent(e, this._opts))));154this._register(this._editor.onMouseUp((e: IEditorMouseEvent) => this._onEditorMouseUp(new ClickLinkMouseEvent(e, this._opts))));155this._register(this._editor.onKeyDown((e: IKeyboardEvent) => this._onEditorKeyDown(new ClickLinkKeyboardEvent(e, this._opts))));156this._register(this._editor.onKeyUp((e: IKeyboardEvent) => this._onEditorKeyUp(new ClickLinkKeyboardEvent(e, this._opts))));157this._register(this._editor.onMouseDrag(() => this._resetHandler()));158159this._register(this._editor.onDidChangeCursorSelection((e) => this._onDidChangeCursorSelection(e)));160this._register(this._editor.onDidChangeModel((e) => this._resetHandler()));161this._register(this._editor.onDidChangeModelContent(() => this._resetHandler()));162this._register(this._editor.onDidScrollChange((e) => {163if (e.scrollTopChanged || e.scrollLeftChanged) {164this._resetHandler();165}166}));167}168169private _onDidChangeCursorSelection(e: ICursorSelectionChangedEvent): void {170if (e.selection && e.selection.startColumn !== e.selection.endColumn) {171this._resetHandler(); // immediately stop this feature if the user starts to select (https://github.com/microsoft/vscode/issues/7827)172}173}174175private _onEditorMouseMove(mouseEvent: ClickLinkMouseEvent): void {176this._lastMouseMoveEvent = mouseEvent;177178this._onMouseMoveOrRelevantKeyDown.fire([mouseEvent, null]);179}180181private _onEditorMouseDown(mouseEvent: ClickLinkMouseEvent): void {182// We need to record if we had the trigger key on mouse down because someone might select something in the editor183// holding the mouse down and then while mouse is down start to press Ctrl/Cmd to start a copy operation and then184// release the mouse button without wanting to do the navigation.185// With this flag we prevent goto definition if the mouse was down before the trigger key was pressed.186this._hasTriggerKeyOnMouseDown = mouseEvent.hasTriggerModifier;187this._lineNumberOnMouseDown = this._extractLineNumberFromMouseEvent(mouseEvent);188}189190private _onEditorMouseUp(mouseEvent: ClickLinkMouseEvent): void {191const currentLineNumber = this._extractLineNumberFromMouseEvent(mouseEvent);192if (this._hasTriggerKeyOnMouseDown && this._lineNumberOnMouseDown && this._lineNumberOnMouseDown === currentLineNumber) {193this._onExecute.fire(mouseEvent);194}195}196197private _onEditorKeyDown(e: ClickLinkKeyboardEvent): void {198if (199this._lastMouseMoveEvent200&& (201e.keyCodeIsTriggerKey // User just pressed Ctrl/Cmd (normal goto definition)202|| (e.keyCodeIsSideBySideKey && e.hasTriggerModifier) // User pressed Ctrl/Cmd+Alt (goto definition to the side)203)204) {205this._onMouseMoveOrRelevantKeyDown.fire([this._lastMouseMoveEvent, e]);206} else if (e.hasTriggerModifier) {207this._onCancel.fire(); // remove decorations if user holds another key with ctrl/cmd to prevent accident goto declaration208}209}210211private _onEditorKeyUp(e: ClickLinkKeyboardEvent): void {212if (e.keyCodeIsTriggerKey) {213this._onCancel.fire();214}215}216217private _resetHandler(): void {218this._lastMouseMoveEvent = null;219this._hasTriggerKeyOnMouseDown = false;220this._onCancel.fire();221}222}223224225