Merge remote-tracking branch 'ups/master' into pr/joyceerhl/3214

This commit is contained in:
Daniel Imms
2021-07-22 14:33:17 -07:00
123 changed files with 7696 additions and 4499 deletions
+1 -1
View File
@@ -112,7 +112,7 @@ export class AccessibilityManager extends Disposable {
}
private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
const boundaryElement = <HTMLElement>e.target;
const boundaryElement = e.target as HTMLElement;
const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2];
// Don't scroll if the buffer top has reached the end in that direction
+2 -2
View File
@@ -89,8 +89,8 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, screenElement: HTMLElement, selectionService: ISelectionService, shouldSelectWord: boolean): void {
moveTextAreaUnderMouseCursor(ev, textarea, screenElement);
if (shouldSelectWord && !selectionService.isClickInSelection(ev)) {
selectionService.selectWordAtCursor(ev);
if (shouldSelectWord) {
selectionService.rightClickSelect(ev);
}
// Get textarea ready to copy from the context menu
+2 -2
View File
@@ -17,7 +17,7 @@ describe('ColorManager', () => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
(<any>window).HTMLCanvasElement.prototype.getContext = () => ({
(window as any).HTMLCanvasElement.prototype.getContext = () => ({
createLinearGradient(): any {
return null;
},
@@ -36,7 +36,7 @@ describe('ColorManager', () => {
for (const key of Object.keys(cm.colors)) {
if (key !== 'ansi' && key !== 'contrastCache') {
// A #rrggbb or rgba(...)
assert.ok((<any>cm.colors)[key].css.length >= 7);
assert.ok((cm.colors as any)[key].css.length >= 7);
}
}
assert.equal(cm.colors.ansi.length, 256);
+3 -4
View File
@@ -17,9 +17,8 @@ const DEFAULT_SELECTION = {
rgba: 0xFFFFFF4D
};
// An IIFE to generate DEFAULT_ANSI_COLORS. Do not mutate DEFAULT_ANSI_COLORS, instead make a copy
// and mutate that.
export const DEFAULT_ANSI_COLORS = (() => {
// An IIFE to generate DEFAULT_ANSI_COLORS.
export const DEFAULT_ANSI_COLORS = Object.freeze((() => {
const colors = [
// dark:
css.toColor('#2e3436'),
@@ -64,7 +63,7 @@ export const DEFAULT_ANSI_COLORS = (() => {
}
return colors;
})();
})());
/**
* Manages the source of truth for a terminal's colors.
+2 -2
View File
@@ -174,7 +174,7 @@ describe('Linkifier', () => {
assert.equal(mouseZoneManager.zones[0].y1, 1);
assert.equal(mouseZoneManager.zones[0].y2, 1);
// Fires done()
mouseZoneManager.zones[0].clickCallback(<any>{});
mouseZoneManager.zones[0].clickCallback({} as any);
}
});
linkifier.linkifyRows();
@@ -210,7 +210,7 @@ describe('Linkifier', () => {
let count = 0;
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
validationCallback: (url, cb) => {
count += 1;
count++;
if (count === 2) {
done();
}
+3 -1
View File
@@ -89,7 +89,9 @@ export class Linkifier implements ILinkifier {
if (this._rowsTimeoutId) {
clearTimeout(this._rowsTimeoutId);
}
this._rowsTimeoutId = <number><any>setTimeout(() => this._linkifyRows(), Linkifier._timeBeforeLatency);
// Cannot use window.setTimeout since tests need to run in node
this._rowsTimeoutId = setTimeout(() => this._linkifyRows(), Linkifier._timeBeforeLatency) as any as number;
}
/**
+2 -11
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations } from 'browser/Types';
import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations, ILinkWithState } from 'browser/Types';
import { IDisposable } from 'common/Types';
import { IMouseService, IRenderService } from './services/Services';
import { IBufferService } from 'common/services/Services';
@@ -11,21 +11,12 @@ import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable, getDisposeArrayDisposable, disposeArray } from 'common/Lifecycle';
import { addDisposableDomListener } from 'browser/Lifecycle';
interface ILinkState {
decorations: ILinkDecorations;
isHovered: boolean;
}
interface ILinkWithState {
link: ILink;
state?: ILinkState;
}
export class Linkifier2 extends Disposable implements ILinkifier2 {
private _element: HTMLElement | undefined;
private _mouseService: IMouseService | undefined;
private _renderService: IRenderService | undefined;
private _linkProviders: ILinkProvider[] = [];
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
protected _currentLink: ILinkWithState | undefined;
private _lastMouseEvent: MouseEvent | undefined;
private _linkCacheDisposables: IDisposable[] = [];
+1 -3
View File
@@ -156,9 +156,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
private _onTooltip(e: MouseEvent): void {
this._tooltipTimeout = undefined;
const zone = this._findZoneEventAt(e);
if (zone && zone.tooltipCallback) {
zone.tooltipCallback(e);
}
zone?.tooltipCallback(e);
}
private _onMouseDown(e: MouseEvent): void {
File diff suppressed because it is too large Load Diff
+74 -36
View File
@@ -21,8 +21,8 @@
* http://linux.die.net/man/7/urxvt
*/
import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport, ILinkifier2 } from 'browser/Types';
import { IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types';
import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport, ILinkifier2, CharacterJoinerHandler } from 'browser/Types';
import { IRenderer } from 'browser/renderer/Types';
import { CompositionHelper } from 'browser/input/CompositionHelper';
import { Viewport } from 'browser/Viewport';
import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard';
@@ -39,13 +39,13 @@ import { MouseZoneManager } from 'browser/MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager';
import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider } from 'xterm';
import { DomRenderer } from 'browser/renderer/dom/DomRenderer';
import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions } from 'common/Types';
import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IAnsiColorChangeEvent } from 'common/Types';
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { ColorManager } from 'browser/ColorManager';
import { RenderService } from 'browser/services/RenderService';
import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService } from 'browser/services/Services';
import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService, ICharacterJoinerService } from 'browser/services/Services';
import { CharSizeService } from 'browser/services/CharSizeService';
import { IBuffer } from 'common/buffer/Types';
import { MouseService } from 'browser/services/MouseService';
@@ -53,6 +53,8 @@ import { Linkifier2 } from 'browser/Linkifier2';
import { CoreBrowserService } from 'browser/services/CoreBrowserService';
import { CoreTerminal } from 'common/CoreTerminal';
import { ITerminalOptions as IInitializedTerminalOptions } from 'common/services/Services';
import { rgba } from 'browser/Color';
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
// Let it work inside Node.js for automated testing purposes.
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;
@@ -70,7 +72,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
// private _visualBellTimer: number;
public browser: IBrowser = <any>Browser;
public browser: IBrowser = Browser as any;
// TODO: We should remove options once components adopt optionsService
public get options(): IInitializedTerminalOptions { return this.optionsService.options; }
@@ -81,6 +83,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _charSizeService: ICharSizeService | undefined;
private _mouseService: IMouseService | undefined;
private _renderService: IRenderService | undefined;
private _characterJoinerService: ICharacterJoinerService | undefined;
private _selectionService: ISelectionService | undefined;
private _soundService: ISoundService | undefined;
@@ -110,6 +113,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }
private _onTitleChange = new EventEmitter<string>();
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
private _onBell = new EventEmitter<void>();
public get onBell (): IEvent<void> { return this._onBell.event; }
private _onFocus = new EventEmitter<void>();
public get onFocus(): IEvent<void> { return this._onFocus.event; }
@@ -146,8 +151,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.register(this._inputHandler.onRequestBell(() => this.bell()));
this.register(this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end)));
this.register(this._inputHandler.onRequestReset(() => this.reset()));
this.register(this._inputHandler.onRequestScroll((eraseAttr, isWrapped) => this.scroll(eraseAttr, isWrapped || undefined)));
this.register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type)));
this.register(this._inputHandler.onAnsiColorChange((event) => this._changeAnsiColor(event)));
this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove));
this.register(forwardEvent(this._inputHandler.onTitleChange, this._onTitleChange));
this.register(forwardEvent(this._inputHandler.onA11yChar, this._onA11yCharEmitter));
@@ -157,6 +162,19 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows)));
}
private _changeAnsiColor(event: IAnsiColorChangeEvent): void {
if (!this._colorManager) { return; }
for (const ansiColor of event.colors) {
const color = rgba.toColor(ansiColor.red, ansiColor.green, ansiColor.blue);
this._colorManager!.colors.ansi[ansiColor.colorIndex] = color;
}
this._renderService?.setColors(this._colorManager!.colors);
this.viewport?.onThemeChange(this._colorManager!.colors);
}
public dispose(): void {
if (this._isDisposed) {
return;
@@ -282,19 +300,26 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
private _syncTextArea(): void {
if (!this.textarea || !this.buffer.isCursorInViewport || this._compositionHelper!.isComposing) {
if (!this.textarea || !this.buffer.isCursorInViewport || this._compositionHelper!.isComposing || !this._renderService) {
return;
}
const cellHeight = Math.ceil(this._charSizeService!.height * this.optionsService.options.lineHeight);
const cursorTop = this._bufferService.buffer.y * cellHeight;
const cursorLeft = this._bufferService.buffer.x * this._charSizeService!.width;
const cursorY = this.buffer.ybase + this.buffer.y;
const bufferLine = this.buffer.lines.get(cursorY);
if (!bufferLine) {
return;
}
const cursorX = Math.min(this.buffer.x, this.cols - 1);
const cellHeight = this._renderService.dimensions.actualCellHeight;
const width = bufferLine.getWidth(cursorX);
const cellWidth = this._renderService.dimensions.actualCellWidth * width;
const cursorTop = this.buffer.y * this._renderService.dimensions.actualCellHeight;
const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth;
// Sync the textarea to the exact position of the composition view so the IME knows where the
// text is.
this.textarea.style.left = cursorLeft + 'px';
this.textarea.style.top = cursorTop + 'px';
this.textarea.style.width = this._charSizeService!.width + 'px';
this.textarea.style.width = cellWidth + 'px';
this.textarea.style.height = cellHeight + 'px';
this.textarea.style.lineHeight = cellHeight + 'px';
this.textarea.style.zIndex = '-5';
@@ -371,7 +396,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
throw new Error('Terminal requires a parent element.');
}
if (!document.body.contains(parent)) {
if (!parent.isConnected) {
this._logService.debug('Terminal.open was called on an element that was not attached to the DOM');
}
@@ -423,6 +448,20 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
this._instantiationService.setService(ICharSizeService, this._charSizeService);
this._theme = this.options.theme || this._theme;
this._colorManager = new ColorManager(document, this.options.allowTransparency);
this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e)));
this._colorManager.setTheme(this._theme);
this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService);
this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService);
const renderer = this._createRenderer();
this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement));
this._instantiationService.setService(IRenderService, this._renderService);
this.register(this._renderService.onRenderedBufferChange(e => this._onRender.fire(e)));
this.onResize(e => this._renderService!.resize(e.cols, e.rows));
this._compositionView = document.createElement('div');
this._compositionView.classList.add('composition-view');
this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
@@ -431,24 +470,13 @@ export class Terminal extends CoreTerminal implements ITerminal {
// Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment);
this._theme = this.options.theme || this._theme;
this._colorManager = new ColorManager(document, this.options.allowTransparency);
this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e)));
this._colorManager.setTheme(this._theme);
const renderer = this._createRenderer();
this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement));
this._instantiationService.setService(IRenderService, this._renderService);
this.register(this._renderService.onRenderedBufferChange(e => this._onRender.fire(e)));
this.onResize(e => this._renderService!.resize(e.cols, e.rows));
this._soundService = this._instantiationService.createInstance(SoundService);
this._instantiationService.setService(ISoundService, this._soundService);
this._mouseService = this._instantiationService.createInstance(MouseService);
this._instantiationService.setService(IMouseService, this._mouseService);
this.viewport = this._instantiationService.createInstance(Viewport,
(amount: number, suppressEvent: boolean) => this.scrollLines(amount, suppressEvent),
(amount: number) => this.scrollLines(amount, true, ScrollSource.VIEWPORT),
this._viewportElement,
this._viewportScrollArea
);
@@ -467,7 +495,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._selectionService = this.register(this._instantiationService.createInstance(SelectionService,
this.element,
this.screenElement));
this.screenElement,
this.linkifier2
));
this._instantiationService.setService(ISelectionService, this._selectionService);
this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire()));
@@ -480,7 +510,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.textarea!.focus();
this.textarea!.select();
}));
this.register(this.onScroll(() => {
this.register(this._onScroll.event(ev => {
this.viewport!.syncScrollArea();
this._selectionService!.refresh();
}));
@@ -571,7 +601,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
let but: CoreMouseButton;
let action: CoreMouseAction | undefined;
switch ((<any>ev).overrideType || ev.type) {
switch ((ev as any).overrideType || ev.type) {
case 'mousemove':
action = CoreMouseAction.MOVE;
if (ev.buttons === undefined) {
@@ -819,7 +849,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
* Change the cursor style for different selection modes
*/
public updateCursorStyle(ev: KeyboardEvent): void {
if (this._selectionService && this._selectionService.shouldColumnSelect(ev)) {
if (this._selectionService?.shouldColumnSelect(ev)) {
this.element!.classList.add('column-select');
} else {
this.element!.classList.remove('column-select');
@@ -836,8 +866,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
}
public scrollLines(disp: number, suppressScrollEvent?: boolean): void {
super.scrollLines(disp, suppressScrollEvent);
public scrollLines(disp: number, suppressScrollEvent?: boolean, source = ScrollSource.TERMINAL): void {
super.scrollLines(disp, suppressScrollEvent, source);
this.refresh(0, this.rows - 1);
}
@@ -889,13 +919,19 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
const joinerId = this._renderService!.registerCharacterJoiner(handler);
if (!this._characterJoinerService) {
throw new Error('Terminal must be opened first');
}
const joinerId = this._characterJoinerService.register(handler);
this.refresh(0, this.rows - 1);
return joinerId;
}
public deregisterCharacterJoiner(joinerId: number): void {
if (this._renderService!.deregisterCharacterJoiner(joinerId)) {
if (!this._characterJoinerService) {
throw new Error('Terminal must be opened first');
}
if (this._characterJoinerService.deregister(joinerId)) {
this.refresh(0, this.rows - 1);
}
}
@@ -984,7 +1020,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
if (!this._compositionHelper!.keydown(event)) {
if (this.buffer.ybase !== this.buffer.ydisp) {
this.scrollToBottom();
this._bufferService.scrollToBottom();
}
return false;
}
@@ -1113,9 +1149,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
*/
public bell(): void {
if (this._soundBell()) {
this._soundService!.playBellSound();
this._soundService?.playBellSound();
}
this._onBell.fire();
// if (this._visualBell()) {
// this.element.classList.add('visual-bell-active');
// clearTimeout(this._visualBellTimer);
@@ -1168,7 +1206,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
this.refresh(0, this.rows - 1);
this._onScroll.fire(this.buffer.ydisp);
this._onScroll.fire({ position: this.buffer.ydisp, source: ScrollSource.TERMINAL });
}
/**
+4 -3
View File
@@ -17,6 +17,7 @@ const ROWS = 25;
const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '../..')});
const SKIP_FILES = [
't0055-EL.in', // EL/ED handle cursor at cols differently (see #3362)
't0084-CBT.in',
't0101-NLM.in',
't0103-reverse_wrap.in', // not comparable, we deviate from xterm reverse wrap on purpose
@@ -73,8 +74,8 @@ describe('Escape Sequence Files', function(): void {
// register handler to trigger viewport scraping, wait for it to finish
let content = '';
const OSC_CODE = 12345;
await new Promise(resolve => {
customHandler = term.addOscHandler(OSC_CODE, () => {
await new Promise<void>(resolve => {
customHandler = term.registerOscHandler(OSC_CODE, () => {
// grab terminal viewport content
content = terminalToString(term);
resolve();
@@ -105,7 +106,7 @@ function formatError(input: string, output: string, expected: string): string {
function addLineNumber(start: number, color: string): (s: string) => string {
let counter = start || 0;
return (s: string): string => {
counter += 1;
counter++;
return '\x1b[33m' + (' ' + counter).slice(-2) + color + s;
};
}
+25 -16
View File
@@ -5,9 +5,9 @@
import { IDisposable, IMarker, ISelectionPosition, ILinkProvider } from 'xterm';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
import { IRenderDimensions, IRenderer, CharacterJoinerHandler, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper } from 'browser/Types';
import { ICharacterJoinerService, ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler } from 'browser/Types';
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types';
import { Buffer } from 'common/buffer/Buffer';
@@ -21,6 +21,9 @@ export class TestTerminal extends Terminal {
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
public keyDown(ev: any): boolean | undefined { return this._keyDown(ev); }
public keyPress(ev: any): boolean { return this._keyPress(ev); }
public writeP(data: string | Uint8Array): Promise<void> {
return new Promise(r => this.write(data, r));
}
}
export class MockTerminal implements ITerminal {
@@ -34,6 +37,7 @@ export class MockTerminal implements ITerminal {
public onData!: IEvent<string>;
public onBinary!: IEvent<string>;
public onTitleChange!: IEvent<string>;
public onBell!: IEvent<void>;
public onScroll!: IEvent<number>;
public onKey!: IEvent<{ key: string, domEvent: KeyboardEvent }>;
public onRender!: IEvent<{ start: number, end: number }>;
@@ -75,16 +79,16 @@ export class MockTerminal implements ITerminal {
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
throw new Error('Method not implemented.');
}
public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
@@ -149,7 +153,7 @@ export class MockTerminal implements ITerminal {
public textarea!: HTMLTextAreaElement;
public rows!: number;
public cols!: number;
public browser: IBrowser = <any>Browser;
public browser: IBrowser = Browser as any;
public writeBuffer!: string[];
public children!: HTMLElement[];
public cursorHidden!: boolean;
@@ -281,8 +285,6 @@ export class MockRenderer implements IRenderer {
public onDevicePixelRatioChange(): void { }
public clear(): void { }
public renderRows(start: number, end: number): void { }
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
public deregisterCharacterJoiner(): boolean { return true; }
}
export class MockViewport implements IViewport {
@@ -406,13 +408,20 @@ export class MockRenderService implements IRenderService {
public clear(): void {
throw new Error('Method not implemented.');
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
throw new Error('Method not implemented.');
}
public deregisterCharacterJoiner(joinerId: number): boolean {
throw new Error('Method not implemented.');
}
public dispose(): void {
throw new Error('Method not implemented.');
}
}
export class MockCharacterJoinerService implements ICharacterJoinerService {
public serviceBrand: undefined;
public register(handler: (text: string) => [number, number][]): number {
return 0;
}
public deregister(joinerId: number): boolean {
return true;
}
public getJoinedCharacters(row: number): [number, number][] {
return [];
}
}
+22 -4
View File
@@ -47,15 +47,16 @@ export interface IPublicTerminal extends IDisposable {
onRender: IEvent<{ start: number, end: number }>;
onResize: IEvent<{ cols: number, rows: number }>;
onTitleChange: IEvent<string>;
onBell: IEvent<void>;
blur(): void;
focus(): void;
resize(columns: number, rows: number): void;
open(parent: HTMLElement): void;
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable;
addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable;
addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable;
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): void;
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
@@ -205,9 +206,19 @@ export interface ILinkifier {
deregisterLinkMatcher(matcherId: number): boolean;
}
interface ILinkState {
decorations: ILinkDecorations;
isHovered: boolean;
}
export interface ILinkWithState {
link: ILink;
state?: ILinkState;
}
export interface ILinkifier2 {
onShowLinkUnderline: IEvent<ILinkifierEvent>;
onHideLinkUnderline: IEvent<ILinkifierEvent>;
readonly currentLink: ILinkWithState | undefined;
attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void;
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
@@ -292,3 +303,10 @@ interface IBufferCellPosition {
x: number;
y: number;
}
export type CharacterJoinerHandler = (text: string) => [number, number][];
export interface ICharacterJoiner {
id: number;
handler: CharacterJoinerHandler;
}
+4 -2
View File
@@ -33,7 +33,7 @@ export class Viewport extends Disposable implements IViewport {
private _ignoreNextScrollEvent: boolean = false;
constructor(
private readonly _scrollLines: (amount: number, suppressEvent: boolean) => void,
private readonly _scrollLines: (amount: number) => void,
private readonly _viewportElement: HTMLElement,
private readonly _scrollArea: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@@ -151,12 +151,14 @@ export class Viewport extends Disposable implements IViewport {
// Ignore the event if it was flagged to ignore (when the source of the event is from Viewport)
if (this._ignoreNextScrollEvent) {
this._ignoreNextScrollEvent = false;
// Still trigger the scroll so lines get refreshed
this._scrollLines(0);
return;
}
const newRow = Math.round(this._lastScrollTop / this._currentRowHeight);
const diff = newRow - this._bufferService.buffer.ydisp;
this._scrollLines(diff, true);
this._scrollLines(diff);
}
/**
+26 -26
View File
@@ -5,7 +5,7 @@
import { assert } from 'chai';
import { CompositionHelper } from 'browser/input/CompositionHelper';
import { MockCharSizeService } from 'browser/TestUtils.test';
import { MockRenderService } from 'browser/TestUtils.test';
import { MockCoreService, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
describe('CompositionHelper', () => {
@@ -42,14 +42,14 @@ describe('CompositionHelper', () => {
};
handledText = '';
const bufferService = new MockBufferService(10, 5);
compositionHelper = new CompositionHelper(textarea, compositionView, bufferService, new MockOptionsService(), new MockCharSizeService(10, 10), coreService);
compositionHelper = new CompositionHelper(textarea, compositionView, bufferService, new MockOptionsService(), coreService, new MockRenderService());
});
describe('Input', () => {
it('Should insert simple characters', (done) => {
// First character 'ㅇ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate(<CompositionEvent><CompositionEvent>{ data: 'ㅇ' });
compositionHelper.compositionupdate({ data: 'ㅇ' });
textarea.value = 'ㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -57,7 +57,7 @@ describe('CompositionHelper', () => {
assert.equal(handledText, 'ㅇ');
// Second character 'ㅇ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate(<CompositionEvent><CompositionEvent>{ data: 'ㅇ' });
compositionHelper.compositionupdate({ data: 'ㅇ' });
textarea.value = 'ㅇㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -73,13 +73,13 @@ describe('CompositionHelper', () => {
it('Should insert complex characters', (done) => {
// First character '앙'
compositionHelper.compositionstart();
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
compositionHelper.compositionupdate({ data: 'ㅇ' });
textarea.value = 'ㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
compositionHelper.compositionupdate({ data: '아' });
textarea.value = '아';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate(<CompositionEvent>{ data: '앙' });
compositionHelper.compositionupdate({ data: '앙' });
textarea.value = '앙';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -87,13 +87,13 @@ describe('CompositionHelper', () => {
assert.equal(handledText, '앙');
// Second character '앙'
compositionHelper.compositionstart();
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
compositionHelper.compositionupdate({ data: 'ㅇ' });
textarea.value = '앙ㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
compositionHelper.compositionupdate({ data: '아' });
textarea.value = '앙아';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate(<CompositionEvent>{ data: '앙' });
compositionHelper.compositionupdate({ data: '앙' });
textarea.value = '앙앙';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -113,19 +113,19 @@ describe('CompositionHelper', () => {
it('Should insert complex characters that change with following character', (done) => {
// First character '아'
compositionHelper.compositionstart();
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
compositionHelper.compositionupdate({ data: 'ㅇ' });
textarea.value = 'ㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
compositionHelper.compositionupdate({ data: '아' });
textarea.value = '아';
setTimeout(() => { // wait for any textarea updates
// Start second character '아' in first character
compositionHelper.compositionupdate(<CompositionEvent>{ data: '앙' });
compositionHelper.compositionupdate({ data: '앙' });
textarea.value = '앙';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
compositionHelper.compositionstart();
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
compositionHelper.compositionupdate({ data: '아' });
textarea.value = '아아';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -142,14 +142,14 @@ describe('CompositionHelper', () => {
it('Should insert multi-characters compositions', (done) => {
// First character 'だ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'd' });
compositionHelper.compositionupdate({ data: 'd' });
textarea.value = 'd';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だ' });
compositionHelper.compositionupdate({ data: 'だ' });
textarea.value = 'だ';
setTimeout(() => { // wait for any textarea updates
// Second character 'あ'
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だあ' });
compositionHelper.compositionupdate({ data: 'だあ' });
textarea.value = 'だあ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -165,18 +165,18 @@ describe('CompositionHelper', () => {
it('Should insert multi-character compositions that are converted to other characters with the same length', (done) => {
// First character 'だ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'd' });
compositionHelper.compositionupdate({ data: 'd' });
textarea.value = 'd';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だ' });
compositionHelper.compositionupdate({ data: 'だ' });
textarea.value = 'だ';
setTimeout(() => { // wait for any textarea updates
// Second character 'ー'
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だー' });
compositionHelper.compositionupdate({ data: 'だー' });
textarea.value = 'だー';
setTimeout(() => { // wait for any textarea updates
// Convert to katakana 'ダー'
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ダー' });
compositionHelper.compositionupdate({ data: 'ダー' });
textarea.value = 'ダー';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -193,18 +193,18 @@ describe('CompositionHelper', () => {
it('Should insert multi-character compositions that are converted to other characters with different lengths', (done) => {
// First character 'い'
compositionHelper.compositionstart();
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'い' });
compositionHelper.compositionupdate({ data: 'い' });
textarea.value = 'い';
setTimeout(() => { // wait for any textarea updates
// Second character 'ま'
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'いm' });
compositionHelper.compositionupdate({ data: 'いm' });
textarea.value = 'いm';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'いま' });
compositionHelper.compositionupdate({ data: 'いま' });
textarea.value = 'いま';
setTimeout(() => { // wait for any textarea updates
// Convert to kanji '今'
compositionHelper.compositionupdate(<CompositionEvent>{ data: '今' });
compositionHelper.compositionupdate({ data: '今' });
textarea.value = '今';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -221,7 +221,7 @@ describe('CompositionHelper', () => {
it('Should insert non-composition characters input immediately after composition characters', (done) => {
// First character 'ㅇ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
compositionHelper.compositionupdate({ data: 'ㅇ' });
textarea.value = 'ㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
+26 -10
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ICharSizeService } from 'browser/services/Services';
import { IRenderService } from 'browser/services/Services';
import { IBufferService, ICoreService, IOptionsService } from 'common/services/Services';
interface IPosition {
@@ -35,17 +35,23 @@ export class CompositionHelper {
*/
private _isSendingComposition: boolean;
/**
* Data already sent due to keydown event.
*/
private _dataAlreadySent: string;
constructor(
private readonly _textarea: HTMLTextAreaElement,
private readonly _compositionView: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@ICoreService private readonly _coreService: ICoreService
@ICoreService private readonly _coreService: ICoreService,
@IRenderService private readonly _renderService: IRenderService
) {
this._isComposing = false;
this._isSendingComposition = false;
this._compositionPosition = { start: 0, end: 0 };
this._dataAlreadySent = '';
}
/**
@@ -55,6 +61,7 @@ export class CompositionHelper {
this._isComposing = true;
this._compositionPosition.start = this._textarea.value.length;
this._compositionView.textContent = '';
this._dataAlreadySent = '';
this._compositionView.classList.add('active');
}
@@ -62,7 +69,7 @@ export class CompositionHelper {
* Handles the compositionupdate event, updating the composition view.
* @param ev The event.
*/
public compositionupdate(ev: CompositionEvent): void {
public compositionupdate(ev: Pick<CompositionEvent, 'data'>): void {
this._compositionView.textContent = ev.data;
this.updateCompositionElements();
setTimeout(() => {
@@ -147,6 +154,9 @@ export class CompositionHelper {
if (this._isSendingComposition) {
this._isSendingComposition = false;
let input;
// Add length of data already sent due to keydown event,
// 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);
@@ -156,7 +166,9 @@ export class CompositionHelper {
// (eg. 2) after a composition character.
input = this._textarea.value.substring(currentCompositionPosition.start);
}
this._coreService.triggerDataEvent(input, true);
if (input.length > 0) {
this._coreService.triggerDataEvent(input, true);
}
}
}, 0);
}
@@ -176,6 +188,7 @@ export class CompositionHelper {
const newValue = this._textarea.value;
const diff = newValue.replace(oldValue, '');
if (diff.length > 0) {
this._dataAlreadySent = diff;
this._coreService.triggerDataEvent(diff, true);
}
}
@@ -194,9 +207,11 @@ export class CompositionHelper {
}
if (this._bufferService.buffer.isCursorInViewport) {
const cellHeight = Math.ceil(this._charSizeService.height * this._optionsService.options.lineHeight);
const cursorTop = this._bufferService.buffer.y * cellHeight;
const cursorLeft = this._bufferService.buffer.x * this._charSizeService.width;
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
const cellHeight = this._renderService.dimensions.actualCellHeight;
const cursorTop = this._bufferService.buffer.y * this._renderService.dimensions.actualCellHeight;
const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth;
this._compositionView.style.left = cursorLeft + 'px';
this._compositionView.style.top = cursorTop + 'px';
@@ -209,8 +224,9 @@ export class CompositionHelper {
const compositionViewBounds = this._compositionView.getBoundingClientRect();
this._textarea.style.left = cursorLeft + 'px';
this._textarea.style.top = cursorTop + 'px';
this._textarea.style.width = compositionViewBounds.width + 'px';
this._textarea.style.height = compositionViewBounds.height + 'px';
// Ensure the text area is at least 1x1, otherwise certain IMEs may break
this._textarea.style.width = Math.max(compositionViewBounds.width, 1) + 'px';
this._textarea.style.height = Math.max(compositionViewBounds.height, 1) + 'px';
this._textarea.style.lineHeight = compositionViewBounds.height + 'px';
}
+1 -1
View File
@@ -31,7 +31,7 @@ export class AddonManager implements IDisposable {
};
this._addons.push(loadedAddon);
instance.dispose = () => this._wrappedAddonDispose(loadedAddon);
instance.activate(<any>terminal);
instance.activate(terminal as any);
}
private _wrappedAddonDispose(loadedAddon: ILoadedAddon): void {
+1
View File
@@ -36,6 +36,7 @@ export class Terminal implements ITerminalApi {
public get onData(): IEvent<string> { return this._core.onData; }
public get onBinary(): IEvent<string> { return this._core.onBinary; }
public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
public get onBell(): IEvent<void> { return this._core.onBell; }
public get onScroll(): IEvent<number> { return this._core.onScroll; }
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; }
public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }
+20 -5
View File
@@ -66,7 +66,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
private _initCanvas(): void {
this._ctx = throwIfFalsy(this._canvas.getContext('2d', {alpha: this._alpha}));
this._ctx = throwIfFalsy(this._canvas.getContext('2d', { alpha: this._alpha }));
// Draw the background if this is an opaque layer
if (!this._alpha) {
this._clearAll();
@@ -152,6 +152,21 @@ export abstract class BaseRenderLayer implements IRenderLayer {
height * this._scaledCellHeight);
}
/**
* Fills a 1px line (2px on HDPI) at the middle of the cell. This uses the
* existing fillStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
protected _fillMiddleLineAtCells(x: number, y: number, width: number = 1): void {
const cellOffset = Math.ceil(this._scaledCellHeight * 0.5);
this._ctx.fillRect(
x * this._scaledCellWidth,
(y + 1) * this._scaledCellHeight - cellOffset - window.devicePixelRatio,
width * this._scaledCellWidth,
window.devicePixelRatio);
}
/**
* Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the
* existing fillStyle on the context.
@@ -242,12 +257,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/
protected _fillCharTrueColor(cell: CellData, x: number, y: number): void {
this._ctx.font = this._getFont(false, false);
this._ctx.textBaseline = 'middle';
this._ctx.textBaseline = 'ideographic';
this._clipRow(y);
this._ctx.fillText(
cell.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
}
/**
@@ -320,7 +335,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void {
this._ctx.save();
this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic());
this._ctx.textBaseline = 'middle';
this._ctx.textBaseline = 'ideographic';
if (cell.isInverse()) {
if (fgOverride) {
@@ -362,7 +377,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._ctx.fillText(
cell.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
this._ctx.restore();
}
+10 -5
View File
@@ -37,10 +37,10 @@ export class CursorRenderLayer extends BaseRenderLayer {
colors: IColorSet,
rendererId: number,
private _onRequestRedraw: IEventEmitter<IRequestRedrawEvent>,
bufferService: IBufferService,
optionsService: IOptionsService,
private readonly _coreService: ICoreService,
private readonly _coreBrowserService: ICoreBrowserService
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService,
@ICoreService private readonly _coreService: ICoreService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
) {
super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService);
this._state = {
@@ -197,7 +197,12 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _clearCursor(): void {
if (this._state) {
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
// Avoid potential rounding errors when device pixel ratio is less than 1
if (window.devicePixelRatio < 1) {
this._clearAll();
} else {
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
}
this._state = {
x: 0,
y: 0,
+2 -2
View File
@@ -20,8 +20,8 @@ export class LinkRenderLayer extends BaseRenderLayer {
rendererId: number,
linkifier: ILinkifier,
linkifier2: ILinkifier2,
bufferService: IBufferService,
optionsService: IOptionsService
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService
) {
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService);
linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
+8 -20
View File
@@ -6,13 +6,12 @@
import { TextRenderLayer } from 'browser/renderer/TextRenderLayer';
import { SelectionRenderLayer } from 'browser/renderer/SelectionRenderLayer';
import { CursorRenderLayer } from 'browser/renderer/CursorRenderLayer';
import { IRenderLayer, IRenderer, IRenderDimensions, CharacterJoinerHandler, ICharacterJoinerRegistry, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IRenderLayer, IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer';
import { CharacterJoinerRegistry } from 'browser/renderer/CharacterJoinerRegistry';
import { Disposable } from 'common/Lifecycle';
import { IColorSet, ILinkifier, ILinkifier2 } from 'browser/Types';
import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
import { IBufferService, IOptionsService, ICoreService, IInstantiationService } from 'common/services/Services';
import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache';
import { EventEmitter, IEvent } from 'common/EventEmitter';
@@ -23,7 +22,6 @@ export class Renderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
private _devicePixelRatio: number;
private _characterJoinerRegistry: ICharacterJoinerRegistry;
public dimensions: IRenderDimensions;
@@ -35,20 +33,18 @@ export class Renderer extends Disposable implements IRenderer {
private readonly _screenElement: HTMLElement,
linkifier: ILinkifier,
linkifier2: ILinkifier2,
@IInstantiationService instantiationService: IInstantiationService,
@IBufferService private readonly _bufferService: IBufferService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICoreService coreService: ICoreService,
@ICoreBrowserService coreBrowserService: ICoreBrowserService
@IOptionsService private readonly _optionsService: IOptionsService
) {
super();
const allowTransparency = this._optionsService.options.allowTransparency;
this._characterJoinerRegistry = new CharacterJoinerRegistry(this._bufferService);
this._renderLayers = [
new TextRenderLayer(this._screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, this._bufferService, _optionsService),
new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, _optionsService),
new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier, linkifier2, this._bufferService, _optionsService),
new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRedraw, this._bufferService, _optionsService, coreService, coreBrowserService)
instantiationService.createInstance(TextRenderLayer, this._screenElement, 0, this._colors, allowTransparency, this._id),
instantiationService.createInstance(SelectionRenderLayer, this._screenElement, 1, this._colors, this._id),
instantiationService.createInstance(LinkRenderLayer, this._screenElement, 2, this._colors, this._id, linkifier, linkifier2),
instantiationService.createInstance(CursorRenderLayer, this._screenElement, 3, this._colors, this._id, this._onRequestRedraw)
];
this.dimensions = {
scaledCharWidth: 0,
@@ -210,12 +206,4 @@ export class Renderer extends Disposable implements IRenderer {
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
return this._characterJoinerRegistry.registerCharacterJoiner(handler);
}
public deregisterCharacterJoiner(joinerId: number): boolean {
return this._characterJoinerRegistry.deregisterCharacterJoiner(joinerId);
}
}
+2 -2
View File
@@ -23,8 +23,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
zIndex: number,
colors: IColorSet,
rendererId: number,
bufferService: IBufferService,
optionsService: IOptionsService
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService
) {
super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService);
this._clearState();
+16 -14
View File
@@ -3,16 +3,17 @@
* @license MIT
*/
import { ICharacterJoinerRegistry, IRenderDimensions } from 'browser/renderer/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { CharData, ICellData } from 'common/Types';
import { GridCache } from 'browser/renderer/GridCache';
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
import { AttributeData } from 'common/buffer/AttributeData';
import { NULL_CELL_CODE, Content } from 'common/buffer/Constants';
import { JoinedCellData } from 'browser/renderer/CharacterJoinerRegistry';
import { IColorSet } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { IOptionsService, IBufferService } from 'common/services/Services';
import { ICharacterJoinerService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
/**
* This CharData looks like a null character, which will forc a clear and render
@@ -26,22 +27,20 @@ export class TextRenderLayer extends BaseRenderLayer {
private _characterWidth: number = 0;
private _characterFont: string = '';
private _characterOverlapCache: { [key: string]: boolean } = {};
private _characterJoinerRegistry: ICharacterJoinerRegistry;
private _workCell = new CellData();
constructor(
container: HTMLElement,
zIndex: number,
colors: IColorSet,
characterJoinerRegistry: ICharacterJoinerRegistry,
alpha: boolean,
rendererId: number,
bufferService: IBufferService,
optionsService: IOptionsService
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService,
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService
) {
super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService);
this._state = new GridCache<CharData>();
this._characterJoinerRegistry = characterJoinerRegistry;
}
public resize(dim: IRenderDimensions): void {
@@ -67,7 +66,6 @@ export class TextRenderLayer extends BaseRenderLayer {
private _forEachCell(
firstRow: number,
lastRow: number,
joinerRegistry: ICharacterJoinerRegistry | null,
callback: (
cell: ICellData,
x: number,
@@ -77,7 +75,7 @@ export class TextRenderLayer extends BaseRenderLayer {
for (let y = firstRow; y <= lastRow; y++) {
const row = y + this._bufferService.buffer.ydisp;
const line = this._bufferService.buffer.lines.get(row);
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
for (let x = 0; x < this._bufferService.cols; x++) {
line!.loadCell(x, this._workCell);
let cell = this._workCell;
@@ -101,7 +99,6 @@ export class TextRenderLayer extends BaseRenderLayer {
// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly
cell = new JoinedCellData(
this._workCell,
line!.translateToString(true, range[0], range[1]),
@@ -160,7 +157,7 @@ export class TextRenderLayer extends BaseRenderLayer {
ctx.save();
this._forEachCell(firstRow, lastRow, null, (cell, x, y) => {
this._forEachCell(firstRow, lastRow, (cell, x, y) => {
// libvte and xterm both draw the background (but not foreground) of invisible characters,
// so we should too.
let nextFillStyle = null; // null represents default background color
@@ -213,12 +210,12 @@ export class TextRenderLayer extends BaseRenderLayer {
}
private _drawForeground(firstRow: number, lastRow: number): void {
this._forEachCell(firstRow, lastRow, this._characterJoinerRegistry, (cell, x, y) => {
this._forEachCell(firstRow, lastRow, (cell, x, y) => {
if (cell.isInvisible()) {
return;
}
this._drawChars(cell, x, y);
if (cell.isUnderline()) {
if (cell.isUnderline() || cell.isStrikethrough()) {
this._ctx.save();
if (cell.isInverse()) {
@@ -247,7 +244,12 @@ export class TextRenderLayer extends BaseRenderLayer {
}
}
this._fillBottomLineAtCells(x, y, cell.getWidth());
if (cell.isStrikethrough()) {
this._fillMiddleLineAtCells(x, y, cell.getWidth());
}
if (cell.isUnderline()) {
this._fillBottomLineAtCells(x, y, cell.getWidth());
}
this._ctx.restore();
}
});
-25
View File
@@ -7,8 +7,6 @@ import { IDisposable } from 'common/Types';
import { IColorSet } from 'browser/Types';
import { IEvent } from 'common/EventEmitter';
export type CharacterJoinerHandler = (text: string) => [number, number][];
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
@@ -54,19 +52,6 @@ export interface IRenderer extends IDisposable {
onOptionsChanged(): void;
clear(): void;
renderRows(start: number, end: number): void;
registerCharacterJoiner(handler: CharacterJoinerHandler): number;
deregisterCharacterJoiner(joinerId: number): boolean;
}
export interface ICharacterJoiner {
id: number;
handler: CharacterJoinerHandler;
}
export interface ICharacterJoinerRegistry {
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
deregisterCharacterJoiner(joinerId: number): boolean;
getJoinedCharacters(row: number): [number, number][];
}
export interface IRenderLayer extends IDisposable {
@@ -106,16 +91,6 @@ export interface IRenderLayer extends IDisposable {
*/
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
/**
* Registers a handler to join characters to render as a group
*/
registerCharacterJoiner?(joiner: ICharacterJoiner): void;
/**
* Deregisters the specified character joiner handler
*/
deregisterCharacterJoiner?(joinerId: number): void;
/**
* Resize the render layer.
*/
+2 -4
View File
@@ -10,15 +10,13 @@ import { ITerminalOptions } from 'common/services/Services';
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, options: ITerminalOptions, colors: IColorSet): ICharAtlasConfig {
// null out some fields that don't matter
const clonedColors = <IPartialColorSet>{
const clonedColors: IPartialColorSet = {
foreground: colors.foreground,
background: colors.background,
cursor: undefined,
cursorAccent: undefined,
selection: undefined,
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
// dynamic character atlas.
ansi: colors.ansi.slice(0, 16)
ansi: colors.ansi
};
return {
devicePixelRatio: window.devicePixelRatio,
@@ -91,12 +91,12 @@ export class DynamicCharAtlas extends BaseCharAtlas {
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
// It might also contain some characters with transparent backgrounds if allowTransparency is
// set.
this._cacheCtx = throwIfFalsy(this._cacheCanvas.getContext('2d', {alpha: true}));
this._cacheCtx = throwIfFalsy(this._cacheCanvas.getContext('2d', { alpha: true }));
const tmpCanvas = document.createElement('canvas');
tmpCanvas.width = this._config.scaledCharWidth;
tmpCanvas.height = this._config.scaledCharHeight;
this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', {alpha: this._config.allowTransparency}));
this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency }));
this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth);
this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight);
@@ -256,7 +256,7 @@ export class DynamicCharAtlas extends BaseCharAtlas {
const fontStyle = glyph.italic ? 'italic' : '';
this._tmpCtx.font =
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = 'middle';
this._tmpCtx.textBaseline = 'ideographic';
this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css;
@@ -265,7 +265,7 @@ export class DynamicCharAtlas extends BaseCharAtlas {
this._tmpCtx.globalAlpha = DIM_OPACITY;
}
// Draw the character
this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight / 2);
this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight);
this._tmpCtx.restore();
// clear the background from the character to avoid issues with drawing over the previous
+6 -8
View File
@@ -3,13 +3,13 @@
* @license MIT
*/
import { IRenderer, IRenderDimensions, CharacterJoinerHandler, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { Disposable } from 'common/Lifecycle';
import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types';
import { ICharSizeService } from 'browser/services/Services';
import { IOptionsService, IBufferService } from 'common/services/Services';
import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { color } from 'browser/Color';
import { removeElementFromParent } from 'browser/Dom';
@@ -49,6 +49,7 @@ export class DomRenderer extends Disposable implements IRenderer {
private readonly _viewportElement: HTMLElement,
private readonly _linkifier: ILinkifier,
private readonly _linkifier2: ILinkifier2,
@IInstantiationService instantiationService: IInstantiationService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IOptionsService private readonly _optionsService: IOptionsService,
@IBufferService private readonly _bufferService: IBufferService
@@ -80,7 +81,7 @@ export class DomRenderer extends Disposable implements IRenderer {
this._updateDimensions();
this._injectCss();
this._rowFactory = new DomRendererRowFactory(document, this._optionsService, this._colors);
this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document, this._colors);
this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
this._screenElement.appendChild(this._rowContainer);
@@ -364,7 +365,7 @@ export class DomRenderer extends Disposable implements IRenderer {
const row = y + this._bufferService.buffer.ydisp;
const lineData = this._bufferService.buffer.lines.get(row);
const cursorStyle = this._optionsService.options.cursorStyle;
rowElement.appendChild(this._rowFactory.createRow(lineData!, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols));
rowElement.appendChild(this._rowFactory.createRow(lineData!, row, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols));
}
}
@@ -372,9 +373,6 @@ export class DomRenderer extends Disposable implements IRenderer {
return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return -1; }
public deregisterCharacterJoiner(joinerId: number): boolean { return false; }
private _onLinkHover(e: ILinkifierEvent): void {
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, true);
}
@@ -389,7 +387,7 @@ export class DomRenderer extends Disposable implements IRenderer {
if (!row) {
return;
}
const span = <HTMLElement>row.children[x];
const span = row.children[x] as HTMLElement;
if (span) {
span.style.textDecoration = enabled ? 'underline' : 'none';
}
@@ -12,6 +12,7 @@ import { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { MockOptionsService } from 'common/TestUtils.test';
import { css } from 'browser/Color';
import { MockCharacterJoinerService } from 'browser/TestUtils.test';
describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM;
@@ -20,7 +21,7 @@ describe('DomRendererRowFactory', () => {
beforeEach(() => {
dom = new jsdom.JSDOM('');
rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true }), {
rowFactory = new DomRendererRowFactory(dom.window.document, {
background: css.toColor('#010101'),
foreground: css.toColor('#020202'),
ansi: [
@@ -43,13 +44,13 @@ describe('DomRendererRowFactory', () => {
css.toColor('#34e2e2'),
css.toColor('#eeeeec')
]
} as any);
} as any, new MockCharacterJoinerService(), new MockOptionsService({ drawBoldTextInBrightColors: true }));
lineData = createEmptyLineData(2);
});
describe('createRow', () => {
it('should not create anything for an empty row', () => {
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
''
);
@@ -59,7 +60,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]));
// There should be no element for the following "empty" cell
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '', 0, 0]));
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span style="width: 10px;">語</span>'
);
@@ -67,7 +68,7 @@ describe('DomRendererRowFactory', () => {
it('should add class for cursor and cursor style', () => {
for (const style of ['block', 'bar', 'underline']) {
const fragment = rowFactory.createRow(lineData, true, style, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, true, style, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-cursor xterm-cursor-${style}"> </span>`
);
@@ -75,7 +76,7 @@ describe('DomRendererRowFactory', () => {
});
it('should add class for cursor blink', () => {
const fragment = rowFactory.createRow(lineData, true, 'block', 0, true, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, true, 'block', 0, true, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-cursor xterm-cursor-blink xterm-cursor-block"> </span>`
);
@@ -84,7 +85,7 @@ describe('DomRendererRowFactory', () => {
it('should not render cells that go beyond the terminal\'s columns', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 1);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 1);
assert.equal(getFragmentHtml(fragment),
'<span>a</span>'
);
@@ -95,7 +96,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BOLD;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-bold">a</span>'
);
@@ -105,7 +106,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.ITALIC;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-italic">a</span>'
);
@@ -115,7 +116,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.DIM;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-dim">a</span>'
);
@@ -125,12 +126,22 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-underline">a</span>'
);
});
it('should add class for strikethrough', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.STRIKETHROUGH;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-strikethrough">a</span>'
);
});
it('should add classes for 256 foreground colors', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg |= Attributes.CM_P256;
@@ -138,7 +149,7 @@ describe('DomRendererRowFactory', () => {
cell.fg &= ~Attributes.PCOLOR_MASK;
cell.fg |= i;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-fg-${i}">a</span>`
);
@@ -152,7 +163,7 @@ describe('DomRendererRowFactory', () => {
cell.bg &= ~Attributes.PCOLOR_MASK;
cell.bg |= i;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bg-${i}">a</span>`
);
@@ -164,7 +175,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= Attributes.CM_P16 | 2 | FgFlags.INVERSE;
cell.bg |= Attributes.CM_P16 | 1;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-2">a</span>'
);
@@ -175,7 +186,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= FgFlags.INVERSE;
cell.bg |= Attributes.CM_P16 | 1;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-257">a</span>'
);
@@ -185,7 +196,7 @@ describe('DomRendererRowFactory', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.fg |= Attributes.CM_P16 | 1 | FgFlags.INVERSE;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-257 xterm-bg-1">a</span>'
);
@@ -198,7 +209,7 @@ describe('DomRendererRowFactory', () => {
cell.fg &= ~Attributes.PCOLOR_MASK;
cell.fg |= i;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>`
);
@@ -210,7 +221,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3;
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span style="color:#010203;background-color:#040506;">a</span>'
);
@@ -221,7 +232,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3 | FgFlags.INVERSE;
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span style="color:#040506;background-color:#010203;">a</span>'
);
@@ -10,11 +10,14 @@ import { CellData } from 'common/buffer/CellData';
import { IOptionsService } from 'common/services/Services';
import { color, rgba } from 'browser/Color';
import { IColorSet, IColor } from 'browser/Types';
import { ICharacterJoinerService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
export const BOLD_CLASS = 'xterm-bold';
export const DIM_CLASS = 'xterm-dim';
export const ITALIC_CLASS = 'xterm-italic';
export const UNDERLINE_CLASS = 'xterm-underline';
export const STRIKETHROUGH_CLASS = 'xterm-strikethrough';
export const CURSOR_CLASS = 'xterm-cursor';
export const CURSOR_BLINK_CLASS = 'xterm-cursor-blink';
export const CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block';
@@ -26,8 +29,9 @@ export class DomRendererRowFactory {
constructor(
private readonly _document: Document,
private readonly _optionsService: IOptionsService,
private _colors: IColorSet
private _colors: IColorSet,
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
@IOptionsService private readonly _optionsService: IOptionsService
) {
}
@@ -35,9 +39,10 @@ export class DomRendererRowFactory {
this._colors = colors;
}
public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
public createRow(lineData: IBufferLine, row: number, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
const fragment = this._document.createDocumentFragment();
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
// Find the line length first, this prevents the need to output a bunch of
// empty cells at the end. This cannot easily be integrated into the main
// loop below because of the colCount feature (which can be removed after we
@@ -53,18 +58,58 @@ export class DomRendererRowFactory {
for (let x = 0; x < lineLength; x++) {
lineData.loadCell(x, this._workCell);
const width = this._workCell.getWidth();
let width = this._workCell.getWidth();
// The character to the left is a wide character, drawing is owned by the char at x-1
if (width === 0) {
continue;
}
// If true, indicates that the current character(s) to draw were joined.
let isJoined = false;
let lastCharX = x;
// Process any joined character ranges as needed. Because of how the
// ranges are produced, we know that they are valid for the characters
// and attributes of our input.
let cell = this._workCell;
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
isJoined = true;
const range = joinedRanges.shift()!;
// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly
cell = new JoinedCellData(
this._workCell,
lineData.translateToString(true, range[0], range[1]),
range[1] - range[0]
);
// Skip over the cells occupied by this range in the loop
lastCharX = range[1] - 1;
// Recalculate width
width = cell.getWidth();
}
const charElement = this._document.createElement('span');
if (width > 1) {
charElement.style.width = `${cellWidth * width}px`;
}
if (isJoined) {
// Ligatures in the DOM renderer must use display inline, as they may not show with
// inline-block if they are outside the bounds of the element
charElement.style.display = 'inline';
// The DOM renderer colors the background of the cursor but for ligatures all cells are
// joined. The workaround here is to show a cursor around the whole ligature so it shows up,
// the cursor looks the same when on any character of the ligature though
if (cursorX >= x && cursorX <= lastCharX) {
cursorX = x;
}
}
if (isCursorRow && x === cursorX) {
charElement.classList.add(CURSOR_CLASS);
@@ -85,33 +130,37 @@ export class DomRendererRowFactory {
}
}
if (this._workCell.isBold()) {
if (cell.isBold()) {
charElement.classList.add(BOLD_CLASS);
}
if (this._workCell.isItalic()) {
if (cell.isItalic()) {
charElement.classList.add(ITALIC_CLASS);
}
if (this._workCell.isDim()) {
if (cell.isDim()) {
charElement.classList.add(DIM_CLASS);
}
if (this._workCell.isUnderline()) {
if (cell.isUnderline()) {
charElement.classList.add(UNDERLINE_CLASS);
}
if (this._workCell.isInvisible()) {
if (cell.isInvisible()) {
charElement.textContent = WHITESPACE_CELL_CHAR;
} else {
charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR;
charElement.textContent = cell.getChars() || WHITESPACE_CELL_CHAR;
}
let fg = this._workCell.getFgColor();
let fgColorMode = this._workCell.getFgColorMode();
let bg = this._workCell.getBgColor();
let bgColorMode = this._workCell.getBgColorMode();
const isInverse = !!this._workCell.isInverse();
if (cell.isStrikethrough()) {
charElement.classList.add(STRIKETHROUGH_CLASS);
}
let fg = cell.getFgColor();
let fgColorMode = cell.getFgColorMode();
let bg = cell.getBgColor();
let bgColorMode = cell.getBgColorMode();
const isInverse = !!cell.isInverse();
if (isInverse) {
const temp = fg;
fg = bg;
@@ -125,7 +174,7 @@ export class DomRendererRowFactory {
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (this._workCell.isBold() && fg < 8 && this._optionsService.options.drawBoldTextInBrightColors) {
if (cell.isBold() && fg < 8 && this._optionsService.options.drawBoldTextInBrightColors) {
fg += 8;
}
if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg])) {
@@ -168,6 +217,8 @@ export class DomRendererRowFactory {
}
fragment.appendChild(charElement);
x = lastCharX;
}
return fragment;
}
@@ -4,15 +4,15 @@
*/
import { assert } from 'chai';
import { ICharacterJoinerRegistry } from 'browser/renderer/Types';
import { CharacterJoinerRegistry } from 'browser/renderer/CharacterJoinerRegistry';
import { ICharacterJoinerService } from 'browser/services/Services';
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
import { BufferLine } from 'common/buffer/BufferLine';
import { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { MockBufferService } from 'common/TestUtils.test';
describe('CharacterJoinerRegistry', () => {
let registry: ICharacterJoinerRegistry;
describe('CharacterJoinerService', () => {
let service: ICharacterJoinerService;
beforeEach(() => {
const bufferService = new MockBufferService(16, 10);
@@ -39,225 +39,225 @@ describe('CharacterJoinerRegistry', () => {
for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData()));
lines.set(6, line6);
registry = new CharacterJoinerRegistry(bufferService);
service = new CharacterJoinerService(bufferService);
});
it('has no joiners upon creation', () => {
assert.deepEqual(registry.getJoinedCharacters(0), []);
assert.deepEqual(service.getJoinedCharacters(0), []);
});
it('returns ranges matched by the registered joiners', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
service.register(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[2, 4], [7, 9], [12, 14]]
);
});
it('processes the input using all provided joiners', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
service.register(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(1),
service.getJoinedCharacters(1),
[[2, 4], [12, 14]]
);
registry.registerCharacterJoiner(substringJoiner('=>'));
service.register(substringJoiner('=>'));
assert.deepEqual(
registry.getJoinedCharacters(1),
service.getJoinedCharacters(1),
[[2, 4], [7, 9], [12, 14]]
);
});
it('removes deregistered joiners from future calls', () => {
const joiner1 = registry.registerCharacterJoiner(substringJoiner('->'));
const joiner2 = registry.registerCharacterJoiner(substringJoiner('=>'));
const joiner1 = service.register(substringJoiner('->'));
const joiner2 = service.register(substringJoiner('=>'));
assert.deepEqual(
registry.getJoinedCharacters(1),
service.getJoinedCharacters(1),
[[2, 4], [7, 9], [12, 14]]
);
registry.deregisterCharacterJoiner(joiner1);
service.deregister(joiner1);
assert.deepEqual(
registry.getJoinedCharacters(1),
service.getJoinedCharacters(1),
[[7, 9]]
);
registry.deregisterCharacterJoiner(joiner2);
service.deregister(joiner2);
assert.deepEqual(
registry.getJoinedCharacters(1),
service.getJoinedCharacters(1),
[]
);
});
it('doesn\'t process joins on differently-styled characters', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
service.register(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(2),
service.getJoinedCharacters(2),
[[2, 4], [12, 14]]
);
});
it('returns an empty list of ranges if there is nothing to be joined', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
service.register(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(3),
service.getJoinedCharacters(3),
[]
);
});
it('returns an empty list of ranges if the line is empty', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
service.register(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(4),
service.getJoinedCharacters(4),
[]
);
});
it('returns false when trying to deregister a joiner that does not exist', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
assert.deepEqual(registry.deregisterCharacterJoiner(123), false);
service.register(substringJoiner('->'));
assert.deepEqual(service.deregister(123), false);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[2, 4], [7, 9], [12, 14]]
);
});
it('doesn\'t process same-styled ranges that only have one character', () => {
registry.registerCharacterJoiner(substringJoiner('a'));
registry.registerCharacterJoiner(substringJoiner('b'));
registry.registerCharacterJoiner(substringJoiner('d'));
service.register(substringJoiner('a'));
service.register(substringJoiner('b'));
service.register(substringJoiner('d'));
assert.deepEqual(
registry.getJoinedCharacters(5),
service.getJoinedCharacters(5),
[[5, 6]]
);
});
it('handles ranges that extend all the way to the end of the line', () => {
registry.registerCharacterJoiner(substringJoiner('-> d'));
service.register(substringJoiner('-> d'));
assert.deepEqual(
registry.getJoinedCharacters(2),
service.getJoinedCharacters(2),
[[12, 16]]
);
});
it('handles adjacent ranges', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
registry.registerCharacterJoiner(substringJoiner('> c '));
service.register(substringJoiner('->'));
service.register(substringJoiner('> c '));
assert.deepEqual(
registry.getJoinedCharacters(2),
service.getJoinedCharacters(2),
[[2, 4], [8, 12], [12, 14]]
);
});
it('handles fullwidth characters in the middle of ranges', () => {
registry.registerCharacterJoiner(substringJoiner('wi¥de'));
service.register(substringJoiner('wi¥de'));
assert.deepEqual(
registry.getJoinedCharacters(6),
service.getJoinedCharacters(6),
[[0, 6]]
);
});
it('handles fullwidth characters at the end of ranges', () => {
registry.registerCharacterJoiner(substringJoiner('wi¥'));
service.register(substringJoiner('wi¥'));
assert.deepEqual(
registry.getJoinedCharacters(6),
service.getJoinedCharacters(6),
[[0, 4]]
);
});
it('handles emojis in the middle of ranges', () => {
registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 ji'));
service.register(substringJoiner('emo\xf0\x9f\x98\x81 ji'));
assert.deepEqual(
registry.getJoinedCharacters(6),
service.getJoinedCharacters(6),
[[6, 13]]
);
});
it('handles emojis at the end of ranges', () => {
registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 '));
service.register(substringJoiner('emo\xf0\x9f\x98\x81 '));
assert.deepEqual(
registry.getJoinedCharacters(6),
service.getJoinedCharacters(6),
[[6, 11]]
);
});
it('handles ranges after wide and emoji characters', () => {
registry.registerCharacterJoiner(substringJoiner('abc'));
service.register(substringJoiner('abc'));
assert.deepEqual(
registry.getJoinedCharacters(6),
service.getJoinedCharacters(6),
[[13, 16]]
);
});
describe('range merging', () => {
it('inserts a new range before the existing ones', () => {
registry.registerCharacterJoiner(() => [[1, 2], [2, 3]]);
registry.registerCharacterJoiner(() => [[0, 1]]);
service.register(() => [[1, 2], [2, 3]]);
service.register(() => [[0, 1]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[0, 1], [1, 2], [2, 3]]
);
});
it('inserts in between two ranges', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[2, 4]]);
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[2, 4]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[0, 2], [2, 4], [4, 6]]
);
});
it('inserts after the last range', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[6, 8]]);
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[6, 8]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[0, 2], [4, 6], [6, 8]]
);
});
it('extends the beginning of a range', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[3, 5]]);
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[3, 5]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[0, 2], [3, 6]]
);
});
it('extends the end of a range', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[1, 4]]);
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[1, 4]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[0, 4], [4, 6]]
);
});
it('extends the last range', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[5, 7]]);
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[5, 7]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[0, 2], [4, 7]]
);
});
it('connects two ranges', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[1, 5]]);
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[1, 5]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[0, 6]]
);
});
it('connects more than two ranges', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6], [8, 10], [12, 14]]);
registry.registerCharacterJoiner(() => [[1, 10]]);
service.register(() => [[0, 2], [4, 6], [8, 10], [12, 14]]);
service.register(() => [[1, 10]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
service.getJoinedCharacters(0),
[[0, 10], [12, 14]]
);
});
@@ -270,7 +270,7 @@ function lineData(data: IPartialLineData[]): IBufferLine {
const tline = new BufferLine(0);
for (let i = 0; i < data.length; ++i) {
const line = data[i][0];
const attr = <number>(data[i][1] || 0);
const attr = (data[i][1] || 0) as number;
const offset = tline.length;
tline.resize(tline.length + line.split('').length, CellData.fromCharData([0, '', 0, 0]));
line.split('').map((char, idx) => tline.setCell(idx + offset, CellData.fromCharData([attr, char, 1, char.charCodeAt(0)])));
@@ -4,11 +4,12 @@
*/
import { IBufferLine, ICellData, CharData } from 'common/Types';
import { ICharacterJoinerRegistry, ICharacterJoiner } from 'browser/renderer/Types';
import { ICharacterJoiner } from 'browser/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { IBufferService } from 'common/services/Services';
import { ICharacterJoinerService } from 'browser/services/Services';
export class JoinedCellData extends AttributeData implements ICellData {
private _width: number;
@@ -55,15 +56,18 @@ export class JoinedCellData extends AttributeData implements ICellData {
}
}
export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
export class CharacterJoinerService implements ICharacterJoinerService {
public serviceBrand: undefined;
private _characterJoiners: ICharacterJoiner[] = [];
private _nextCharacterJoinerId: number = 0;
private _workCell: CellData = new CellData();
constructor(private _bufferService: IBufferService) { }
constructor(
@IBufferService private _bufferService: IBufferService
) { }
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
public register(handler: (text: string) => [number, number][]): number {
const joiner: ICharacterJoiner = {
id: this._nextCharacterJoinerId++,
handler
@@ -73,7 +77,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
return joiner.id;
}
public deregisterCharacterJoiner(joinerId: number): boolean {
public deregister(joinerId: number): boolean {
for (let i = 0; i < this._characterJoiners.length; i++) {
if (this._characterJoiners[i].id === joinerId) {
this._characterJoiners.splice(i, 1);
@@ -177,7 +181,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
// We merge any overlapping ranges across the different joiners
const joinerRanges = this._characterJoiners[i].handler(text);
for (let j = 0; j < joinerRanges.length; j++) {
CharacterJoinerRegistry._mergeRanges(joinedRanges, joinerRanges[j]);
CharacterJoinerService._mergeRanges(joinedRanges, joinerRanges[j]);
}
}
this._stringRangesToCellRanges(joinedRanges, lineData, startCol);
+5 -9
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types';
import { IRenderer, IRenderDimensions } from 'browser/renderer/Types';
import { RenderDebouncer } from 'browser/RenderDebouncer';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
@@ -174,6 +174,10 @@ export class RenderService extends Disposable implements IRenderService {
}
public onDevicePixelRatioChange(): void {
// Force char size measurement as DomMeasureStrategy(getBoundingClientRect) is not stable
// when devicePixelRatio changes
this._charSizeService.measure();
this._renderer.onDevicePixelRatioChange();
this.refreshRows(0, this._rowCount - 1);
}
@@ -210,12 +214,4 @@ export class RenderService extends Disposable implements IRenderService {
public clear(): void {
this._renderer.clear();
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
return this._renderer.registerCharacterJoiner(handler);
}
public deregisterCharacterJoiner(joinerId: number): boolean {
return this._renderer.deregisterCharacterJoiner(joinerId);
}
}
@@ -21,7 +21,7 @@ class TestSelectionService extends SelectionService {
optionsService: IOptionsService,
renderService: IRenderService
) {
super(null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService);
super(null!, null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService);
}
public get model(): SelectionModel { return this._model; }
+68 -13
View File
@@ -10,11 +10,13 @@ import * as Browser from 'common/Platform';
import { SelectionModel } from 'browser/selection/SelectionModel';
import { CellData } from 'common/buffer/CellData';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { ICharSizeService, IMouseService, ISelectionService, IRenderService } from 'browser/services/Services';
import { IMouseService, ISelectionService, IRenderService } from 'browser/services/Services';
import { ILinkifier2 } from 'browser/Types';
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
import { getCoordsRelativeToElement } from 'browser/input/Mouse';
import { moveToCellSequence } from 'browser/input/MoveToCell';
import { Disposable } from 'common/Lifecycle';
import { getRangeLength } from 'common/buffer/BufferRange';
/**
* The number of pixels the mouse needs to be above or below the viewport in
@@ -105,6 +107,9 @@ export class SelectionService extends Disposable implements ISelectionService {
private _workCell: CellData = new CellData();
private _mouseDownTimeStamp: number = 0;
private _oldHasSelection: boolean = false;
private _oldSelectionStart: [number, number] | undefined = undefined;
private _oldSelectionEnd: [number, number] | undefined = undefined;
private _onLinuxMouseSelection = this.register(new EventEmitter<string>());
public get onLinuxMouseSelection(): IEvent<string> { return this._onLinuxMouseSelection.event; }
@@ -118,6 +123,7 @@ export class SelectionService extends Disposable implements ISelectionService {
constructor(
private readonly _element: HTMLElement,
private readonly _screenElement: HTMLElement,
private readonly _linkifier: ILinkifier2,
@IBufferService private readonly _bufferService: IBufferService,
@ICoreService private readonly _coreService: ICoreService,
@IMouseService private readonly _mouseService: IMouseService,
@@ -127,8 +133,8 @@ export class SelectionService extends Disposable implements ISelectionService {
super();
// Init listeners
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
this._mouseMoveListener = event => this._onMouseMove(event as MouseEvent);
this._mouseUpListener = event => this._onMouseUp(event as MouseEvent);
this._coreService.onUserInput(() => {
if (this.hasSelection) {
this.clearSelection();
@@ -290,7 +296,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* Checks if the current click was inside the current selection
* @param event The mouse event
*/
public isClickInSelection(event: MouseEvent): boolean {
private _isClickInSelection(event: MouseEvent): boolean {
const coords = this._getMouseBufferCoords(event);
const start = this._model.finalSelectionStart;
const end = this._model.finalSelectionEnd;
@@ -313,13 +319,23 @@ export class SelectionService extends Disposable implements ISelectionService {
* Selects word at the current mouse event coordinates.
* @param event The mouse event.
*/
public selectWordAtCursor(event: MouseEvent): void {
private _selectWordAtCursor(event: MouseEvent, allowWhitespaceOnlySelection: boolean): boolean {
// Check if there is a link under the cursor first and select that if so
const range = this._linkifier.currentLink?.link?.range;
if (range) {
this._model.selectionStart = [range.start.x - 1, range.start.y - 1];
this._model.selectionStartLength = getRangeLength(range, this._bufferService.cols);
this._model.selectionEnd = undefined;
return true;
}
const coords = this._getMouseBufferCoords(event);
if (coords) {
this._selectWordAt(coords, false);
this._selectWordAt(coords, allowWhitespaceOnlySelection);
this._model.selectionEnd = undefined;
this.refresh(true);
return true;
}
return false;
}
/**
@@ -524,14 +540,12 @@ export class SelectionService extends Disposable implements ISelectionService {
}
/**
* Performs a double click, selecting the current work.
* Performs a double click, selecting the current word.
* @param event The mouse event.
*/
private _onDoubleClick(event: MouseEvent): void {
const coords = this._getMouseBufferCoords(event);
if (coords) {
if (this._selectWordAtCursor(event, true)) {
this._activeSelectionMode = SelectionMode.WORD;
this._selectWordAt(coords, true);
}
}
@@ -681,11 +695,43 @@ export class SelectionService extends Disposable implements ISelectionService {
this._coreService.triggerDataEvent(sequence, true);
}
}
} else if (this.hasSelection) {
this._onSelectionChange.fire();
} else {
this._fireEventIfSelectionChanged();
}
}
private _fireEventIfSelectionChanged(): void {
const start = this._model.finalSelectionStart;
const end = this._model.finalSelectionEnd;
const hasSelection = !!start && !!end && (start[0] !== end[0] || start[1] !== end[1]);
if (!hasSelection) {
if (this._oldHasSelection) {
this._fireOnSelectionChange(start, end, hasSelection);
}
return;
}
// Sanity check, these should not be undefined as there is a selection
if (!start || !end) {
return;
}
if (!this._oldSelectionStart || !this._oldSelectionEnd || (
start[0] !== this._oldSelectionStart[0] || start[1] !== this._oldSelectionStart[1] ||
end[0] !== this._oldSelectionEnd[0] || end[1] !== this._oldSelectionEnd[1])) {
this._fireOnSelectionChange(start, end, hasSelection);
}
}
private _fireOnSelectionChange(start: [number, number] | undefined, end: [number, number] | undefined, hasSelection: boolean): void {
this._oldSelectionStart = start;
this._oldSelectionEnd = end;
this._oldHasSelection = hasSelection;
this._onSelectionChange.fire();
}
private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void {
this.clearSelection();
// Only adjust the selection on trim, shiftElements is rarely used (only in
@@ -727,6 +773,15 @@ export class SelectionService extends Disposable implements ISelectionService {
this.refresh();
}
public rightClickSelect(ev: MouseEvent): void {
if (!this._isClickInSelection(ev)) {
if (this._selectWordAtCursor(ev, false)) {
this.refresh(true);
}
this._fireEventIfSelectionChanged();
}
}
/**
* Gets positional information for the word at the coordinated specified.
* @param coords The coordinates to get the word at.
+12 -5
View File
@@ -4,7 +4,7 @@
*/
import { IEvent } from 'common/EventEmitter';
import { IRenderDimensions, IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types';
import { IRenderDimensions, IRenderer } from 'browser/renderer/Types';
import { IColorSet } from 'browser/Types';
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
@@ -66,8 +66,6 @@ export interface IRenderService extends IDisposable {
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
onCursorMove(): void;
clear(): void;
registerCharacterJoiner(handler: CharacterJoinerHandler): number;
deregisterCharacterJoiner(joinerId: number): boolean;
}
export const ISelectionService = createDecorator<ISelectionService>('SelectionService');
@@ -91,8 +89,7 @@ export interface ISelectionService {
selectAll(): void;
selectLines(start: number, end: number): void;
clearSelection(): void;
isClickInSelection(event: MouseEvent): boolean;
selectWordAtCursor(event: MouseEvent): void;
rightClickSelect(event: MouseEvent): void;
shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean;
shouldForceSelection(event: MouseEvent): boolean;
refresh(isLinuxMouseSelection?: boolean): void;
@@ -105,3 +102,13 @@ export interface ISoundService {
playBellSound(): void;
}
export const ICharacterJoinerService = createDecorator<ICharacterJoinerService>('CharacterJoinerService');
export interface ICharacterJoinerService {
serviceBrand: undefined;
register(handler: (text: string) => [number, number][]): number;
deregister(joinerId: number): boolean;
getJoinedCharacters(row: number): [number, number][];
}
+1 -1
View File
@@ -13,7 +13,7 @@ export class SoundService implements ISoundService {
public static get audioContext(): AudioContext | null {
if (!SoundService._audioContext) {
const audioContextCtor: typeof AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
const audioContextCtor: typeof AudioContext = (window as any).AudioContext || (window as any).webkitAudioContext;
if (!audioContextCtor) {
console.warn('Web Audio API is not supported by this browser. Consider upgrading to the latest version');
return null;