Files
vscode/src/vs/editor/browser/services/hoverService.ts
T
Matt BiernerandGitHub 31fbc3dc94 Remove extra not null assertions (#202201)
* Remove extra not null assertions

With https://github.com/microsoft/TypeScript/pull/56908, TS should better preserve type refinements. To help test this out, I made a quick pass through our code to remove type assertions that are no longer needed

Most of these are not impacted by https://github.com/microsoft/TypeScript/pull/56908 but removing them helps TS test changes like this

Not adding an eslint rule for now as it requires whole program intellisense, which is too slow for commit hooks

* Fix merge
2024-01-30 15:43:09 -08:00

232 lines
9.0 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { editorHoverBorder } from 'vs/platform/theme/common/colorRegistry';
import { IHoverService, IHoverOptions } from 'vs/platform/hover/browser/hover';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { HoverWidget } from 'vs/editor/browser/widget/hoverWidget/hoverWidget';
import { IContextViewProvider, IDelegate } from 'vs/base/browser/ui/contextview/contextview';
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { addDisposableListener, EventType, getActiveElement, isAncestorOfActiveElement, isAncestor, getWindow } from 'vs/base/browser/dom';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { mainWindow } from 'vs/base/browser/window';
import { IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
export class HoverService implements IHoverService {
declare readonly _serviceBrand: undefined;
private _currentHoverOptions: IHoverOptions | undefined;
private _currentHover: HoverWidget | undefined;
private _lastHoverOptions: IHoverOptions | undefined;
private _lastFocusedElementBeforeOpen: HTMLElement | undefined;
constructor(
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IContextViewService private readonly _contextViewService: IContextViewService,
@IContextMenuService contextMenuService: IContextMenuService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@ILayoutService private readonly _layoutService: ILayoutService,
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService
) {
contextMenuService.onDidShowContextMenu(() => this.hideHover());
}
showHover(options: IHoverOptions, focus?: boolean, skipLastFocusedUpdate?: boolean): IHoverWidget | undefined {
if (getHoverOptionsIdentity(this._currentHoverOptions) === getHoverOptionsIdentity(options)) {
return undefined;
}
if (this._currentHover && this._currentHoverOptions?.persistence?.sticky) {
return undefined;
}
this._currentHoverOptions = options;
this._lastHoverOptions = options;
const trapFocus = options.trapFocus || this._accessibilityService.isScreenReaderOptimized();
const activeElement = getActiveElement();
// HACK, remove this check when #189076 is fixed
if (!skipLastFocusedUpdate) {
if (trapFocus && activeElement) {
this._lastFocusedElementBeforeOpen = activeElement as HTMLElement;
} else {
this._lastFocusedElementBeforeOpen = undefined;
}
}
const hoverDisposables = new DisposableStore();
const hover = this._instantiationService.createInstance(HoverWidget, options);
if (options.persistence?.sticky) {
hover.isLocked = true;
}
hover.onDispose(() => {
const hoverWasFocused = this._currentHover?.domNode && isAncestorOfActiveElement(this._currentHover.domNode);
if (hoverWasFocused) {
// Required to handle cases such as closing the hover with the escape key
this._lastFocusedElementBeforeOpen?.focus();
}
// Only clear the current options if it's the current hover, the current options help
// reduce flickering when the same hover is shown multiple times
if (this._currentHoverOptions === options) {
this._currentHoverOptions = undefined;
}
hoverDisposables.dispose();
});
// Set the container explicitly to enable aux window support
if (!options.container) {
const targetElement = options.target instanceof HTMLElement ? options.target : options.target.targetElements[0];
options.container = this._layoutService.getContainer(getWindow(targetElement));
}
const provider = this._contextViewService as IContextViewProvider;
provider.showContextView(
new HoverContextViewDelegate(hover, focus),
options.container
);
hover.onRequestLayout(() => provider.layout());
if (options.persistence?.sticky) {
hoverDisposables.add(addDisposableListener(getWindow(options.container).document, EventType.MOUSE_DOWN, e => {
if (!isAncestor(e.target as HTMLElement, hover.domNode)) {
this.doHideHover();
}
}));
} else {
if ('targetElements' in options.target) {
for (const element of options.target.targetElements) {
hoverDisposables.add(addDisposableListener(element, EventType.CLICK, () => this.hideHover()));
}
} else {
hoverDisposables.add(addDisposableListener(options.target, EventType.CLICK, () => this.hideHover()));
}
const focusedElement = getActiveElement();
if (focusedElement) {
const focusedElementDocument = getWindow(focusedElement).document;
hoverDisposables.add(addDisposableListener(focusedElement, EventType.KEY_DOWN, e => this._keyDown(e, hover, !!options.persistence?.hideOnKeyDown)));
hoverDisposables.add(addDisposableListener(focusedElementDocument, EventType.KEY_DOWN, e => this._keyDown(e, hover, !!options.persistence?.hideOnKeyDown)));
hoverDisposables.add(addDisposableListener(focusedElement, EventType.KEY_UP, e => this._keyUp(e, hover)));
hoverDisposables.add(addDisposableListener(focusedElementDocument, EventType.KEY_UP, e => this._keyUp(e, hover)));
}
}
if ('IntersectionObserver' in mainWindow) {
const observer = new IntersectionObserver(e => this._intersectionChange(e, hover), { threshold: 0 });
const firstTargetElement = 'targetElements' in options.target ? options.target.targetElements[0] : options.target;
observer.observe(firstTargetElement);
hoverDisposables.add(toDisposable(() => observer.disconnect()));
}
this._currentHover = hover;
return hover;
}
hideHover(): void {
if (this._currentHover?.isLocked || !this._currentHoverOptions) {
return;
}
this.doHideHover();
}
private doHideHover(): void {
this._currentHover = undefined;
this._currentHoverOptions = undefined;
this._contextViewService.hideContextView();
}
private _intersectionChange(entries: IntersectionObserverEntry[], hover: IDisposable): void {
const entry = entries[entries.length - 1];
if (!entry.isIntersecting) {
hover.dispose();
}
}
showAndFocusLastHover(): void {
if (!this._lastHoverOptions) {
return;
}
this.showHover(this._lastHoverOptions, true, true);
}
private _keyDown(e: KeyboardEvent, hover: HoverWidget, hideOnKeyDown: boolean) {
if (e.key === 'Alt') {
hover.isLocked = true;
return;
}
const event = new StandardKeyboardEvent(e);
const keybinding = this._keybindingService.resolveKeyboardEvent(event);
if (keybinding.getSingleModifierDispatchChords().some(value => !!value) || this._keybindingService.softDispatch(event, event.target).kind !== ResultKind.NoMatchingKb) {
return;
}
if (hideOnKeyDown && (!this._currentHoverOptions?.trapFocus || e.key !== 'Tab')) {
this.hideHover();
this._lastFocusedElementBeforeOpen?.focus();
}
}
private _keyUp(e: KeyboardEvent, hover: HoverWidget) {
if (e.key === 'Alt') {
hover.isLocked = false;
// Hide if alt is released while the mouse is not over hover/target
if (!hover.isMouseIn) {
this.hideHover();
this._lastFocusedElementBeforeOpen?.focus();
}
}
}
}
function getHoverOptionsIdentity(options: IHoverOptions | undefined): IHoverOptions | number | string | undefined {
if (options === undefined) {
return undefined;
}
return options?.id ?? options;
}
class HoverContextViewDelegate implements IDelegate {
get anchorPosition() {
return this._hover.anchor;
}
constructor(
private readonly _hover: HoverWidget,
private readonly _focus: boolean = false
) {
}
render(container: HTMLElement) {
this._hover.render(container);
if (this._focus) {
this._hover.focus();
}
return this._hover;
}
getAnchor() {
return {
x: this._hover.x,
y: this._hover.y
};
}
layout() {
this._hover.layout();
}
}
registerSingleton(IHoverService, HoverService, InstantiationType.Delayed);
registerThemingParticipant((theme, collector) => {
const hoverBorder = theme.getColor(editorHoverBorder);
if (hoverBorder) {
collector.addRule(`.monaco-workbench .workbench-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`);
collector.addRule(`.monaco-workbench .workbench-hover hr { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`);
}
});