Merge branch 'master' into a11y-char-width2

This commit is contained in:
Daniel Imms
2024-06-04 05:29:06 -07:00
committed by GitHub
108 changed files with 1988 additions and 587 deletions
@@ -5,11 +5,13 @@
import { assert } from 'chai';
import { IBufferService } from 'common/services/Services';
import { Linkifier2 } from 'browser/Linkifier2';
import { Linkifier } from './Linkifier';
import { MockBufferService } from 'common/TestUtils.test';
import { ILink } from 'browser/Types';
import { LinkProviderService } from 'browser/services/LinkProviderService';
import jsdom = require('jsdom');
class TestLinkifier2 extends Linkifier2 {
class TestLinkifier2 extends Linkifier {
public set currentLink(link: any) {
this._currentLink = link;
}
@@ -43,8 +45,9 @@ describe('Linkifier2', () => {
};
beforeEach(() => {
const dom = new jsdom.JSDOM();
bufferService = new MockBufferService(100, 10);
linkifier = new TestLinkifier2(bufferService);
linkifier = new TestLinkifier2(dom.window.document.createElement('div'), null!, null!, bufferService, new LinkProviderService());
linkifier.currentLink = {
link,
state: {
@@ -4,18 +4,14 @@
*/
import { addDisposableDomListener } from 'browser/Lifecycle';
import { IBufferCellPosition, ILink, ILinkDecorations, ILinkProvider, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
import { EventEmitter } from 'common/EventEmitter';
import { Disposable, disposeArray, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle';
import { IDisposable } from 'common/Types';
import { IBufferService } from 'common/services/Services';
import { IMouseService, IRenderService } from './services/Services';
import { ILinkProviderService, IMouseService, IRenderService } from './services/Services';
export class Linkifier2 extends Disposable implements ILinkifier2 {
private _element: HTMLElement | undefined;
private _mouseService: IMouseService | undefined;
private _renderService: IRenderService | undefined;
private _linkProviders: ILinkProvider[] = [];
export class Linkifier extends Disposable implements ILinkifier2 {
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
protected _currentLink: ILinkWithState | undefined;
private _mouseDownLink: ILinkWithState | undefined;
@@ -33,39 +29,24 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
public readonly onHideLinkUnderline = this._onHideLinkUnderline.event;
constructor(
@IBufferService private readonly _bufferService: IBufferService
private readonly _element: HTMLElement,
@IMouseService private readonly _mouseService: IMouseService,
@IRenderService private readonly _renderService: IRenderService,
@IBufferService private readonly _bufferService: IBufferService,
@ILinkProviderService private readonly _linkProviderService: ILinkProviderService
) {
super();
this.register(getDisposeArrayDisposable(this._linkCacheDisposables));
this.register(toDisposable(() => {
this._lastMouseEvent = undefined;
// Clear out link providers as they could easily cause an embedder memory leak
this._activeProviderReplies?.clear();
}));
// Listen to resize to catch the case where it's resized and the cursor is out of the viewport.
this.register(this._bufferService.onResize(() => {
this._clearCurrentLink();
this._wasResized = true;
}));
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
this._linkProviders.push(linkProvider);
return {
dispose: () => {
// Remove the link provider from the list
const providerIndex = this._linkProviders.indexOf(linkProvider);
if (providerIndex !== -1) {
this._linkProviders.splice(providerIndex, 1);
}
}
};
}
public attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void {
this._element = element;
this._mouseService = mouseService;
this._renderService = renderService;
this.register(addDisposableDomListener(this._element, 'mouseleave', () => {
this._isMouseOut = true;
this._clearCurrentLink();
@@ -78,10 +59,6 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
private _handleMouseMove(event: MouseEvent): void {
this._lastMouseEvent = event;
if (!this._element || !this._mouseService) {
return;
}
const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
if (!position) {
return;
@@ -142,7 +119,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
let linkProvided = false;
// There is no link cached, so ask for one
for (const [i, linkProvider] of this._linkProviders.entries()) {
for (const [i, linkProvider] of this._linkProviderService.linkProviders.entries()) {
if (useLineCache) {
const existingReply = this._activeProviderReplies?.get(i);
// If there isn't a reply, the provider hasn't responded yet.
@@ -164,7 +141,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
// If all providers have responded, remove lower priority links that intersect ranges of
// higher priority links
if (this._activeProviderReplies?.size === this._linkProviders.length) {
if (this._activeProviderReplies?.size === this._linkProviderService.linkProviders.length) {
this._removeIntersectingLinks(position.y, this._activeProviderReplies);
}
});
@@ -220,7 +197,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
// Check if all the providers have responded
if (this._activeProviderReplies.size === this._linkProviders.length && !linkProvided) {
if (this._activeProviderReplies.size === this._linkProviderService.linkProviders.length && !linkProvided) {
// Respect the order of the link providers
for (let j = 0; j < this._activeProviderReplies.size; j++) {
const currentLink = this._activeProviderReplies.get(j)?.find(link => this._linkAtPosition(link.link, position));
@@ -240,7 +217,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
private _handleMouseUp(event: MouseEvent): void {
if (!this._element || !this._mouseService || !this._currentLink) {
if (!this._currentLink) {
return;
}
@@ -255,7 +232,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
private _clearCurrentLink(startRow?: number, endRow?: number): void {
if (!this._element || !this._currentLink || !this._lastMouseEvent) {
if (!this._currentLink || !this._lastMouseEvent) {
return;
}
@@ -268,7 +245,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
private _handleNewLink(linkWithState: ILinkWithState): void {
if (!this._element || !this._lastMouseEvent || !this._mouseService) {
if (!this._lastMouseEvent) {
return;
}
@@ -299,7 +276,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
if (this._currentLink?.state && this._currentLink.state.decorations.pointerCursor !== v) {
this._currentLink.state.decorations.pointerCursor = v;
if (this._currentLink.state.isHovered) {
this._element?.classList.toggle('xterm-cursor-pointer', v);
this._element.classList.toggle('xterm-cursor-pointer', v);
}
}
}
@@ -319,29 +296,27 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
// Listen to viewport changes to re-render the link under the cursor (only when the line the
// link is on changes)
if (this._renderService) {
this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
// Sanity check, this shouldn't happen in practice as this listener would be disposed
if (!this._currentLink) {
return;
}
// When start is 0 a scroll most likely occurred, make sure links above the fold also get
// cleared.
const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp;
const end = this._bufferService.buffer.ydisp + 1 + e.end;
// Only clear the link if the viewport change happened on this line
if (this._currentLink.link.range.start.y >= start && this._currentLink.link.range.end.y <= end) {
this._clearCurrentLink(start, end);
if (this._lastMouseEvent && this._element) {
// re-eval previously active link after changes
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
if (position) {
this._askForLink(position, false);
}
this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
// Sanity check, this shouldn't happen in practice as this listener would be disposed
if (!this._currentLink) {
return;
}
// When start is 0 a scroll most likely occurred, make sure links above the fold also get
// cleared.
const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp;
const end = this._bufferService.buffer.ydisp + 1 + e.end;
// Only clear the link if the viewport change happened on this line
if (this._currentLink.link.range.start.y >= start && this._currentLink.link.range.end.y <= end) {
this._clearCurrentLink(start, end);
if (this._lastMouseEvent) {
// re-eval previously active link after changes
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
if (position) {
this._askForLink(position, false);
}
}
}));
}
}
}));
}
}
+2 -1
View File
@@ -3,7 +3,8 @@
* @license MIT
*/
import { IBufferRange, ILink, ILinkProvider } from 'browser/Types';
import { IBufferRange, ILink } from 'browser/Types';
import { ILinkProvider } from 'browser/services/Services';
import { CellData } from 'common/buffer/CellData';
import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
+6 -5
View File
@@ -4,6 +4,7 @@
*/
import { IRenderDebouncerWithCallback } from 'browser/Types';
import { ICoreBrowserService } from 'browser/services/Services';
/**
* Debounces calls to render terminal rows using animation frames.
@@ -16,14 +17,14 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
private _refreshCallbacks: FrameRequestCallback[] = [];
constructor(
private _parentWindow: Window,
private _renderCallback: (start: number, end: number) => void
private _renderCallback: (start: number, end: number) => void,
private readonly _coreBrowserService: ICoreBrowserService
) {
}
public dispose(): void {
if (this._animationFrame) {
this._parentWindow.cancelAnimationFrame(this._animationFrame);
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
@@ -31,7 +32,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
public addRefreshCallback(callback: FrameRequestCallback): number {
this._refreshCallbacks.push(callback);
if (!this._animationFrame) {
this._animationFrame = this._parentWindow.requestAnimationFrame(() => this._innerRefresh());
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
}
return this._animationFrame;
}
@@ -49,7 +50,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
return;
}
this._animationFrame = this._parentWindow.requestAnimationFrame(() => this._innerRefresh());
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
}
private _innerRefresh(): void {
+41 -33
View File
@@ -23,10 +23,10 @@
import { copyHandler, handlePasteEvent, moveTextAreaUnderMouseCursor, paste, rightClickHandler } from 'browser/Clipboard';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { Linkifier2 } from 'browser/Linkifier2';
import { Linkifier } from './Linkifier';
import * as Strings from 'browser/LocalizableStrings';
import { OscLinkProvider } from 'browser/OscLinkProvider';
import { CharacterJoinerHandler, CustomKeyEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal, IViewport } from 'browser/Types';
import { CharacterJoinerHandler, CustomKeyEventHandler, CustomWheelEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal, IViewport } from 'browser/Types';
import { Viewport } from 'browser/Viewport';
import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRenderer';
import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer';
@@ -39,9 +39,9 @@ import { CoreBrowserService } from 'browser/services/CoreBrowserService';
import { MouseService } from 'browser/services/MouseService';
import { RenderService } from 'browser/services/RenderService';
import { SelectionService } from 'browser/services/SelectionService';
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { ThemeService } from 'browser/services/ThemeService';
import { color, rgba } from 'common/Color';
import { channels, color } from 'common/Color';
import { CoreTerminal } from 'common/CoreTerminal';
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
import { MutableDisposable, toDisposable } from 'common/Lifecycle';
@@ -57,6 +57,7 @@ import { IDecorationService } from 'common/services/Services';
import { IDecoration, IDecorationOptions, IDisposable, ILinkProvider, IMarker } from '@xterm/xterm';
import { WindowsOptionsReportType } from '../common/InputHandler';
import { AccessibilityManager } from './AccessibilityManager';
import { LinkProviderService } from 'browser/services/LinkProviderService';
export class Terminal extends CoreTerminal implements ITerminal {
public textarea: HTMLTextAreaElement | undefined;
@@ -69,14 +70,19 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _helperContainer: HTMLElement | undefined;
private _compositionView: HTMLElement | undefined;
public linkifier: ILinkifier2 | undefined;
private _overviewRulerRenderer: OverviewRulerRenderer | undefined;
public browser: IBrowser = Browser as any;
private _customKeyEventHandler: CustomKeyEventHandler | undefined;
private _customWheelEventHandler: CustomWheelEventHandler | undefined;
// browser services
// Browser services
private _decorationService: DecorationService;
private _linkProviderService: ILinkProviderService;
// Optional browser services
private _charSizeService: ICharSizeService | undefined;
private _coreBrowserService: ICoreBrowserService | undefined;
private _mouseService: IMouseService | undefined;
@@ -112,7 +118,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
*/
private _unprocessedDeadKey: boolean = false;
public linkifier2: ILinkifier2;
public viewport: IViewport | undefined;
private _compositionHelper: ICompositionHelper | undefined;
private _accessibilityManager: MutableDisposable<AccessibilityManager> = this.register(new MutableDisposable());
@@ -148,10 +153,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._setup();
this.linkifier2 = this.register(this._instantiationService.createInstance(Linkifier2));
this.linkifier2.registerLinkProvider(this._instantiationService.createInstance(OscLinkProvider));
this._decorationService = this._instantiationService.createInstance(DecorationService);
this._instantiationService.setService(IDecorationService, this._decorationService);
this._linkProviderService = this._instantiationService.createInstance(LinkProviderService);
this._instantiationService.setService(ILinkProviderService, this._linkProviderService);
this._linkProviderService.registerLinkProvider(this._instantiationService.createInstance(OscLinkProvider));
// Setup InputHandler listeners
this.register(this._inputHandler.onRequestBell(() => this._onBell.fire()));
@@ -205,17 +211,17 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
switch (req.type) {
case ColorRequestType.REPORT:
const channels = color.toColorRGB(acc === 'ansi'
const colorRgb = color.toColorRGB(acc === 'ansi'
? this._themeService.colors.ansi[req.index]
: this._themeService.colors[acc]);
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C1_ESCAPED.ST}`);
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(colorRgb)}${C1_ESCAPED.ST}`);
break;
case ColorRequestType.SET:
if (acc === 'ansi') {
this._themeService.modifyColors(colors => colors.ansi[req.index] = rgba.toColor(...req.color));
this._themeService.modifyColors(colors => colors.ansi[req.index] = channels.toColor(...req.color));
} else {
const narrowedAcc = acc;
this._themeService.modifyColors(colors => colors[narrowedAcc] = rgba.toColor(...req.color));
this._themeService.modifyColors(colors => colors[narrowedAcc] = channels.toColor(...req.color));
}
break;
case ColorRequestType.RESTORE:
@@ -260,11 +266,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
/**
* Binds the desired focus behavior on a given terminal object.
*/
private _handleTextAreaFocus(ev: KeyboardEvent): void {
private _handleTextAreaFocus(ev: FocusEvent): void {
if (this.coreService.decPrivateModes.sendFocus) {
this.coreService.triggerDataEvent(C0.ESC + '[I');
}
this.updateCursorStyle(ev);
this.element!.classList.add('focus');
this._showCursor();
this._onFocus.fire();
@@ -428,6 +433,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.screenElement = this._document.createElement('div');
this.screenElement.classList.add('xterm-screen');
this.register(addDisposableDomListener(this.screenElement, 'mousemove', (ev: MouseEvent) => this.updateCursorStyle(ev)));
// Create the container that will hold helpers like the textarea for
// capturing DOM Events. Then produce the helpers.
this._helperContainer = this._document.createElement('div');
@@ -458,11 +464,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
));
this._instantiationService.setService(ICoreBrowserService, this._coreBrowserService);
this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._handleTextAreaFocus(ev)));
this.register(addDisposableDomListener(this.textarea, 'focus', (ev: FocusEvent) => this._handleTextAreaFocus(ev)));
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._handleTextAreaBlur()));
this._helperContainer.appendChild(this.textarea);
this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
this._instantiationService.setService(ICharSizeService, this._charSizeService);
@@ -482,6 +487,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
this._helperContainer.appendChild(this._compositionView);
this._mouseService = this._instantiationService.createInstance(MouseService);
this._instantiationService.setService(IMouseService, this._mouseService);
this.linkifier = this.register(this._instantiationService.createInstance(Linkifier, this.screenElement));
// Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment);
@@ -493,9 +503,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._renderService.setRenderer(this._createRenderer());
}
this._mouseService = this._instantiationService.createInstance(MouseService);
this._instantiationService.setService(IMouseService, this._mouseService);
this.viewport = this._instantiationService.createInstance(Viewport, this._viewportElement, this._viewportScrollArea);
this.viewport.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent, ScrollSource.VIEWPORT)),
this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea()));
@@ -513,7 +520,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._selectionService = this.register(this._instantiationService.createInstance(SelectionService,
this.element,
this.screenElement,
this.linkifier2
this.linkifier
));
this._instantiationService.setService(ISelectionService, this._selectionService);
this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
@@ -533,7 +540,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
}));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService!.refresh()));
this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService);
this.register(this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement));
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e)));
@@ -575,7 +581,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
private _createRenderer(): IRenderer {
return this._instantiationService.createInstance(DomRenderer, this, this._document!, this.element!, this.screenElement!, this._viewportElement!, this._helperContainer!, this.linkifier2);
return this._instantiationService.createInstance(DomRenderer, this, this._document!, this.element!, this.screenElement!, this._viewportElement!, this._helperContainer!, this.linkifier!);
}
/**
@@ -633,6 +639,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
break;
case 'wheel':
if (self._customWheelEventHandler && self._customWheelEventHandler(ev as WheelEvent) === false) {
return false;
}
const amount = self.viewport!.getLinesScrolled(ev as WheelEvent);
if (amount === 0) {
@@ -792,6 +801,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
// do nothing, if app side handles wheel itself
if (requestedEvents.wheel) return;
if (this._customWheelEventHandler && this._customWheelEventHandler(ev) === false) {
return false;
}
if (!this.buffer.hasScrollback) {
// Convert wheel events into up/down events when the buffer does not have scrollback, this
// enables scrolling in apps hosted in the alt buffer such as vim or tmux.
@@ -847,7 +860,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
/**
* Change the cursor style for different selection modes
*/
public updateCursorStyle(ev: KeyboardEvent): void {
public updateCursorStyle(ev: KeyboardEvent | MouseEvent): void {
if (this._selectionService?.shouldColumnSelect(ev)) {
this.element!.classList.add('column-select');
} else {
@@ -878,21 +891,16 @@ export class Terminal extends CoreTerminal implements ITerminal {
paste(data, this.textarea!, this.coreService, this.optionsService);
}
/**
* Attaches a custom key event handler which is run before keys are processed,
* giving consumers of xterm.js ultimate control as to what keys should be
* processed by the terminal and what keys should not.
* @param customKeyEventHandler The custom KeyboardEvent handler to attach.
* This is a function that takes a KeyboardEvent, allowing consumers to stop
* propagation and/or prevent the default action. The function returns whether
* the event should be processed by xterm.js.
*/
public attachCustomKeyEventHandler(customKeyEventHandler: CustomKeyEventHandler): void {
this._customKeyEventHandler = customKeyEventHandler;
}
public attachCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler): void {
this._customWheelEventHandler = customWheelEventHandler;
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
return this.linkifier2.registerLinkProvider(linkProvider);
return this._linkProviderService.registerLinkProvider(linkProvider);
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
+7 -1
View File
@@ -48,6 +48,7 @@ export class MockTerminal implements ITerminal {
public onRender!: IEvent<{ start: number, end: number }>;
public onResize!: IEvent<{ cols: number, rows: number }>;
public markers!: IMarker[];
public linkifier: ILinkifier2 | undefined;
public coreMouseService!: ICoreMouseService;
public coreService!: ICoreService;
public optionsService!: IOptionsService;
@@ -71,6 +72,9 @@ export class MockTerminal implements ITerminal {
public focus(): void {
throw new Error('Method not implemented.');
}
public input(data: string, wasUserInput: boolean = true): void {
throw new Error('Method not implemented.');
}
public resize(columns: number, rows: number): void {
throw new Error('Method not implemented.');
}
@@ -86,6 +90,9 @@ export class MockTerminal implements ITerminal {
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
throw new Error('Method not implemented.');
}
public attachCustomWheelEventHandler(customWheelEventHandler: (event: WheelEvent) => boolean): void {
throw new Error('Method not implemented.');
}
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
@@ -148,7 +155,6 @@ export class MockTerminal implements ITerminal {
}
public bracketedPasteMode!: boolean;
public renderer!: IRenderer;
public linkifier2!: ILinkifier2;
public isFocused!: boolean;
public options!: Required<ITerminalOptions>;
public element!: HTMLElement;
+2 -9
View File
@@ -7,7 +7,6 @@ import { IEvent } from 'common/EventEmitter';
import { CharData, IColor, ICoreTerminal, ITerminalOptions } from 'common/Types';
import { IBuffer } from 'common/buffer/Types';
import { IDisposable, Terminal as ITerminalApi } from '@xterm/xterm';
import { IMouseService, IRenderService } from './services/Services';
/**
* A portion of the public API that are implemented identially internally and simply passed through.
@@ -18,9 +17,9 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal {
screenElement: HTMLElement | undefined;
browser: IBrowser;
buffer: IBuffer;
linkifier: ILinkifier2 | undefined;
viewport: IViewport | undefined;
options: Required<ITerminalOptions>;
linkifier2: ILinkifier2;
onBlur: IEvent<void>;
onFocus: IEvent<void>;
@@ -32,6 +31,7 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal {
}
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
export type CustomWheelEventHandler = (event: WheelEvent) => boolean;
export type LineData = CharData[];
@@ -127,13 +127,6 @@ export interface ILinkifier2 extends IDisposable {
onShowLinkUnderline: IEvent<ILinkifierEvent>;
onHideLinkUnderline: IEvent<ILinkifierEvent>;
readonly currentLink: ILinkWithState | undefined;
attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void;
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
}
interface ILinkProvider {
provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void;
}
interface ILink {
+15 -2
View File
@@ -36,6 +36,8 @@ export class Viewport extends Disposable implements IViewport {
private _activeBuffer: IBuffer;
private _renderDimensions: IRenderDimensions;
private _smoothScrollAnimationFrame: number = 0;
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
// quick fix and could have a more robust solution in place that reset the value when needed.
@@ -49,6 +51,8 @@ export class Viewport extends Disposable implements IViewport {
target: -1
};
private _ensureTimeout: number;
private readonly _onRequestScrollLines = this.register(new EventEmitter<{ amount: number, suppressScrollEvent: boolean }>());
public readonly onRequestScrollLines = this._onRequestScrollLines.event;
@@ -81,7 +85,7 @@ export class Viewport extends Disposable implements IViewport {
this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.syncScrollArea()));
// Perform this async to ensure the ICharSizeService is ready.
setTimeout(() => this.syncScrollArea());
this._ensureTimeout = window.setTimeout(() => this.syncScrollArea());
}
private _handleThemeChange(colors: ReadonlyColorSet): void {
@@ -211,7 +215,12 @@ export class Viewport extends Disposable implements IViewport {
// Continue or finish smooth scroll
if (percent < 1) {
this._coreBrowserService.window.requestAnimationFrame(() => this._smoothScroll());
if (!this._smoothScrollAnimationFrame) {
this._smoothScrollAnimationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
this._smoothScrollAnimationFrame = 0;
this._smoothScroll();
});
}
} else {
this._clearSmoothScrollState();
}
@@ -398,4 +407,8 @@ export class Viewport extends Disposable implements IViewport {
this._viewportElement.scrollTop += deltaY;
return this._bubbleScroll(ev, deltaY);
}
public dispose(): void {
clearTimeout(this._ensureTimeout);
}
}
+3 -2
View File
@@ -159,8 +159,9 @@ export class CompositionHelper {
// otherwise input characters can be duplicated. (Issue #3191)
currentCompositionPosition.start += this._dataAlreadySent.length;
if (this._isComposing) {
// Use the end position to get the string if a new composition has started.
input = this._textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
// Use the start position of the new composition to get the string
// if a new composition has started.
input = this._textarea.value.substring(currentCompositionPosition.start, this._compositionPosition.start);
} else {
// Don't use the end position here in order to pick up any characters after the
// composition has finished, for example when typing a non-composition character
+12 -4
View File
@@ -20,6 +20,8 @@ import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOption
*/
const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'];
let $value = 0;
export class Terminal extends Disposable implements ITerminalApi {
private _core: ITerminal;
private _addonManager: AddonManager;
@@ -138,6 +140,9 @@ export class Terminal extends Disposable implements ITerminalApi {
public focus(): void {
this._core.focus();
}
public input(data: string, wasUserInput: boolean = true): void {
this._core.input(data, wasUserInput);
}
public resize(columns: number, rows: number): void {
this._verifyIntegers(columns, rows);
this._core.resize(columns, rows);
@@ -148,6 +153,9 @@ export class Terminal extends Disposable implements ITerminalApi {
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
this._core.attachCustomKeyEventHandler(customKeyEventHandler);
}
public attachCustomWheelEventHandler(customWheelEventHandler: (event: WheelEvent) => boolean): void {
this._core.attachCustomWheelEventHandler(customWheelEventHandler);
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
return this._core.registerLinkProvider(linkProvider);
}
@@ -243,16 +251,16 @@ export class Terminal extends Disposable implements ITerminalApi {
}
private _verifyIntegers(...values: number[]): void {
for (const value of values) {
if (value === Infinity || isNaN(value) || value % 1 !== 0) {
for ($value of values) {
if ($value === Infinity || isNaN($value) || $value % 1 !== 0) {
throw new Error('This API only accepts integers');
}
}
}
private _verifyPositiveIntegers(...values: number[]): void {
for (const value of values) {
if (value && (value === Infinity || isNaN(value) || value % 1 !== 0 || value < 0)) {
for ($value of values) {
if ($value && ($value === Infinity || isNaN($value) || $value % 1 !== 0 || $value < 0)) {
throw new Error('This API only accepts positive integers');
}
}
+27 -10
View File
@@ -183,14 +183,23 @@ export class DomRenderer extends Disposable implements IRenderer {
` font-style: italic;` +
`}`;
// Blink animation
const blinkAnimationUnderlineId = `blink_underline_${this._terminalClass}`;
const blinkAnimationBarId = `blink_bar_${this._terminalClass}`;
const blinkAnimationBlockId = `blink_block_${this._terminalClass}`;
styles +=
`@keyframes blink_box_shadow` + `_` + this._terminalClass + ` {` +
`@keyframes ${blinkAnimationUnderlineId} {` +
` 50% {` +
` border-bottom-style: hidden;` +
` }` +
`}`;
styles +=
`@keyframes blink_block` + `_` + this._terminalClass + ` {` +
`@keyframes ${blinkAnimationBarId} {` +
` 50% {` +
` box-shadow: none;` +
` }` +
`}`;
styles +=
`@keyframes ${blinkAnimationBlockId} {` +
` 0% {` +
` background-color: ${colors.cursor.css};` +
` color: ${colors.cursorAccent.css};` +
@@ -202,13 +211,23 @@ export class DomRenderer extends Disposable implements IRenderer {
`}`;
// Cursor
styles +=
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}:not(.${RowCss.CURSOR_STYLE_BLOCK_CLASS}) {` +
` animation: blink_box_shadow` + `_` + this._terminalClass + ` 1s step-end infinite;` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_UNDERLINE_CLASS} {` +
` animation: ${blinkAnimationUnderlineId} 1s step-end infinite;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BAR_CLASS} {` +
` animation: ${blinkAnimationBarId} 1s step-end infinite;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
` animation: blink_block` + `_` + this._terminalClass + ` 1s step-end infinite;` +
` animation: ${blinkAnimationBlockId} 1s step-end infinite;` +
`}` +
// !important helps fix an issue where the cursor will not render on top of the selection,
// however it's very hard to fix this issue and retain the blink animation without the use of
// !important. So this edge case fails when cursor blink is on.
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
` background-color: ${colors.cursor.css};` +
` color: ${colors.cursorAccent.css};` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS}:not(.${RowCss.CURSOR_BLINK_CLASS}) {` +
` background-color: ${colors.cursor.css} !important;` +
` color: ${colors.cursorAccent.css} !important;` +
`}` +
@@ -324,6 +343,9 @@ export class DomRenderer extends Disposable implements IRenderer {
}
this._selectionRenderModel.update(this._terminal, start, end, columnSelectMode);
if (!this._selectionRenderModel.hasSelection) {
return;
}
// Translate from buffer position to viewport position
const viewportStartRow = this._selectionRenderModel.viewportStartRow;
@@ -331,11 +353,6 @@ export class DomRenderer extends Disposable implements IRenderer {
const viewportCappedStartRow = this._selectionRenderModel.viewportCappedStartRow;
const viewportCappedEndRow = this._selectionRenderModel.viewportCappedEndRow;
// No need to draw the selection
if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) {
return;
}
// Create the selections
const documentFragment = this._document.createDocumentFragment();
@@ -8,10 +8,10 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { color, rgba } from 'common/Color';
import { channels, color } from 'common/Color';
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils';
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
import { AttributeData } from 'common/buffer/AttributeData';
import { WidthCache } from 'browser/renderer/dom/WidthCache';
import { IColorContrastCache } from 'browser/Types';
@@ -376,7 +376,7 @@ export class DomRendererRowFactory {
classes.push(`xterm-bg-${bg}`);
break;
case Attributes.CM_RGB:
resolvedBg = rgba.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF);
resolvedBg = channels.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF);
this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`);
break;
case Attributes.CM_DEFAULT:
@@ -408,7 +408,7 @@ export class DomRendererRowFactory {
}
break;
case Attributes.CM_RGB:
const color = rgba.toColor(
const color = channels.toColor(
(fg >> 16) & 0xFF,
(fg >> 8) & 0xFF,
(fg ) & 0xFF
@@ -458,7 +458,7 @@ export class DomRendererRowFactory {
}
private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor, cell: ICellData, bgOverride: IColor | undefined, fgOverride: IColor | undefined): boolean {
if (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode())) {
if (this._optionsService.rawOptions.minimumContrastRatio === 1 || treatGlyphAsBackgroundColor(cell.getCode())) {
return false;
}
+11 -4
View File
@@ -134,9 +134,14 @@ export class WidthCache implements IDisposable {
public get(c: string, bold: boolean | number, italic: boolean | number): number {
let cp = 0;
if (!bold && !italic && c.length === 1 && (cp = c.charCodeAt(0)) < WidthCacheSettings.FLAT_SIZE) {
return this._flat[cp] !== WidthCacheSettings.FLAT_UNSET
? this._flat[cp]
: (this._flat[cp] = this._measure(c, 0));
if (this._flat[cp] !== WidthCacheSettings.FLAT_UNSET) {
return this._flat[cp];
}
const width = this._measure(c, 0);
if (width > 0) {
this._flat[cp] = width;
}
return width;
}
let key = c;
if (bold) key += 'B';
@@ -147,7 +152,9 @@ export class WidthCache implements IDisposable {
if (bold) variant |= FontVariant.BOLD;
if (italic) variant |= FontVariant.ITALIC;
width = this._measure(c, variant);
this._holey!.set(key, width);
if (width > 0) {
this._holey!.set(key, width);
}
}
return width;
}
@@ -5,6 +5,8 @@ import { Attributes, BgFlags, ExtFlags, FgFlags, NULL_CELL_CODE, UnderlineStyle
import { IDecorationService, IOptionsService } from 'common/services/Services';
import { ICellData } from 'common/Types';
import { Terminal } from '@xterm/xterm';
import { rgba } from 'common/Color';
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
// Work variables to avoid garbage collection
let $fg = 0;
@@ -65,11 +67,11 @@ export class CellColorResolver {
// Apply decorations on the bottom layer
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
if (d.backgroundColorRGB) {
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
$bg = d.backgroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
$hasBg = true;
}
if (d.foregroundColorRGB) {
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
$fg = d.foregroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
$hasFg = true;
}
});
@@ -77,10 +79,94 @@ export class CellColorResolver {
// Apply the selection color if needed
$isSelected = this._selectionRenderModel.isCellSelected(this._terminal, x, y);
if ($isSelected) {
$bg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
// If the cell has a bg color, retain the color by blending it with the selection color
if (
(this.result.fg & FgFlags.INVERSE) ||
(this.result.bg & Attributes.CM_MASK) !== Attributes.CM_DEFAULT
) {
// Resolve the standard bg color
if (this.result.fg & FgFlags.INVERSE) {
switch (this.result.fg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
$bg = this._themeService.colors.ansi[this.result.fg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$bg = ((this.result.fg & Attributes.RGB_MASK) << 8) | 0xFF;
break;
case Attributes.CM_DEFAULT:
default:
$bg = this._themeService.colors.foreground.rgba;
}
} else {
switch (this.result.bg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
$bg = this._themeService.colors.ansi[this.result.bg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$bg = ((this.result.bg & Attributes.RGB_MASK) << 8) | 0xFF;
break;
// No need to consider default bg color here as it's not possible
}
}
// Blend with selection bg color
$bg = rgba.blend(
$bg,
((this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba & 0xFFFFFF00) | 0x80
) >> 8 & Attributes.RGB_MASK;
} else {
$bg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & Attributes.RGB_MASK;
}
$hasBg = true;
// Apply explicit selection foreground if present
if ($colors.selectionForeground) {
$fg = $colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
$fg = $colors.selectionForeground.rgba >> 8 & Attributes.RGB_MASK;
$hasFg = true;
}
// Overwrite fg as bg if it's a special decorative glyph (eg. powerline)
if (treatGlyphAsBackgroundColor(cell.getCode())) {
// Inverse default background should be treated as transparent
if (
(this.result.fg & FgFlags.INVERSE) &&
(this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT
) {
$fg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & Attributes.RGB_MASK;
} else {
if (this.result.fg & FgFlags.INVERSE) {
switch (this.result.bg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
$fg = this._themeService.colors.ansi[this.result.bg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$fg = ((this.result.bg & Attributes.RGB_MASK) << 8) | 0xFF;
break;
// No need to consider default bg color here as it's not possible
}
} else {
switch (this.result.fg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
$fg = this._themeService.colors.ansi[this.result.fg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$fg = ((this.result.fg & Attributes.RGB_MASK) << 8) | 0xFF;
break;
case Attributes.CM_DEFAULT:
default:
$fg = this._themeService.colors.foreground.rgba;
}
}
$fg = rgba.blend(
$fg,
((this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba & 0xFFFFFF00) | 0x80
) >> 8 & Attributes.RGB_MASK;
}
$hasFg = true;
}
}
@@ -88,11 +174,11 @@ export class CellColorResolver {
// Apply decorations on the top layer
this._decorationService.forEachDecorationAtCell(x, y, 'top', d => {
if (d.backgroundColorRGB) {
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
$bg = d.backgroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
$hasBg = true;
}
if (d.foregroundColorRGB) {
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
$fg = d.foregroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
$hasFg = true;
}
});
@@ -119,7 +205,7 @@ export class CellColorResolver {
if ($hasBg && !$hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | (($colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | (($colors.background.rgba >> 8 & Attributes.RGB_MASK) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this.result.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
@@ -128,7 +214,7 @@ export class CellColorResolver {
if (!$hasBg && $hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this.result.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | (($colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | (($colors.foreground.rgba >> 8 & Attributes.RGB_MASK) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this.result.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
@@ -355,6 +355,12 @@ const enum VectorType {
* Original symbols defined in https://github.com/powerline/fontpatcher
*/
export const powerlineDefinitions: { [index: string]: IVectorShape } = {
// Git branch
'\u{E0A0}': { d: 'M.3,1 L.03,1 L.03,.88 C.03,.82,.06,.78,.11,.73 C.15,.7,.2,.68,.28,.65 L.43,.6 C.49,.58,.53,.56,.56,.53 C.59,.5,.6,.47,.6,.43 L.6,.27 L.4,.27 L.69,.1 L.98,.27 L.78,.27 L.78,.46 C.78,.52,.76,.56,.72,.61 C.68,.66,.63,.67,.56,.7 L.48,.72 C.42,.74,.38,.76,.35,.78 C.32,.8,.31,.84,.31,.88 L.31,1 M.3,.5 L.03,.59 L.03,.09 L.3,.09 L.3,.655', type: VectorType.FILL },
// L N
'\u{E0A1}': { d: 'M.7,.4 L.7,.47 L.2,.47 L.2,.03 L.355,.03 L.355,.4 L.705,.4 M.7,.5 L.86,.5 L.86,.95 L.69,.95 L.44,.66 L.46,.86 L.46,.95 L.3,.95 L.3,.49 L.46,.49 L.71,.78 L.69,.565 L.69,.5', type: VectorType.FILL },
// Lock
'\u{E0A2}': { d: 'M.25,.94 C.16,.94,.11,.92,.11,.87 L.11,.53 C.11,.48,.15,.455,.23,.45 L.23,.3 C.23,.25,.26,.22,.31,.19 C.36,.16,.43,.15,.51,.15 C.59,.15,.66,.16,.71,.19 C.77,.22,.79,.26,.79,.3 L.79,.45 C.87,.45,.91,.48,.91,.53 L.91,.87 C.91,.92,.86,.94,.77,.94 L.24,.94 M.53,.2 C.49,.2,.45,.21,.42,.23 C.39,.25,.38,.27,.38,.3 L.38,.45 L.68,.45 L.68,.3 C.68,.27,.67,.25,.64,.23 C.61,.21,.58,.2,.53,.2 M.58,.82 L.58,.66 C.63,.65,.65,.63,.65,.6 C.65,.58,.64,.57,.61,.56 C.58,.55,.56,.54,.52,.54 C.48,.54,.46,.55,.43,.56 C.4,.57,.39,.59,.39,.6 C.39,.63,.41,.64,.46,.66 L.46,.82 L.57,.82', type: VectorType.FILL },
// Right triangle solid
'\u{E0B0}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.FILL, rightPadding: 2 },
// Right triangle line
+34 -1
View File
@@ -23,11 +23,44 @@ export function isRestrictedPowerlineGlyph(codepoint: number): boolean {
return 0xE0B0 <= codepoint && codepoint <= 0xE0B7;
}
function isNerdFontGlyph(codepoint: number): boolean {
return 0xE000 <= codepoint && codepoint <= 0xF8FF;
}
function isBoxOrBlockGlyph(codepoint: number): boolean {
return 0x2500 <= codepoint && codepoint <= 0x259F;
}
export function excludeFromContrastRatioDemands(codepoint: number): boolean {
export function isEmoji(codepoint: number): boolean {
return (
codepoint >= 0x1F600 && codepoint <= 0x1F64F || // Emoticons
codepoint >= 0x1F300 && codepoint <= 0x1F5FF || // Misc Symbols and Pictographs
codepoint >= 0x1F680 && codepoint <= 0x1F6FF || // Transport and Map
codepoint >= 0x2600 && codepoint <= 0x26FF || // Misc symbols
codepoint >= 0x2700 && codepoint <= 0x27BF || // Dingbats
codepoint >= 0xFE00 && codepoint <= 0xFE0F || // Variation Selectors
codepoint >= 0x1F900 && codepoint <= 0x1F9FF || // Supplemental Symbols and Pictographs
codepoint >= 0x1F1E6 && codepoint <= 0x1F1FF
);
}
export function allowRescaling(codepoint: number | undefined, width: number, glyphSizeX: number, deviceCellWidth: number): boolean {
return (
// Is single cell width
width === 1 &&
// Glyph exceeds cell bounds, add 50% to avoid hurting readability by rescaling glyphs that
// barely overlap
glyphSizeX > Math.ceil(deviceCellWidth * 1.5) &&
// Never rescale ascii
codepoint !== undefined && codepoint > 0xFF &&
// Never rescale emoji
!isEmoji(codepoint) &&
// Never rescale powerline or nerd fonts
!isPowerlineGlyph(codepoint) && !isNerdFontGlyph(codepoint)
);
}
export function treatGlyphAsBackgroundColor(codepoint: number): boolean {
return isPowerlineGlyph(codepoint) || isBoxOrBlockGlyph(codepoint);
}
+6 -9
View File
@@ -6,16 +6,15 @@
import { IColorContrastCache } from 'browser/Types';
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
import { computeNextVariantOffset, excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { computeNextVariantOffset, treatGlyphAsBackgroundColor, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types';
import { NULL_COLOR, color, rgba } from 'common/Color';
import { NULL_COLOR, channels, color, rgba } from 'common/Color';
import { EventEmitter } from 'common/EventEmitter';
import { FourKeyMap } from 'common/MultiKeyMap';
import { IdleTaskQueue } from 'common/TaskQueue';
import { IColor } from 'common/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
import { traceCall } from 'common/services/LogService';
import { IUnicodeService } from 'common/services/Services';
/**
@@ -292,8 +291,7 @@ export class TextureAtlas implements ITextureAtlas {
break;
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(bgColor);
// TODO: This object creation is slow
result = rgba.toColor(arr[0], arr[1], arr[2]);
result = channels.toColor(arr[0], arr[1], arr[2]);
break;
case Attributes.CM_DEFAULT:
default:
@@ -325,7 +323,7 @@ export class TextureAtlas implements ITextureAtlas {
break;
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(fgColor);
result = rgba.toColor(arr[0], arr[1], arr[2]);
result = channels.toColor(arr[0], arr[1], arr[2]);
break;
case Attributes.CM_DEFAULT:
default:
@@ -407,7 +405,7 @@ export class TextureAtlas implements ITextureAtlas {
return undefined;
}
const color = rgba.toColor(
const color = channels.toColor(
(result >> 24) & 0xFF,
(result >> 16) & 0xFF,
(result >> 8) & 0xFF
@@ -424,7 +422,6 @@ export class TextureAtlas implements ITextureAtlas {
return this._config.colors.contrastCache;
}
@traceCall
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean = false): IRasterizedGlyph {
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
@@ -492,7 +489,7 @@ export class TextureAtlas implements ITextureAtlas {
const powerlineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
const restrictedPowerlineGlyph = chars.length === 1 && isRestrictedPowerlineGlyph(chars.charCodeAt(0));
const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, dim, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));
const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, dim, bold, treatGlyphAsBackgroundColor(chars.charCodeAt(0)));
this._tmpCtx.fillStyle = foregroundColor.css;
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
+55 -30
View File
@@ -8,12 +8,6 @@ import { EventEmitter } from 'common/EventEmitter';
import { ICharSizeService } from 'browser/services/Services';
import { Disposable } from 'common/Lifecycle';
const enum MeasureSettings {
REPEAT = 32
}
export class CharSizeService extends Disposable implements ICharSizeService {
public serviceBrand: undefined;
@@ -32,7 +26,11 @@ export class CharSizeService extends Disposable implements ICharSizeService {
@IOptionsService private readonly _optionsService: IOptionsService
) {
super();
this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService);
try {
this._measureStrategy = this.register(new TextMetricsMeasureStrategy(this._optionsService));
} catch {
this._measureStrategy = this.register(new DomMeasureStrategy(document, parentElement, this._optionsService));
}
this.register(this._optionsService.onMultipleOptionChange(['fontFamily', 'fontSize'], () => this.measure()));
}
@@ -47,12 +45,7 @@ export class CharSizeService extends Disposable implements ICharSizeService {
}
interface IMeasureStrategy {
measure(): IReadonlyMeasureResult;
}
interface IReadonlyMeasureResult {
readonly width: number;
readonly height: number;
measure(): Readonly<IMeasureResult>;
}
interface IMeasureResult {
@@ -60,10 +53,26 @@ interface IMeasureResult {
height: number;
}
// TODO: For supporting browsers we should also provide a CanvasCharDimensionsProvider that uses
// ctx.measureText
class DomMeasureStrategy implements IMeasureStrategy {
private _result: IMeasureResult = { width: 0, height: 0 };
const enum DomMeasureStrategyConstants {
REPEAT = 32
}
abstract class BaseMeasureStategy extends Disposable implements IMeasureStrategy {
protected _result: IMeasureResult = { width: 0, height: 0 };
protected _validateAndSet(width: number | undefined, height: number | undefined): void {
// If values are 0 then the element is likely currently display:none, in which case we should
// retain the previous value.
if (width !== undefined && width > 0 && height !== undefined && height > 0) {
this._result.width = width;
this._result.height = height;
}
}
public abstract measure(): Readonly<IMeasureResult>;
}
class DomMeasureStrategy extends BaseMeasureStategy {
private _measureElement: HTMLElement;
constructor(
@@ -71,32 +80,48 @@ class DomMeasureStrategy implements IMeasureStrategy {
private _parentElement: HTMLElement,
private _optionsService: IOptionsService
) {
super();
this._measureElement = this._document.createElement('span');
this._measureElement.classList.add('xterm-char-measure-element');
this._measureElement.textContent = 'W'.repeat(MeasureSettings.REPEAT);
this._measureElement.textContent = 'W'.repeat(DomMeasureStrategyConstants.REPEAT);
this._measureElement.setAttribute('aria-hidden', 'true');
this._measureElement.style.whiteSpace = 'pre';
this._measureElement.style.fontKerning = 'none';
this._parentElement.appendChild(this._measureElement);
}
public measure(): IReadonlyMeasureResult {
public measure(): Readonly<IMeasureResult> {
this._measureElement.style.fontFamily = this._optionsService.rawOptions.fontFamily;
this._measureElement.style.fontSize = `${this._optionsService.rawOptions.fontSize}px`;
// Note that this triggers a synchronous layout
const geometry = {
height: Number(this._measureElement.offsetHeight),
width: Number(this._measureElement.offsetWidth)
};
// If values are 0 then the element is likely currently display:none, in which case we should
// retain the previous value.
if (geometry.width !== 0 && geometry.height !== 0) {
this._result.width = geometry.width / MeasureSettings.REPEAT;
this._result.height = Math.ceil(geometry.height);
}
this._validateAndSet(Number(this._measureElement.offsetWidth) / DomMeasureStrategyConstants.REPEAT, Number(this._measureElement.offsetHeight));
return this._result;
}
}
class TextMetricsMeasureStrategy extends BaseMeasureStategy {
private _canvas: OffscreenCanvas;
private _ctx: OffscreenCanvasRenderingContext2D;
constructor(
private _optionsService: IOptionsService
) {
super();
// This will throw if any required API is not supported
this._canvas = new OffscreenCanvas(100, 100);
this._ctx = this._canvas.getContext('2d')!;
const a = this._ctx.measureText('W');
if (!('width' in a && 'fontBoundingBoxAscent' in a && 'fontBoundingBoxDescent' in a)) {
throw new Error('Required font metrics not supported');
}
}
public measure(): Readonly<IMeasureResult> {
this._ctx.font = `${this._optionsService.rawOptions.fontSize}px ${this._optionsService.rawOptions.fontFamily}`;
const metrics = this._ctx.measureText('W');
this._validateAndSet(metrics.width, metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent);
return this._result;
}
}
+7 -3
View File
@@ -13,7 +13,7 @@ export class CoreBrowserService extends Disposable implements ICoreBrowserServic
private _isFocused = false;
private _cachedIsFocused: boolean | undefined = undefined;
private _screenDprMonitor = new ScreenDprMonitor(this._window);
private _screenDprMonitor = this.register(new ScreenDprMonitor(this._window));
private readonly _onDprChange = this.register(new EventEmitter<number>());
public readonly onDprChange = this._onDprChange.event;
@@ -31,8 +31,12 @@ export class CoreBrowserService extends Disposable implements ICoreBrowserServic
this.register(this.onWindowChange(w => this._screenDprMonitor.setWindow(w)));
this.register(forwardEvent(this._screenDprMonitor.onDprChange, this._onDprChange));
this._textarea.addEventListener('focus', () => this._isFocused = true);
this._textarea.addEventListener('blur', () => this._isFocused = false);
this.register(
addDisposableDomListener(this._textarea, 'focus', () => (this._isFocused = true))
);
this.register(
addDisposableDomListener(this._textarea, 'blur', () => (this._isFocused = false))
);
}
public get window(): Window & typeof globalThis {
@@ -0,0 +1,28 @@
import { ILinkProvider, ILinkProviderService } from 'browser/services/Services';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IDisposable } from 'common/Types';
export class LinkProviderService extends Disposable implements ILinkProviderService {
declare public serviceBrand: undefined;
public readonly linkProviders: ILinkProvider[] = [];
constructor() {
super();
this.register(toDisposable(() => this.linkProviders.length = 0));
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
this.linkProviders.push(linkProvider);
return {
dispose: () => {
// Remove the link provider from the list
const providerIndex = this.linkProviders.indexOf(linkProvider);
if (providerIndex !== -1) {
this.linkProviders.splice(providerIndex, 1);
}
}
};
}
}
+16 -10
View File
@@ -8,9 +8,9 @@ import { IRenderDebouncerWithCallback } from 'browser/Types';
import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { EventEmitter } from 'common/EventEmitter';
import { Disposable, MutableDisposable } from 'common/Lifecycle';
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
import { DebouncedIdleTask } from 'common/TaskQueue';
import { IBufferService, IDecorationService, IInstantiationService, IOptionsService } from 'common/services/Services';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
interface ISelectionState {
start: [number, number] | undefined;
@@ -24,6 +24,7 @@ export class RenderService extends Disposable implements IRenderService {
private _renderer: MutableDisposable<IRenderer> = this.register(new MutableDisposable());
private _renderDebouncer: IRenderDebouncerWithCallback;
private _pausedResizeTask = new DebouncedIdleTask();
private _observerDisposable = this.register(new MutableDisposable());
private _isPaused: boolean = false;
private _needsFullRefresh: boolean = false;
@@ -38,7 +39,7 @@ export class RenderService extends Disposable implements IRenderService {
};
private readonly _onDimensionsChange = this.register(new EventEmitter<IRenderDimensions>());
public readonly onDimensionsChange = this._onDimensionsChange.event;
public readonly onDimensionsChange = this._onDimensionsChange.event;
private readonly _onRenderedViewportChange = this.register(new EventEmitter<{ start: number, end: number }>());
public readonly onRenderedViewportChange = this._onRenderedViewportChange.event;
private readonly _onRender = this.register(new EventEmitter<{ start: number, end: number }>());
@@ -56,12 +57,11 @@ export class RenderService extends Disposable implements IRenderService {
@IDecorationService decorationService: IDecorationService,
@IBufferService bufferService: IBufferService,
@ICoreBrowserService coreBrowserService: ICoreBrowserService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService
) {
super();
this._renderDebouncer = new RenderDebouncer(coreBrowserService.window, (start, end) => this._renderRows(start, end));
this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end), coreBrowserService);
this.register(this._renderDebouncer);
this.register(coreBrowserService.onDprChange(() => this.handleDevicePixelRatioChange()));
@@ -87,7 +87,8 @@ export class RenderService extends Disposable implements IRenderService {
'fontSize',
'fontWeight',
'fontWeightBold',
'minimumContrastRatio'
'minimumContrastRatio',
'rescaleOverlappingGlyphs'
], () => {
this.clear();
this.handleResize(bufferService.cols, bufferService.rows);
@@ -102,12 +103,17 @@ export class RenderService extends Disposable implements IRenderService {
this.register(themeService.onChangeColors(() => this._fullRefresh()));
this._registerIntersectionObserver(coreBrowserService.window, screenElement);
this.register(coreBrowserService.onWindowChange((w) => this._registerIntersectionObserver(w, screenElement)));
}
private _registerIntersectionObserver(w: Window & typeof globalThis, screenElement: HTMLElement): void {
// Detect whether IntersectionObserver is detected and enable renderer pause
// and resume based on terminal visibility if so
if ('IntersectionObserver' in coreBrowserService.window) {
const observer = new coreBrowserService.window.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 });
if ('IntersectionObserver' in w) {
const observer = new w.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 });
observer.observe(screenElement);
this.register({ dispose: () => observer.disconnect() });
this._observerDisposable.value = toDisposable(() => observer.disconnect());
}
}
@@ -242,7 +248,7 @@ export class RenderService extends Disposable implements IRenderService {
return;
}
if (this._isPaused) {
this._pausedResizeTask.set(() => this._renderer.value!.handleResize(cols, rows));
this._pausedResizeTask.set(() => this._renderer.value?.handleResize(cols, rows));
} else {
this._renderer.value.handleResize(cols, rows);
}
+12 -1
View File
@@ -5,7 +5,7 @@
import { IEvent } from 'common/EventEmitter';
import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { IColorSet, ILink, ReadonlyColorSet } from 'browser/Types';
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
import { AllColorIndex, IDisposable } from 'common/Types';
@@ -145,3 +145,14 @@ export interface IThemeService {
*/
modifyColors(callback: (colors: IColorSet) => void): void;
}
export const ILinkProviderService = createDecorator<ILinkProviderService>('LinkProviderService');
export interface ILinkProviderService extends IDisposable {
serviceBrand: undefined;
readonly linkProviders: ReadonlyArray<ILinkProvider>;
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
}
export interface ILinkProvider {
provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void;
}