Path: blob/main/src/vs/editor/contrib/gotoSymbol/browser/link/clickLinkGesture.ts
5347 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, MouseMiddleClickAction } 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;3031public readonly mouseMiddleClickAction: MouseMiddleClickAction;3233constructor(source: IEditorMouseEvent, opts: ClickLinkOptions) {34this.target = source.target;35this.isLeftClick = source.event.leftButton;36this.isMiddleClick = source.event.middleButton;37this.isRightClick = source.event.rightButton;38this.mouseMiddleClickAction = opts.mouseMiddleClickAction;39this.hasTriggerModifier = hasModifier(source.event, opts.triggerModifier);4041if (this.isMiddleClick && opts.mouseMiddleClickAction === 'ctrlLeftClick') {42// Redirect middle click to left click with modifier43this.isMiddleClick = false;44this.isLeftClick = true;45this.hasTriggerModifier = true;46}47this.hasSideBySideModifier = hasModifier(source.event, opts.triggerSideBySideModifier);48this.isNoneOrSingleMouseDown = (source.event.detail <= 1);49}50}5152/**53* An event that encapsulates the various trigger modifiers logic needed for go to definition.54*/55export class ClickLinkKeyboardEvent {5657public readonly keyCodeIsTriggerKey: boolean;58public readonly keyCodeIsSideBySideKey: boolean;59public readonly hasTriggerModifier: boolean;6061constructor(source: IKeyboardEvent, opts: ClickLinkOptions) {62this.keyCodeIsTriggerKey = (source.keyCode === opts.triggerKey);63this.keyCodeIsSideBySideKey = (source.keyCode === opts.triggerSideBySideKey);64this.hasTriggerModifier = hasModifier(source, opts.triggerModifier);65}66}67export type TriggerModifier = 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey';6869export class ClickLinkOptions {7071public readonly triggerKey: KeyCode;72public readonly triggerModifier: TriggerModifier;73public readonly triggerSideBySideKey: KeyCode;74public readonly triggerSideBySideModifier: TriggerModifier;7576constructor(77triggerKey: KeyCode,78triggerModifier: TriggerModifier,79triggerSideBySideKey: KeyCode,80triggerSideBySideModifier: TriggerModifier,81public readonly mouseMiddleClickAction: MouseMiddleClickAction,82) {83this.triggerKey = triggerKey;84this.triggerModifier = triggerModifier;85this.triggerSideBySideKey = triggerSideBySideKey;86this.triggerSideBySideModifier = triggerSideBySideModifier;87}8889public equals(other: ClickLinkOptions): boolean {90return (91this.triggerKey === other.triggerKey92&& this.triggerModifier === other.triggerModifier93&& this.triggerSideBySideKey === other.triggerSideBySideKey94&& this.triggerSideBySideModifier === other.triggerSideBySideModifier95&& this.mouseMiddleClickAction === other.mouseMiddleClickAction96);97}98}99100function createOptions(multiCursorModifier: 'altKey' | 'ctrlKey' | 'metaKey', mouseMiddleClickAction: MouseMiddleClickAction): ClickLinkOptions {101if (multiCursorModifier === 'altKey') {102if (platform.isMacintosh) {103return new ClickLinkOptions(KeyCode.Meta, 'metaKey', KeyCode.Alt, 'altKey', mouseMiddleClickAction);104}105return new ClickLinkOptions(KeyCode.Ctrl, 'ctrlKey', KeyCode.Alt, 'altKey', mouseMiddleClickAction);106}107108if (platform.isMacintosh) {109return new ClickLinkOptions(KeyCode.Alt, 'altKey', KeyCode.Meta, 'metaKey', mouseMiddleClickAction);110}111return new ClickLinkOptions(KeyCode.Alt, 'altKey', KeyCode.Ctrl, 'ctrlKey', mouseMiddleClickAction);112}113114export interface IClickLinkGestureOptions {115/**116* Return 0 if the mouse event should not be considered.117*/118extractLineNumberFromMouseEvent?: (e: ClickLinkMouseEvent) => number;119}120121export class ClickLinkGesture extends Disposable {122123private readonly _onMouseMoveOrRelevantKeyDown: Emitter<[ClickLinkMouseEvent, ClickLinkKeyboardEvent | null]> = this._register(new Emitter<[ClickLinkMouseEvent, ClickLinkKeyboardEvent | null]>());124public readonly onMouseMoveOrRelevantKeyDown: Event<[ClickLinkMouseEvent, ClickLinkKeyboardEvent | null]> = this._onMouseMoveOrRelevantKeyDown.event;125126private readonly _onExecute: Emitter<ClickLinkMouseEvent> = this._register(new Emitter<ClickLinkMouseEvent>());127public readonly onExecute: Event<ClickLinkMouseEvent> = this._onExecute.event;128129private readonly _onCancel: Emitter<void> = this._register(new Emitter<void>());130public readonly onCancel: Event<void> = this._onCancel.event;131132private readonly _editor: ICodeEditor;133private readonly _extractLineNumberFromMouseEvent: (e: ClickLinkMouseEvent) => number;134private _opts: ClickLinkOptions;135136private _lastMouseMoveEvent: ClickLinkMouseEvent | null;137private _hasTriggerKeyOnMouseDown: boolean;138private _lineNumberOnMouseDown: number;139140constructor(editor: ICodeEditor, opts?: IClickLinkGestureOptions) {141super();142143this._editor = editor;144this._extractLineNumberFromMouseEvent = opts?.extractLineNumberFromMouseEvent ?? ((e) => e.target.position ? e.target.position.lineNumber : 0);145this._opts = createOptions(this._editor.getOption(EditorOption.multiCursorModifier), this._editor.getOption(EditorOption.mouseMiddleClickAction));146147this._lastMouseMoveEvent = null;148this._hasTriggerKeyOnMouseDown = false;149this._lineNumberOnMouseDown = 0;150151this._register(this._editor.onDidChangeConfiguration((e) => {152if (e.hasChanged(EditorOption.multiCursorModifier) || e.hasChanged(EditorOption.mouseMiddleClickAction)) {153const newOpts = createOptions(this._editor.getOption(EditorOption.multiCursorModifier), this._editor.getOption(EditorOption.mouseMiddleClickAction));154if (this._opts.equals(newOpts)) {155return;156}157this._opts = newOpts;158this._lastMouseMoveEvent = null;159this._hasTriggerKeyOnMouseDown = false;160this._lineNumberOnMouseDown = 0;161this._onCancel.fire();162}163}));164this._register(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(new ClickLinkMouseEvent(e, this._opts))));165this._register(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(new ClickLinkMouseEvent(e, this._opts))));166this._register(this._editor.onMouseUp((e: IEditorMouseEvent) => this._onEditorMouseUp(new ClickLinkMouseEvent(e, this._opts))));167this._register(this._editor.onKeyDown((e: IKeyboardEvent) => this._onEditorKeyDown(new ClickLinkKeyboardEvent(e, this._opts))));168this._register(this._editor.onKeyUp((e: IKeyboardEvent) => this._onEditorKeyUp(new ClickLinkKeyboardEvent(e, this._opts))));169this._register(this._editor.onMouseDrag(() => this._resetHandler()));170171this._register(this._editor.onDidChangeCursorSelection((e) => this._onDidChangeCursorSelection(e)));172this._register(this._editor.onDidChangeModel((e) => this._resetHandler()));173this._register(this._editor.onDidChangeModelContent(() => this._resetHandler()));174this._register(this._editor.onDidScrollChange((e) => {175if (e.scrollTopChanged || e.scrollLeftChanged) {176this._resetHandler();177}178}));179}180181private _onDidChangeCursorSelection(e: ICursorSelectionChangedEvent): void {182if (e.selection && e.selection.startColumn !== e.selection.endColumn) {183this._resetHandler(); // immediately stop this feature if the user starts to select (https://github.com/microsoft/vscode/issues/7827)184}185}186187private _onEditorMouseMove(mouseEvent: ClickLinkMouseEvent): void {188this._lastMouseMoveEvent = mouseEvent;189190this._onMouseMoveOrRelevantKeyDown.fire([mouseEvent, null]);191}192193private _onEditorMouseDown(mouseEvent: ClickLinkMouseEvent): void {194// We need to record if we had the trigger key on mouse down because someone might select something in the editor195// holding the mouse down and then while mouse is down start to press Ctrl/Cmd to start a copy operation and then196// release the mouse button without wanting to do the navigation.197// With this flag we prevent goto definition if the mouse was down before the trigger key was pressed.198this._hasTriggerKeyOnMouseDown = mouseEvent.hasTriggerModifier;199this._lineNumberOnMouseDown = this._extractLineNumberFromMouseEvent(mouseEvent);200}201202private _onEditorMouseUp(mouseEvent: ClickLinkMouseEvent): void {203const currentLineNumber = this._extractLineNumberFromMouseEvent(mouseEvent);204const lineNumbersCorrect = !!this._lineNumberOnMouseDown && this._lineNumberOnMouseDown === currentLineNumber;205if (lineNumbersCorrect && (this._hasTriggerKeyOnMouseDown || (mouseEvent.isMiddleClick && mouseEvent.mouseMiddleClickAction === 'openLink'))) {206this._onExecute.fire(mouseEvent);207}208}209210private _onEditorKeyDown(e: ClickLinkKeyboardEvent): void {211if (212this._lastMouseMoveEvent213&& (214e.keyCodeIsTriggerKey // User just pressed Ctrl/Cmd (normal goto definition)215|| (e.keyCodeIsSideBySideKey && e.hasTriggerModifier) // User pressed Ctrl/Cmd+Alt (goto definition to the side)216)217) {218this._onMouseMoveOrRelevantKeyDown.fire([this._lastMouseMoveEvent, e]);219} else if (e.hasTriggerModifier) {220this._onCancel.fire(); // remove decorations if user holds another key with ctrl/cmd to prevent accident goto declaration221}222}223224private _onEditorKeyUp(e: ClickLinkKeyboardEvent): void {225if (e.keyCodeIsTriggerKey) {226this._onCancel.fire();227}228}229230private _resetHandler(): void {231this._lastMouseMoveEvent = null;232this._hasTriggerKeyOnMouseDown = false;233this._onCancel.fire();234}235}236237238