Files
azuredatastudio/src/vs/editor/contrib/hover/hover.ts

347 lines
14 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { IDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
import { IEmptyContentData } from 'vs/editor/browser/controller/mouseTarget';
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions';
import { Range } from 'vs/editor/common/core/range';
import { IEditorContribution, IScrollEvent } from 'vs/editor/common/editorCommon';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { IModeService } from 'vs/editor/common/services/modeService';
import { HoverStartMode } from 'vs/editor/contrib/hover/hoverOperation';
import { ModesContentHoverWidget } from 'vs/editor/contrib/hover/modesContentHover';
import { ModesGlyphHoverWidget } from 'vs/editor/contrib/hover/modesGlyphHover';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { editorHoverBackground, editorHoverBorder, editorHoverHighlight, textCodeBlockBackground, textLinkForeground, editorHoverStatusBarBackground, editorHoverForeground } from 'vs/platform/theme/common/colorRegistry';
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDecorationService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
export class ModesHoverController implements IEditorContribution {
public static readonly ID = 'editor.contrib.hover';
private readonly _toUnhook = new DisposableStore();
private readonly _didChangeConfigurationHandler: IDisposable;
private readonly _contentWidget = new MutableDisposable<ModesContentHoverWidget>();
private readonly _glyphWidget = new MutableDisposable<ModesGlyphHoverWidget>();
get contentWidget(): ModesContentHoverWidget {
if (!this._contentWidget.value) {
this._createHoverWidgets();
}
return this._contentWidget.value!;
}
get glyphWidget(): ModesGlyphHoverWidget {
if (!this._glyphWidget.value) {
this._createHoverWidgets();
}
return this._glyphWidget.value!;
}
private _isMouseDown: boolean;
private _hoverClicked: boolean;
private _isHoverEnabled!: boolean;
private _isHoverSticky!: boolean;
private _hoverVisibleKey: IContextKey<boolean>;
static get(editor: ICodeEditor): ModesHoverController {
return editor.getContribution<ModesHoverController>(ModesHoverController.ID);
}
constructor(private readonly _editor: ICodeEditor,
@IOpenerService private readonly _openerService: IOpenerService,
@IModeService private readonly _modeService: IModeService,
@IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@IThemeService private readonly _themeService: IThemeService,
@IContextKeyService _contextKeyService: IContextKeyService
) {
this._isMouseDown = false;
this._hoverClicked = false;
this._hookEvents();
this._didChangeConfigurationHandler = this._editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {
if (e.hasChanged(EditorOption.hover)) {
this._hideWidgets();
this._unhookEvents();
this._hookEvents();
}
});
this._hoverVisibleKey = EditorContextKeys.hoverVisible.bindTo(_contextKeyService);
}
private _hookEvents(): void {
const hideWidgetsEventHandler = () => this._hideWidgets();
const hoverOpts = this._editor.getOption(EditorOption.hover);
this._isHoverEnabled = hoverOpts.enabled;
this._isHoverSticky = hoverOpts.sticky;
if (this._isHoverEnabled) {
this._toUnhook.add(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(e)));
this._toUnhook.add(this._editor.onMouseUp((e: IEditorMouseEvent) => this._onEditorMouseUp(e)));
this._toUnhook.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(e)));
this._toUnhook.add(this._editor.onKeyDown((e: IKeyboardEvent) => this._onKeyDown(e)));
this._toUnhook.add(this._editor.onDidChangeModelDecorations(() => this._onModelDecorationsChanged()));
} else {
this._toUnhook.add(this._editor.onMouseMove(hideWidgetsEventHandler));
this._toUnhook.add(this._editor.onKeyDown((e: IKeyboardEvent) => this._onKeyDown(e)));
}
this._toUnhook.add(this._editor.onMouseLeave(hideWidgetsEventHandler));
this._toUnhook.add(this._editor.onDidChangeModel(hideWidgetsEventHandler));
this._toUnhook.add(this._editor.onDidScrollChange((e: IScrollEvent) => this._onEditorScrollChanged(e)));
}
private _unhookEvents(): void {
this._toUnhook.clear();
}
private _onModelDecorationsChanged(): void {
this.contentWidget.onModelDecorationsChanged();
this.glyphWidget.onModelDecorationsChanged();
}
private _onEditorScrollChanged(e: IScrollEvent): void {
if (e.scrollTopChanged || e.scrollLeftChanged) {
this._hideWidgets();
}
}
private _onEditorMouseDown(mouseEvent: IEditorMouseEvent): void {
this._isMouseDown = true;
const targetType = mouseEvent.target.type;
if (targetType === MouseTargetType.CONTENT_WIDGET && mouseEvent.target.detail === ModesContentHoverWidget.ID) {
this._hoverClicked = true;
// mouse down on top of content hover widget
return;
}
if (targetType === MouseTargetType.OVERLAY_WIDGET && mouseEvent.target.detail === ModesGlyphHoverWidget.ID) {
// mouse down on top of overlay hover widget
return;
}
if (targetType !== MouseTargetType.OVERLAY_WIDGET && mouseEvent.target.detail !== ModesGlyphHoverWidget.ID) {
this._hoverClicked = false;
}
this._hideWidgets();
}
private _onEditorMouseUp(mouseEvent: IEditorMouseEvent): void {
this._isMouseDown = false;
}
private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void {
let targetType = mouseEvent.target.type;
if (this._isMouseDown && this._hoverClicked && this.contentWidget.isColorPickerVisible()) {
return;
}
if (this._isHoverSticky && targetType === MouseTargetType.CONTENT_WIDGET && mouseEvent.target.detail === ModesContentHoverWidget.ID) {
// mouse moved on top of content hover widget
return;
}
if (this._isHoverSticky && targetType === MouseTargetType.OVERLAY_WIDGET && mouseEvent.target.detail === ModesGlyphHoverWidget.ID) {
// mouse moved on top of overlay hover widget
return;
}
if (targetType === MouseTargetType.CONTENT_EMPTY) {
const epsilon = this._editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth / 2;
const data = <IEmptyContentData>mouseEvent.target.detail;
if (data && !data.isAfterLines && typeof data.horizontalDistanceToText === 'number' && data.horizontalDistanceToText < epsilon) {
// Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough
targetType = MouseTargetType.CONTENT_TEXT;
}
}
if (targetType === MouseTargetType.CONTENT_TEXT) {
this.glyphWidget.hide();
if (this._isHoverEnabled && mouseEvent.target.range) {
this.contentWidget.startShowingAt(mouseEvent.target.range, HoverStartMode.Delayed, false);
}
} else if (targetType === MouseTargetType.GUTTER_GLYPH_MARGIN) {
this.contentWidget.hide();
if (this._isHoverEnabled && mouseEvent.target.position) {
this.glyphWidget.startShowingAt(mouseEvent.target.position.lineNumber);
}
} else {
this._hideWidgets();
}
}
private _onKeyDown(e: IKeyboardEvent): void {
if (e.keyCode !== KeyCode.Ctrl && e.keyCode !== KeyCode.Alt && e.keyCode !== KeyCode.Meta && e.keyCode !== KeyCode.Shift) {
// Do not hide hover when a modifier key is pressed
this._hideWidgets();
}
}
private _hideWidgets(): void {
if (!this._glyphWidget.value || !this._contentWidget.value || (this._isMouseDown && this._hoverClicked && this._contentWidget.value.isColorPickerVisible())) {
return;
}
this._glyphWidget.value.hide();
this._contentWidget.value.hide();
}
private _createHoverWidgets() {
this._contentWidget.value = new ModesContentHoverWidget(this._editor, this._hoverVisibleKey, this._markerDecorationsService, this._keybindingService, this._themeService, this._modeService, this._openerService);
this._glyphWidget.value = new ModesGlyphHoverWidget(this._editor, this._modeService, this._openerService);
}
public showContentHover(range: Range, mode: HoverStartMode, focus: boolean): void {
this.contentWidget.startShowingAt(range, mode, focus);
}
public dispose(): void {
this._unhookEvents();
this._toUnhook.dispose();
this._didChangeConfigurationHandler.dispose();
this._glyphWidget.dispose();
this._contentWidget.dispose();
}
}
class ShowHoverAction extends EditorAction {
constructor() {
super({
id: 'editor.action.showHover',
label: nls.localize({
key: 'showHover',
comment: [
'Label for action that will trigger the showing of a hover in the editor.',
'This allows for users to show the hover without using the mouse.'
]
}, "Show Hover"),
alias: 'Show Hover',
precondition: undefined,
kbOpts: {
kbExpr: EditorContextKeys.editorTextFocus,
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_I),
weight: KeybindingWeight.EditorContrib
}
});
}
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
if (!editor.hasModel()) {
return;
}
let controller = ModesHoverController.get(editor);
if (!controller) {
return;
}
const position = editor.getPosition();
const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column);
const focus = editor.getOption(EditorOption.accessibilitySupport) === AccessibilitySupport.Enabled;
controller.showContentHover(range, HoverStartMode.Immediate, focus);
}
}
class ShowDefinitionPreviewHoverAction extends EditorAction {
constructor() {
super({
id: 'editor.action.showDefinitionPreviewHover',
label: nls.localize({
key: 'showDefinitionPreviewHover',
comment: [
'Label for action that will trigger the showing of definition preview hover in the editor.',
'This allows for users to show the definition preview hover without using the mouse.'
]
}, "Show Definition Preview Hover"),
alias: 'Show Definition Preview Hover',
precondition: undefined
});
}
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
let controller = ModesHoverController.get(editor);
if (!controller) {
return;
}
const position = editor.getPosition();
if (!position) {
return;
}
const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column);
const goto = GotoDefinitionAtPositionEditorContribution.get(editor);
const promise = goto.startFindDefinitionFromCursor(position);
if (promise) {
promise.then(() => {
controller.showContentHover(range, HoverStartMode.Immediate, true);
});
} else {
controller.showContentHover(range, HoverStartMode.Immediate, true);
}
}
}
registerEditorContribution(ModesHoverController.ID, ModesHoverController);
registerEditorAction(ShowHoverAction);
registerEditorAction(ShowDefinitionPreviewHoverAction);
// theming
registerThemingParticipant((theme, collector) => {
const editorHoverHighlightColor = theme.getColor(editorHoverHighlight);
if (editorHoverHighlightColor) {
collector.addRule(`.monaco-editor .hoverHighlight { background-color: ${editorHoverHighlightColor}; }`);
}
const hoverBackground = theme.getColor(editorHoverBackground);
if (hoverBackground) {
collector.addRule(`.monaco-editor .monaco-hover { background-color: ${hoverBackground}; }`);
}
const hoverBorder = theme.getColor(editorHoverBorder);
if (hoverBorder) {
collector.addRule(`.monaco-editor .monaco-hover { border: 1px solid ${hoverBorder}; }`);
collector.addRule(`.monaco-editor .monaco-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`);
collector.addRule(`.monaco-editor .monaco-hover hr { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`);
collector.addRule(`.monaco-editor .monaco-hover hr { border-bottom: 0px solid ${hoverBorder.transparent(0.5)}; }`);
}
const link = theme.getColor(textLinkForeground);
if (link) {
collector.addRule(`.monaco-editor .monaco-hover a { color: ${link}; }`);
}
const hoverForeground = theme.getColor(editorHoverForeground);
if (hoverForeground) {
collector.addRule(`.monaco-editor .monaco-hover { color: ${hoverForeground}; }`);
}
const actionsBackground = theme.getColor(editorHoverStatusBarBackground);
if (actionsBackground) {
collector.addRule(`.monaco-editor .monaco-hover .hover-row .actions { background-color: ${actionsBackground}; }`);
}
const codeBackground = theme.getColor(textCodeBlockBackground);
if (codeBackground) {
collector.addRule(`.monaco-editor .monaco-hover code { background-color: ${codeBackground}; }`);
}
});