Merge branch 'master' into fix/ime-middle-composition-suffix

This commit is contained in:
Daniel Imms
2026-03-11 13:06:35 +09:00
committed by GitHub
107 changed files with 5867 additions and 1394 deletions
+10
View File
@@ -29,4 +29,14 @@ describe('evaluatePastedTextProcessing', () => {
assert.equal(unbracketedText, 'foo bar');
assert.equal(bracketedText, '\x1b[200~foo bar\x1b[201~');
});
it('should escape embedded escape sequences in pasted text only when bracketed', () => {
const ESC_SYMBOL = '\u241b';
const pastedText = '\x1b[201~foo\x1b[200~bar';
const unbracketedText = Clipboard.bracketTextForPaste(pastedText, false);
const bracketedText = Clipboard.bracketTextForPaste(pastedText, true);
assert.equal(unbracketedText, pastedText, 'non bracketed paste should remain unchanged');
assert.equal(bracketedText, `\x1b[200~${ESC_SYMBOL}[201~foo${ESC_SYMBOL}[200~bar\x1b[201~`);
});
});
+6 -3
View File
@@ -19,10 +19,13 @@ export function prepareTextForTerminal(text: string): string {
* @param text The pasted text to bracket
*/
export function bracketTextForPaste(text: string, bracketedPasteMode: boolean): string {
if (bracketedPasteMode) {
return '\x1b[200~' + text + '\x1b[201~';
if (!bracketedPasteMode) {
return text;
}
return text;
// Sanitize pasted text to prevent injected escape sequences (e.g. exiting bracketed paste)
// by replacing ESC (\x1b) with its visible representation U+241B (␛).
const sanitizedText = text.replace(/\x1b/g, '\u241b');
return `\x1b[200~${sanitizedText}\x1b[201~`;
}
/**
+31 -282
View File
@@ -36,16 +36,17 @@ import { CharSizeService } from 'browser/services/CharSizeService';
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
import { CoreBrowserService } from 'browser/services/CoreBrowserService';
import { LinkProviderService } from 'browser/services/LinkProviderService';
import { MouseCoordsService } from 'browser/services/MouseCoordsService';
import { MouseService } from 'browser/services/MouseService';
import { RenderService } from 'browser/services/RenderService';
import { SelectionService } from 'browser/services/SelectionService';
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseCoordsService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { ThemeService } from 'browser/services/ThemeService';
import { KeyboardService } from 'browser/services/KeyboardService';
import { channels, color, rgb } from 'common/Color';
import { CoreTerminal } from 'common/CoreTerminal';
import * as Browser from 'common/Platform';
import { ColorRequestType, CoreMouseAction, CoreMouseButton, CoreMouseEventType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types';
import { ColorRequestType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { IBuffer } from 'common/buffer/Types';
import { C0, C1ESCAPED } from 'common/data/EscapeSequences';
@@ -77,7 +78,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
public browser: IBrowser = Browser as any;
private _customKeyEventHandler: CustomKeyEventHandler | undefined;
private _customWheelEventHandler: CustomWheelEventHandler | undefined;
// Browser services
private readonly _decorationService: DecorationService;
@@ -87,6 +87,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
// Optional browser services
private _charSizeService: ICharSizeService | undefined;
private _coreBrowserService: ICoreBrowserService | undefined;
private _mouseCoordsService: IMouseCoordsService | undefined;
private _mouseService: IMouseService | undefined;
private _renderService: IRenderService | undefined;
private _themeService: IThemeService | undefined;
@@ -413,7 +414,16 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this._register(addDisposableListener(this.textarea!, 'keyup', (ev: KeyboardEvent) => this._keyUp(ev), true));
this._register(addDisposableListener(this.textarea!, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true));
this._register(addDisposableListener(this.textarea!, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true));
this._register(addDisposableListener(this.textarea!, 'compositionstart', () => this._compositionHelper!.compositionstart()));
this._register(addDisposableListener(this.textarea!, 'compositionstart', () => {
// Ensure the textarea is synced to the latest cursor location before composition begins. This
// is to workaround a problem where highly dynamic TUIs like agentic CLIs reprint agressively
// would cause the IME to appear in the wrong position. The theory is that when the IME is
// triggered during a partial render the textarea position becomes locked and will not move
// until it is hidden and a custom move occurs.
this._syncTextArea();
this._compositionHelper!.compositionstart();
this._compositionHelper!.updateCompositionElements();
}));
this._register(addDisposableListener(this.textarea!, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper!.compositionupdate(e)));
this._register(addDisposableListener(this.textarea!, 'compositionend', () => this._compositionHelper!.compositionend()));
this._register(addDisposableListener(this.textarea!, 'input', (ev: InputEvent) => this._inputEvent(ev), true));
@@ -543,8 +553,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
this._helperContainer.appendChild(this._compositionView);
this._mouseService = this._instantiationService.createInstance(MouseService);
this._instantiationService.setService(IMouseService, this._mouseService);
this._mouseCoordsService = this._instantiationService.createInstance(MouseCoordsService);
this._instantiationService.setService(IMouseCoordsService, this._mouseCoordsService);
const linkifier = this._linkifier.value = this._register(this._instantiationService.createInstance(Linkifier, this.screenElement));
@@ -563,7 +573,10 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this._renderService!.handleCursorMove();
this._syncTextArea();
}));
this._register(this.onResize(() => this._renderService!.handleResize(this.cols, this.rows)));
this._register(this.onResize(() => {
this._renderService!.handleResize(this.cols, this.rows);
this._syncTextArea();
}));
this._register(this.onBlur(() => this._renderService!.handleBlur()));
this._register(this.onFocus(() => this._renderService!.handleFocus()));
@@ -579,6 +592,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
linkifier
));
this._instantiationService.setService(ISelectionService, this._selectionService);
this._mouseService = this._instantiationService.createInstance(MouseService);
this._instantiationService.setService(IMouseService, this._mouseService);
this._register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
this._register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire()));
this._register(this._selectionService.onRequestRedraw(e => this._renderService!.handleSelectionChanged(e.start, e.end, e.columnSelectMode)));
@@ -602,7 +617,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this._register(addDisposableListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e)));
// apply mouse event classes set by escape codes before terminal was attached
if (this.coreMouseService.areMouseEventsActive) {
if (this.mouseStateService.areMouseEventsActive) {
this._selectionService.disable();
this.element.classList.add('enable-mouse-events');
} else {
@@ -638,285 +653,18 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
// Listen for mouse events and translate
// them into terminal mouse protocols.
this.bindMouse();
this._mouseService.bindMouse({
element: this.element!,
screenElement: this.screenElement!,
document: this._document!,
handleTouchScroll: amount => this._viewport?.handleTouchScroll(amount)
}, disposable => this._register(disposable), () => this.focus());
}
private _createRenderer(): IRenderer {
return this._instantiationService.createInstance(DomRenderer, this, this._document!, this.element!, this.screenElement!, this._viewportElement!, this._helperContainer!, this.linkifier!);
}
/**
* Bind certain mouse events to the terminal.
* By default only 3 button + wheel up/down is ativated. For higher buttons
* no mouse report will be created. Typically the standard actions will be active.
*
* There are several reasons not to enable support for higher buttons/wheel:
* - Button 4 and 5 are typically used for history back and forward navigation,
* there is no straight forward way to supress/intercept those standard actions.
* - Support for higher buttons does not work in some platform/browser combinations.
* - Left/right wheel was not tested.
* - Emulators vary in mouse button support, typically only 3 buttons and
* wheel up/down work reliable.
*
* TODO: Move mouse event code into its own file.
*/
public bindMouse(): void {
const self = this;
const el = this.element!;
// send event to CoreMouseService
function sendEvent(ev: MouseEvent | WheelEvent): boolean {
// Get mouse coordinates
const pos = self._mouseService?.getMouseReportCoords(ev, self.screenElement!);
if (!pos) {
return false;
}
let but: CoreMouseButton;
let action: CoreMouseAction | undefined;
switch ((ev as any).overrideType || ev.type) {
case 'mousemove':
action = CoreMouseAction.MOVE;
if (ev.buttons === undefined) {
// buttons is not supported on macOS, try to get a value from button instead
but = CoreMouseButton.NONE;
if (ev.button !== undefined) {
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
}
} else {
// according to MDN buttons only reports up to button 5 (AUX2)
but = ev.buttons & 1 ? CoreMouseButton.LEFT :
ev.buttons & 4 ? CoreMouseButton.MIDDLE :
ev.buttons & 2 ? CoreMouseButton.RIGHT :
CoreMouseButton.NONE; // fallback to NONE
}
break;
case 'mouseup':
action = CoreMouseAction.UP;
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
break;
case 'mousedown':
action = CoreMouseAction.DOWN;
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
break;
case 'wheel':
if (self._customWheelEventHandler && self._customWheelEventHandler(ev as WheelEvent) === false) {
return false;
}
const deltaY = (ev as WheelEvent).deltaY;
if (deltaY === 0) {
return false;
}
const lines = self.coreMouseService.consumeWheelEvent(
ev as WheelEvent,
self._renderService?.dimensions?.device?.cell?.height,
self._coreBrowserService?.dpr
);
if (lines === 0) {
return false;
}
action = deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
but = CoreMouseButton.WHEEL;
break;
default:
// dont handle other event types by accident
return false;
}
// exit if we cannot determine valid button/action values
// do nothing for higher buttons than wheel
if (action === undefined || but === undefined || but > CoreMouseButton.WHEEL) {
return false;
}
return self.coreMouseService.triggerMouseEvent({
col: pos.col,
row: pos.row,
x: pos.x,
y: pos.y,
button: but,
action,
ctrl: ev.ctrlKey,
alt: ev.altKey,
shift: ev.shiftKey
});
}
/**
* Event listener state handling.
* We listen to the onProtocolChange event of CoreMouseService and put
* requested listeners in `requestedEvents`. With this the listeners
* have all bits to do the event listener juggling.
* Note: 'mousedown' currently is "always on" and not managed
* by onProtocolChange.
*/
const requestedEvents: { [key: string]: ((ev: MouseEvent | WheelEvent) => void) | null } = {
mouseup: null,
wheel: null,
mousedrag: null,
mousemove: null
};
const eventListeners: { [key: string]: (ev: any) => void | boolean } = {
mouseup: (ev: MouseEvent) => {
sendEvent(ev);
if (!ev.buttons) {
// if no other button is held remove global handlers
this._document!.removeEventListener('mouseup', requestedEvents.mouseup!);
if (requestedEvents.mousedrag) {
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag);
}
}
},
wheel: (ev: WheelEvent) => {
sendEvent(ev);
ev.preventDefault();
ev.stopPropagation();
return false;
},
mousedrag: (ev: MouseEvent) => {
// deal only with move while a button is held
if (ev.buttons) {
sendEvent(ev);
}
},
mousemove: (ev: MouseEvent) => {
// deal only with move without any button
if (!ev.buttons) {
sendEvent(ev);
}
}
};
this._register(this.coreMouseService.onProtocolChange(events => {
// apply global changes on events
if (events) {
if (this.optionsService.rawOptions.logLevel === 'debug') {
this._logService.debug('Binding to mouse events:', this.coreMouseService.explainEvents(events));
}
this.element!.classList.add('enable-mouse-events');
this._selectionService!.disable();
} else {
this._logService.debug('Unbinding from mouse events.');
this.element!.classList.remove('enable-mouse-events');
this._selectionService!.enable();
}
// add/remove handlers from requestedEvents
if (!(events & CoreMouseEventType.MOVE)) {
el.removeEventListener('mousemove', requestedEvents.mousemove!);
requestedEvents.mousemove = null;
} else if (!requestedEvents.mousemove) {
el.addEventListener('mousemove', eventListeners.mousemove);
requestedEvents.mousemove = eventListeners.mousemove;
}
if (!(events & CoreMouseEventType.WHEEL)) {
el.removeEventListener('wheel', requestedEvents.wheel!);
requestedEvents.wheel = null;
} else if (!requestedEvents.wheel) {
el.addEventListener('wheel', eventListeners.wheel, { passive: false });
requestedEvents.wheel = eventListeners.wheel;
}
if (!(events & CoreMouseEventType.UP)) {
this._document!.removeEventListener('mouseup', requestedEvents.mouseup!);
requestedEvents.mouseup = null;
} else {
requestedEvents.mouseup ??= eventListeners.mouseup;
}
if (!(events & CoreMouseEventType.DRAG)) {
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag!);
requestedEvents.mousedrag = null;
} else {
requestedEvents.mousedrag ??= eventListeners.mousedrag;
}
}));
// force initial onProtocolChange so we dont miss early mouse requests
this.coreMouseService.activeProtocol = this.coreMouseService.activeProtocol;
// Ensure document-level listeners are removed on dispose
this._register(toDisposable(() => {
if (requestedEvents.mouseup) {
this._document!.removeEventListener('mouseup', requestedEvents.mouseup);
}
if (requestedEvents.mousedrag) {
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag);
}
}));
/**
* "Always on" event listeners.
*/
this._register(addDisposableListener(el, 'mousedown', (ev: MouseEvent) => {
ev.preventDefault();
this.focus();
// Don't send the mouse button to the pty if mouse events are disabled or
// if the selection manager is having selection forced (ie. a modifier is
// held).
if (!this.coreMouseService.areMouseEventsActive || this._selectionService!.shouldForceSelection(ev)) {
return;
}
sendEvent(ev);
// Register additional global handlers which should keep reporting outside
// of the terminal element.
// Note: Other emulators also do this for 'mousedown' while a button
// is held, we currently limit 'mousedown' to the terminal only.
if (requestedEvents.mouseup) {
this._document!.addEventListener('mouseup', requestedEvents.mouseup);
}
if (requestedEvents.mousedrag) {
this._document!.addEventListener('mousemove', requestedEvents.mousedrag);
}
}));
this._register(addDisposableListener(el, 'wheel', (ev: WheelEvent) => {
// do nothing, if app side handles wheel itself
if (requestedEvents.wheel) return;
if (this._customWheelEventHandler && this._customWheelEventHandler(ev) === false) {
return false;
}
if (!this.buffer.hasScrollback) {
// Convert wheel events into up/down events when the buffer does not have scrollback, this
// enables scrolling in apps hosted in the alt buffer such as vim or tmux even when mouse
// events are not enabled.
// This used implementation used get the actual lines/partial lines scrolled from the
// viewport but since moving to the new viewport implementation has been simplified to
// simply send a single up or down sequence.
// Do nothing if there's no vertical scroll
const deltaY = (ev as WheelEvent).deltaY;
if (deltaY === 0) {
return false;
}
const lines = self.coreMouseService.consumeWheelEvent(
ev as WheelEvent,
self._renderService?.dimensions?.device?.cell?.height,
self._coreBrowserService?.dpr
);
if (lines === 0) {
ev.preventDefault();
ev.stopPropagation();
return false;
}
// Construct and send sequences
const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
this.coreService.triggerDataEvent(sequence, true);
ev.preventDefault();
ev.stopPropagation();
return false;
}
}, { passive: false }));
}
/**
* Tells the renderer to refresh terminal content between two rows (inclusive) at the next
* opportunity.
@@ -990,7 +738,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
}
public attachCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler): void {
this._customWheelEventHandler = customWheelEventHandler;
this.mouseStateService.setCustomWheelEventHandler(customWheelEventHandler);
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
@@ -1360,6 +1108,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this._setup();
super.reset();
this._mouseService?.reset();
this._selectionService?.reset();
this._decorationService.reset();
+8 -8
View File
@@ -7,7 +7,7 @@ import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifie
import { Disposable, dispose, toDisposable } from 'common/Lifecycle';
import { IDisposable } from 'common/Types';
import { IBufferService } from 'common/services/Services';
import { ILinkProviderService, IMouseService, IRenderService } from './services/Services';
import { ILinkProviderService, IMouseCoordsService, IRenderService } from './services/Services';
import { Emitter } from 'common/Event';
import { addDisposableListener } from 'browser/Dom';
@@ -30,7 +30,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
constructor(
private readonly _element: HTMLElement,
@IMouseService private readonly _mouseService: IMouseService,
@IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
@IRenderService private readonly _renderService: IRenderService,
@IBufferService private readonly _bufferService: IBufferService,
@ILinkProviderService private readonly _linkProviderService: ILinkProviderService
@@ -60,7 +60,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
private _handleMouseMove(event: MouseEvent): void {
this._lastMouseEvent = event;
const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
const position = this._positionFromMouseEvent(event, this._element);
if (!position) {
return;
}
@@ -222,7 +222,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
return;
}
const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
const position = this._positionFromMouseEvent(event, this._element);
if (!position) {
return;
}
@@ -251,7 +251,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
return;
}
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService);
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element);
if (!position) {
return;
@@ -312,7 +312,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
this._clearCurrentLink(start, end);
if (this._lastMouseEvent) {
// re-eval previously active link after changes
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element);
if (position) {
this._askForLink(position, false);
}
@@ -378,8 +378,8 @@ export class Linkifier extends Disposable implements ILinkifier2 {
* Get the buffer position from a mouse event
* @param event
*/
private _positionFromMouseEvent(event: MouseEvent, element: HTMLElement, mouseService: IMouseService): IBufferCellPosition | undefined {
const coords = mouseService.getCoords(event, element, this._bufferService.cols, this._bufferService.rows);
private _positionFromMouseEvent(event: MouseEvent, element: HTMLElement): IBufferCellPosition | undefined {
const coords = this._mouseCoordsService.getCoords(event, element, this._bufferService.cols, this._bufferService.rows);
if (!coords) {
return;
}
+32 -32
View File
@@ -8,7 +8,7 @@ import type { IBrowser } from 'browser/Types';
import { assert } from 'chai';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { MockUnicodeService } from 'common/TestUtils.test';
import { MockUnicodeService, createCellData } from 'common/TestUtils.test';
import { IMarker } from 'common/Types';
const INIT_COLS = 80;
@@ -440,8 +440,8 @@ describe('Terminal', () => {
describe('scroll() function', () => {
describe('when scrollback > 0', () => {
it('should create a new line and scroll', () => {
term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(INIT_ROWS - 1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 0));
term.buffer.lines.get(INIT_ROWS - 1)!.setCell(0, createCellData(0, 'b', 0));
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.scroll(DEFAULT_ATTR_DATA.clone());
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
@@ -451,9 +451,9 @@ describe('Terminal', () => {
});
it('should properly scroll inside a scroll region (scrollTop set)', () => {
term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 0));
term.buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 0));
term.buffer.lines.get(2)!.setCell(0, createCellData(0, 'c', 0));
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.scroll(DEFAULT_ATTR_DATA.clone());
@@ -463,11 +463,11 @@ describe('Terminal', () => {
});
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
term.buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 0));
term.buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 0));
term.buffer.lines.get(2)!.setCell(0, createCellData(0, 'c', 0));
term.buffer.lines.get(3)!.setCell(0, createCellData(0, 'd', 0));
term.buffer.lines.get(4)!.setCell(0, createCellData(0, 'e', 0));
term.buffer.y = 3;
term.buffer.scrollBottom = 3;
term.scroll(DEFAULT_ATTR_DATA.clone());
@@ -481,11 +481,11 @@ describe('Terminal', () => {
});
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
term.buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 0));
term.buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 0));
term.buffer.lines.get(2)!.setCell(0, createCellData(0, 'c', 0));
term.buffer.lines.get(3)!.setCell(0, createCellData(0, 'd', 0));
term.buffer.lines.get(4)!.setCell(0, createCellData(0, 'e', 0));
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.buffer.scrollBottom = 3;
@@ -506,9 +506,9 @@ describe('Terminal', () => {
});
it('should create a new line and shift everything up', () => {
term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.lines.get(INIT_ROWS - 1)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 0));
term.buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 0));
term.buffer.lines.get(INIT_ROWS - 1)!.setCell(0, createCellData(0, 'c', 0));
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
assert.equal(term.buffer.lines.length, INIT_ROWS);
term.scroll(DEFAULT_ATTR_DATA.clone());
@@ -521,9 +521,9 @@ describe('Terminal', () => {
});
it('should properly scroll inside a scroll region (scrollTop set)', () => {
term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 0));
term.buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 0));
term.buffer.lines.get(2)!.setCell(0, createCellData(0, 'c', 0));
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.scroll(DEFAULT_ATTR_DATA.clone());
@@ -533,11 +533,11 @@ describe('Terminal', () => {
});
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
term.buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 0));
term.buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 0));
term.buffer.lines.get(2)!.setCell(0, createCellData(0, 'c', 0));
term.buffer.lines.get(3)!.setCell(0, createCellData(0, 'd', 0));
term.buffer.lines.get(4)!.setCell(0, createCellData(0, 'e', 0));
term.buffer.y = 3;
term.buffer.scrollBottom = 3;
term.scroll(DEFAULT_ATTR_DATA.clone());
@@ -550,11 +550,11 @@ describe('Terminal', () => {
});
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
term.buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 0));
term.buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 0));
term.buffer.lines.get(2)!.setCell(0, createCellData(0, 'c', 0));
term.buffer.lines.get(3)!.setCell(0, createCellData(0, 'd', 0));
term.buffer.lines.get(4)!.setCell(0, createCellData(0, 'e', 0));
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.buffer.scrollBottom = 3;
+5 -2
View File
@@ -12,7 +12,7 @@ import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, I
import { Buffer } from 'common/buffer/Buffer';
import * as Browser from 'common/Platform';
import { CoreBrowserTerminal } from 'browser/CoreBrowserTerminal';
import { IUnicodeService, IOptionsService, ICoreService, ICoreMouseService } from 'common/services/Services';
import { IUnicodeService, IOptionsService, ICoreService, IMouseStateService } from 'common/services/Services';
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
@@ -51,7 +51,7 @@ export class MockTerminal implements ITerminal {
public dimensions: IRenderDimensionsApi | undefined;
public markers!: IMarker[];
public linkifier: ILinkifier2 | undefined;
public coreMouseService!: ICoreMouseService;
public mouseStateService!: IMouseStateService;
public coreService!: ICoreService;
public optionsService!: IOptionsService;
public unicodeService!: IUnicodeService;
@@ -392,6 +392,9 @@ export class MockMouseService implements IMouseService {
public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
throw new Error('Not implemented');
}
public bindMouse(): void { }
public reset(): void { }
}
export class MockRenderService implements IRenderService {
+6 -12
View File
@@ -6,14 +6,13 @@
import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { ViewportConstants } from 'browser/shared/Constants';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, ICoreMouseService, IOptionsService } from 'common/services/Services';
import { IBufferService, IMouseStateService, IOptionsService } from 'common/services/Services';
import { CoreMouseEventType } from 'common/Types';
import { addDisposableListener, scheduleAtNextAnimationFrame } from 'browser/Dom';
import { scheduleAtNextAnimationFrame } from 'browser/Dom';
import { SmoothScrollableElement } from 'browser/scrollable/scrollableElement';
import type { IScrollableElementChangeOptions } from 'browser/scrollable/scrollableElementOptions';
import { Emitter, EventUtils } from 'common/Event';
import { Scrollable, ScrollbarVisibility, type IScrollEvent } from 'browser/scrollable/scrollable';
import { Gesture, EventType as GestureEventType, type IGestureEvent } from 'browser/scrollable/touch';
export class Viewport extends Disposable {
@@ -34,7 +33,7 @@ export class Viewport extends Disposable {
screenElement: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@ICoreBrowserService coreBrowserService: ICoreBrowserService,
@ICoreMouseService coreMouseService: ICoreMouseService,
@IMouseStateService mouseStateService: IMouseStateService,
@IThemeService themeService: IThemeService,
@IOptionsService private readonly _optionsService: IOptionsService,
@IRenderService private readonly _renderService: IRenderService
@@ -65,7 +64,7 @@ export class Viewport extends Disposable {
'scrollbar'
], () => this._scrollableElement.updateOptions(this._getChangeOptions())));
// Don't handle mouse wheel if wheel events are supported by the current mouse prototcol
this._register(coreMouseService.onProtocolChange(type => {
this._register(mouseStateService.onProtocolChange(type => {
this._scrollableElement.updateOptions({
handleMouseWheel: !(type & CoreMouseEventType.WHEEL)
});
@@ -107,9 +106,6 @@ export class Viewport extends Disposable {
this._register(this._scrollableElement.onScroll(e => this._handleScroll(e)));
// Touch/gesture scrolling support
this._register(Gesture.addTarget(screenElement));
this._register(addDisposableListener(screenElement, GestureEventType.CHANGE, (e: IGestureEvent) => this._handleGestureChange(e)));
}
public scrollLines(disp: number): void {
@@ -204,12 +200,10 @@ export class Viewport extends Disposable {
this._isHandlingScroll = false;
}
private _handleGestureChange(e: IGestureEvent): void {
e.preventDefault();
e.stopPropagation();
public handleTouchScroll(translationY: number): void {
const pos = this._scrollableElement.getScrollPosition();
this._scrollableElement.setScrollPosition({
scrollTop: pos.scrollTop - e.translationY
scrollTop: pos.scrollTop - translationY
});
}
}
@@ -51,8 +51,6 @@ export class OverviewRulerRenderer extends Disposable {
private _shouldUpdateAnchor: boolean | undefined = true;
private _lastKnownBufferLength: number = 0;
private _containerHeight: number | undefined;
constructor(
private readonly _viewportElement: HTMLElement,
private readonly _screenElement: HTMLElement,
@@ -91,13 +89,7 @@ export class OverviewRulerRenderer extends Disposable {
}
}));
// Container height changed
this._register(this._renderService.onRender((): void => {
if (!this._containerHeight || this._containerHeight !== this._screenElement.clientHeight) {
this._queueRefresh(true);
this._containerHeight = this._screenElement.clientHeight;
}
}));
this._register(this._renderService.onDimensionsChange(() => this._queueRefresh(true)));
this._register(this._coreBrowserService.onDprChange(() => this._queueRefresh(true)));
this._register(this._optionsService.onSpecificOptionChange('scrollbar', () => this._queueRefresh(true)));
@@ -144,10 +136,12 @@ export class OverviewRulerRenderer extends Disposable {
}
private _refreshCanvasDimensions(): void {
const cssCanvasHeight = this._renderService.dimensions.css.canvas.height;
const deviceCanvasHeight = this._renderService.dimensions.device.canvas.height;
this._canvas.style.width = `${this._width}px`;
this._canvas.width = Math.round(this._width * this._coreBrowserService.dpr);
this._canvas.style.height = `${this._screenElement.clientHeight}px`;
this._canvas.height = Math.round(this._screenElement.clientHeight * this._coreBrowserService.dpr);
this._canvas.style.height = `${cssCanvasHeight}px`;
this._canvas.height = deviceCanvasHeight;
this._refreshDrawConstants();
this._refreshColorZonePadding();
}
+9 -1
View File
@@ -87,7 +87,9 @@ export class CompositionHelper {
* @param ev The event.
*/
public compositionupdate(ev: Pick<CompositionEvent, 'data'>): void {
this._compositionView.textContent = ev.data;
// Mark text as LTR, direction=rtl is used in CSS so the end of the text is followed for long
// compositions
this._compositionView.textContent = `\u200E${ev.data}\u200E`;
this.updateCompositionElements();
setTimeout(() => {
const end = this._textarea.selectionEnd ?? this._textarea.value.length;
@@ -256,6 +258,12 @@ export class CompositionHelper {
this._compositionView.style.lineHeight = cellHeight + 'px';
this._compositionView.style.fontFamily = this._optionsService.rawOptions.fontFamily;
this._compositionView.style.fontSize = this._optionsService.rawOptions.fontSize + 'px';
// Limit the composition view width to the space between the cursor and
// the terminal's right edge, preventing it from overflowing the terminal.
const maxWidth = this._bufferService.cols * this._renderService.dimensions.css.cell.width - cursorLeft;
this._compositionView.style.maxWidth = maxWidth + 'px';
this._compositionView.style.overflow = 'hidden';
this._compositionView.style.direction = 'rtl';
// Sync the textarea to the exact position of the composition view so the IME knows where the
// text is.
const compositionViewBounds = this._compositionView.getBoundingClientRect();
+1 -1
View File
@@ -103,7 +103,7 @@ export class Terminal extends Disposable implements ITerminalApi {
public get modes(): IModes {
const m = this._core.coreService.decPrivateModes;
let mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any' = 'none';
switch (this._core.coreMouseService.activeProtocol) {
switch (this._core.mouseStateService.activeProtocol) {
case 'X10': mouseTrackingMode = 'x10'; break;
case 'VT200': mouseTrackingMode = 'vt200'; break;
case 'DRAG': mouseTrackingMode = 'drag'; break;
@@ -10,7 +10,7 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags,
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test';
import { MockCoreService, MockDecorationService, MockOptionsService, createCellData, NULL_CELL_DATA } from 'common/TestUtils.test';
import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } from 'browser/TestUtils.test';
import { TestWidthCache } from 'browser/renderer/dom/WidthCache.test';
@@ -48,9 +48,9 @@ describe('DomRendererRowFactory', () => {
it('should set correct attributes for double width characters', () => {
widthCache.setWidths({ '語': 10 });
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]));
lineData.setCell(0, createCellData(DEFAULT_ATTR, '語', 2));
// There should be no element for the following "empty" cell
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '', 0, 0]));
lineData.setCell(1, createCellData(DEFAULT_ATTR, '', 0));
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span>語</span>'
@@ -117,7 +117,7 @@ describe('DomRendererRowFactory', () => {
describe('attributes', () => {
it('should add class for bold', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BOLD;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
@@ -127,7 +127,7 @@ describe('DomRendererRowFactory', () => {
});
it('should add class for italic', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.ITALIC;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
@@ -137,7 +137,7 @@ describe('DomRendererRowFactory', () => {
});
it('should add class for dim', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.DIM;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
@@ -148,7 +148,7 @@ describe('DomRendererRowFactory', () => {
describe('underline', () => {
it('should add class for straight underline style', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.SINGLE;
@@ -159,7 +159,7 @@ describe('DomRendererRowFactory', () => {
);
});
it('should add class for double underline style', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.DOUBLE;
@@ -170,7 +170,7 @@ describe('DomRendererRowFactory', () => {
);
});
it('should add class for curly underline style', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.CURLY;
@@ -181,7 +181,7 @@ describe('DomRendererRowFactory', () => {
);
});
it('should add class for double dotted style', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.DOTTED;
@@ -192,7 +192,7 @@ describe('DomRendererRowFactory', () => {
);
});
it('should add class for dashed underline style', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.DASHED;
@@ -205,7 +205,7 @@ describe('DomRendererRowFactory', () => {
});
it('should add class for overline', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.OVERLINE;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
@@ -215,7 +215,7 @@ describe('DomRendererRowFactory', () => {
});
it('should add class for strikethrough', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.STRIKETHROUGH;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
@@ -225,7 +225,7 @@ describe('DomRendererRowFactory', () => {
});
it('should hide blinking text when blink is off', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BLINK | FgFlags.UNDERLINE;
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.SINGLE;
@@ -241,7 +241,7 @@ describe('DomRendererRowFactory', () => {
});
it('should add classes for 256 foreground colors', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.fg |= Attributes.CM_P256;
for (let i = 0; i < 256; i++) {
cell.fg &= ~Attributes.PCOLOR_MASK;
@@ -255,7 +255,7 @@ describe('DomRendererRowFactory', () => {
});
it('should add classes for 256 background colors', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.bg |= Attributes.CM_P256;
for (let i = 0; i < 256; i++) {
cell.bg &= ~Attributes.PCOLOR_MASK;
@@ -269,7 +269,7 @@ describe('DomRendererRowFactory', () => {
});
it('should correctly invert colors', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.fg |= Attributes.CM_P16 | 2 | FgFlags.INVERSE;
cell.bg |= Attributes.CM_P16 | 1;
lineData.setCell(0, cell);
@@ -280,7 +280,7 @@ describe('DomRendererRowFactory', () => {
});
it('should correctly invert default fg color', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.fg |= FgFlags.INVERSE;
cell.bg |= Attributes.CM_P16 | 1;
lineData.setCell(0, cell);
@@ -291,7 +291,7 @@ describe('DomRendererRowFactory', () => {
});
it('should correctly invert default bg color', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.fg |= Attributes.CM_P16 | 1 | FgFlags.INVERSE;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
@@ -301,7 +301,7 @@ describe('DomRendererRowFactory', () => {
});
it('should turn bold fg text bright', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.fg |= FgFlags.BOLD | Attributes.CM_P16;
for (let i = 0; i < 8; i++) {
cell.fg &= ~Attributes.PCOLOR_MASK;
@@ -315,7 +315,7 @@ describe('DomRendererRowFactory', () => {
});
it('should set style attribute for RBG', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3;
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
lineData.setCell(0, cell);
@@ -326,7 +326,7 @@ describe('DomRendererRowFactory', () => {
});
it('should correctly invert RGB colors', () => {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
const cell = createCellData(0, 'a', 1);
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);
@@ -339,8 +339,8 @@ describe('DomRendererRowFactory', () => {
describe('selectionForeground', () => {
it('should force selected cells with content to be rendered above the background', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
lineData.setCell(0, createCellData(DEFAULT_ATTR, 'a', 1));
lineData.setCell(1, createCellData(DEFAULT_ATTR, 'b', 1));
rowFactory.handleSelectionChanged([1, 0], [2, 0], false);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
@@ -348,7 +348,7 @@ describe('DomRendererRowFactory', () => {
);
});
it('should force whitespace cells to be rendered above the background', () => {
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, createCellData(DEFAULT_ATTR, 'a', 1));
rowFactory.handleSelectionChanged([0, 0], [2, 0], false);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
@@ -374,9 +374,9 @@ describe('DomRendererRowFactory', () => {
});
it('can merge codepoints for equal spacing', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)]));
lineData.setCell(0, createCellData(DEFAULT_ATTR, 'a', 1));
lineData.setCell(1, createCellData(DEFAULT_ATTR, 'b', 1));
lineData.setCell(2, createCellData(DEFAULT_ATTR, 'c', 1));
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span>abc</span>'
@@ -385,9 +385,9 @@ describe('DomRendererRowFactory', () => {
it('should not merge codepoints with different spacing', () => {
widthCache.setWidths({ '€': 2 });
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '€', 1, '€'.charCodeAt(0)]));
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)]));
lineData.setCell(0, createCellData(DEFAULT_ATTR, 'a', 1));
lineData.setCell(1, createCellData(DEFAULT_ATTR, '€', 1));
lineData.setCell(2, createCellData(DEFAULT_ATTR, 'c', 1));
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span>a</span><span style="letter-spacing: 3px;">€</span><span>c</span>'
@@ -395,9 +395,9 @@ describe('DomRendererRowFactory', () => {
});
it('should not merge on FG change', () => {
const aColor1 = CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]);
const aColor1 = createCellData(DEFAULT_ATTR, 'a', 1);
aColor1.fg |= Attributes.CM_P16 | 1;
const bColor2 = CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]);
const bColor2 = createCellData(DEFAULT_ATTR, 'b', 1);
bColor2.fg |= Attributes.CM_P16 | 2;
lineData.setCell(0, aColor1);
lineData.setCell(1, aColor1);
@@ -410,11 +410,11 @@ describe('DomRendererRowFactory', () => {
});
it('should not merge cursor cell', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'X', 1, 'X'.charCodeAt(0)]));
lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
lineData.setCell(0, createCellData(DEFAULT_ATTR, 'a', 1));
lineData.setCell(1, createCellData(DEFAULT_ATTR, 'a', 1));
lineData.setCell(2, createCellData(DEFAULT_ATTR, 'X', 1));
lineData.setCell(3, createCellData(DEFAULT_ATTR, 'b', 1));
lineData.setCell(4, createCellData(DEFAULT_ATTR, 'b', 1));
const spans = rowFactory.createRow(lineData, 0, true, undefined, undefined, 2, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span>aa</span><span class="xterm-cursor xterm-cursor-block">X</span><span>bb</span>'
@@ -453,7 +453,7 @@ describe('DomRendererRowFactory', () => {
assert.equal(extractHtml(spans),
'<span class="xterm-bg-1"> </span>'
);
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(4, createCellData(DEFAULT_ATTR, 'a', 1));
spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-bg-1"> </span><span>a</span>'
@@ -462,9 +462,9 @@ describe('DomRendererRowFactory', () => {
it('should apply correct positive or negative spacing', () => {
widthCache.setWidths({ '€': 2, '語': 10, '𝄞': 7 }); // €: too small (+3px), 語: exact, 𝄞: too wide (-2px)
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '€', 1, '€'.charCodeAt(0)]));
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)]));
lineData.setCell(0, createCellData(DEFAULT_ATTR, 'a', 1));
lineData.setCell(1, createCellData(DEFAULT_ATTR, '€', 1));
lineData.setCell(2, createCellData(DEFAULT_ATTR, 'c', 1));
lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, '語', 2, 'c'.charCodeAt(0)]));
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, '𝄞', 1, 'c'.charCodeAt(0)]));
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
@@ -474,13 +474,13 @@ describe('DomRendererRowFactory', () => {
});
it('should not merge across link borders', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
lineData.setCell(5, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
lineData.setCell(6, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
lineData.setCell(0, createCellData(DEFAULT_ATTR, 'a', 1));
lineData.setCell(1, createCellData(DEFAULT_ATTR, 'a', 1));
lineData.setCell(2, createCellData(DEFAULT_ATTR, 'x', 1));
lineData.setCell(3, createCellData(DEFAULT_ATTR, 'x', 1));
lineData.setCell(4, createCellData(DEFAULT_ATTR, 'x', 1));
lineData.setCell(5, createCellData(DEFAULT_ATTR, 'b', 1));
lineData.setCell(6, createCellData(DEFAULT_ATTR, 'b', 1));
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, 2, 4);
assert.equal(extractHtml(spans),
'<span>aa</span><span style="text-decoration: underline;">xxx</span><span>bb</span>'
@@ -488,10 +488,10 @@ describe('DomRendererRowFactory', () => {
});
it('empty cells included in link underline', () => {
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
lineData.setCell(0, createCellData(DEFAULT_ATTR, 'a', 1));
lineData.setCell(1, createCellData(DEFAULT_ATTR, 'a', 1));
lineData.setCell(2, createCellData(DEFAULT_ATTR, 'x', 1));
lineData.setCell(4, createCellData(DEFAULT_ATTR, 'x', 1));
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, 2, 4);
assert.equal(extractHtml(spans),
'<span>aa</span><span style="text-decoration: underline;">x x</span>'
@@ -500,7 +500,7 @@ describe('DomRendererRowFactory', () => {
it('link range gets capped to actual line borders', () => {
for (let i = 0; i < 10; ++i) {
lineData.setCell(i, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
lineData.setCell(i, createCellData(DEFAULT_ATTR, 'a', 1));
}
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -100, 100);
assert.equal(extractHtml(spans),
@@ -519,7 +519,7 @@ describe('DomRendererRowFactory', () => {
function createEmptyLineData(cols: number): IBufferLine {
const lineData = new BufferLine(cols);
for (let i = 0; i < cols; i++) {
lineData.setCell(i, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]));
lineData.setCell(i, NULL_CELL_DATA);
}
return lineData;
}
+4
View File
@@ -153,6 +153,8 @@ export interface IGestureEvent extends MouseEvent {
translationY: number;
pageX: number;
pageY: number;
clientX: number;
clientY: number;
tapCount: number;
}
@@ -459,6 +461,8 @@ export class Gesture extends Disposable {
evt.translationY = touch.pageY - tail(data.rollingPageY)!;
evt.pageX = touch.pageX;
evt.pageY = touch.pageY;
evt.clientX = touch.clientX;
evt.clientY = touch.clientY;
this._dispatchEvent(evt);
if (data.rollingPageX.length > 3) {
@@ -9,7 +9,7 @@ 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';
import { MockBufferService, createCellData } from 'common/TestUtils.test';
describe('CharacterJoinerService', () => {
let service: ICharacterJoinerService;
@@ -25,17 +25,17 @@ describe('CharacterJoinerService', () => {
lines.set(4, new BufferLine(0));
lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]]));
const line6 = lineData([['wi']]);
line6.resize(line6.length + 1, CellData.fromCharData([0, '¥', 2, '¥'.charCodeAt(0)]));
line6.resize(line6.length + 1, CellData.fromCharData([0, '', 0, 0]));
line6.resize(line6.length + 1, createCellData(0, '¥', 2));
line6.resize(line6.length + 1, createCellData(0, '', 0));
let sub = lineData([['deemo']]);
let oldSize = line6.length;
line6.resize(oldSize + sub.length, CellData.fromCharData([0, '', 0, 0]));
line6.resize(oldSize + sub.length, createCellData(0, '', 0));
for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData()));
line6.resize(line6.length + 1, CellData.fromCharData([0, '\xf0\x9f\x98\x81', 1, 128513]));
line6.resize(line6.length + 1, CellData.fromCharData([0, ' ', 1, ' '.charCodeAt(0)]));
line6.resize(line6.length + 1, createCellData(0, ' ', 1));
sub = lineData([['jiabc']]);
oldSize = line6.length;
line6.resize(oldSize + sub.length, CellData.fromCharData([0, '', 0, 0]));
line6.resize(oldSize + sub.length, createCellData(0, '', 0));
for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData()));
lines.set(6, line6);
@@ -272,8 +272,8 @@ function lineData(data: IPartialLineData[]): IBufferLine {
const line = data[i][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)])));
tline.resize(tline.length + line.split('').length, createCellData(0, '', 0));
line.split('').map((char, idx) => tline.setCell(idx + offset, createCellData(attr, char, 1)));
}
return tline;
}
+3 -1
View File
@@ -13,7 +13,7 @@ export class CoreBrowserService extends Disposable implements ICoreBrowserServic
private _isFocused = false;
private _cachedIsFocused: boolean | undefined = undefined;
private _screenDprMonitor = this._register(new ScreenDprMonitor(this._window));
private _screenDprMonitor: ScreenDprMonitor;
private readonly _onDprChange = this._register(new Emitter<number>());
public readonly onDprChange = this._onDprChange.event;
@@ -27,6 +27,8 @@ export class CoreBrowserService extends Disposable implements ICoreBrowserServic
) {
super();
this._screenDprMonitor = this._register(new ScreenDprMonitor(this._window));
// Monitor device pixel ratio
this._register(this.onWindowChange(w => this._screenDprMonitor.setWindow(w)));
this._register(EventUtils.forward(this._screenDprMonitor.onDprChange, this._onDprChange));
@@ -0,0 +1,47 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { getWindow } from 'browser/Dom';
import { getCoords, getCoordsRelativeToElement } from 'browser/input/Mouse';
import { ICharSizeService, IMouseCoordsService, IRenderService } from 'browser/services/Services';
export class MouseCoordsService implements IMouseCoordsService {
public serviceBrand: undefined;
constructor(
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IRenderService private readonly _renderService: IRenderService
) {
}
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
return getCoords(
window,
event,
element,
colCount,
rowCount,
this._charSizeService.hasValidSize,
this._renderService.dimensions.css.cell.width,
this._renderService.dimensions.css.cell.height,
isSelection
);
}
public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
const coords = getCoordsRelativeToElement(getWindow(element), event, element);
if (!this._charSizeService.hasValidSize) {
return undefined;
}
coords[0] = Math.min(Math.max(coords[0], 0), this._renderService.dimensions.css.canvas.width - 1);
coords[1] = Math.min(Math.max(coords[1], 0), this._renderService.dimensions.css.canvas.height - 1);
return {
col: Math.floor(coords[0] / this._renderService.dimensions.css.cell.width),
row: Math.floor(coords[1] / this._renderService.dimensions.css.cell.height),
x: Math.floor(coords[0]),
y: Math.floor(coords[1])
};
}
}
+213
View File
@@ -0,0 +1,213 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { MouseService } from 'browser/services/MouseService';
import { MouseStateService } from 'common/services/MouseStateService';
import { CoreMouseAction, CoreMouseButton } from 'common/Types';
import { IBufferService, ICoreService, ILogService, IOptionsService } from 'common/services/Services';
import { MockCoreBrowserService, MockRenderService, MockSelectionService } from 'browser/TestUtils.test';
function toBytes(s: string | undefined): number[] {
if (!s) {
return [];
}
const res: number[] = [];
for (let i = 0; i < s.length; ++i) {
res.push(s.charCodeAt(i));
}
return res;
}
// Minimal mocks for deps that MouseService touches in these tests
const bufferService: IBufferService = {
buffer: { hasScrollback: true } as any,
cols: 500,
rows: 500
} as any;
const optionsService: IOptionsService = {
rawOptions: {
logLevel: 'info',
fastScrollSensitivity: 1,
scrollSensitivity: 1
}
} as any;
const logService: ILogService = {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {}
} as any;
describe('MouseService _triggerMouseEvent', () => {
let mouseService: MouseService;
let mouseStateService: MouseStateService;
let coreService: ICoreService;
let reports: string[];
beforeEach(() => {
reports = [];
mouseStateService = new MouseStateService();
coreService = {
triggerDataEvent: (data: string) => reports.push(data),
triggerBinaryEvent: (data: string) => reports.push(data),
decPrivateModes: { applicationCursorKeys: false }
} as any;
mouseService = new MouseService(
new MockRenderService(),
{
getMouseReportCoords: (_ev: MouseEvent, _el: HTMLElement) => ({ col: 0, row: 0, x: 0, y: 0 })
} as any,
mouseStateService,
coreService,
bufferService,
optionsService,
new MockSelectionService(),
logService,
new MockCoreBrowserService()
);
});
function trigger(e: Parameters<any>[0]): boolean {
return (mouseService as any)._triggerMouseEvent(e);
}
it('NONE', () => {
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), false);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), false);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
});
it('X10', () => {
mouseStateService.activeProtocol = 'X10';
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
});
it('VT200', () => {
mouseStateService.activeProtocol = 'VT200';
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
});
it('DRAG', () => {
mouseStateService.activeProtocol = 'DRAG';
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
});
it('ANY', () => {
mouseStateService.activeProtocol = 'ANY';
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), true);
// should not report in any case
// invalid button + action combinations
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.MOVE }), false);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.DOWN }), false);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.UP }), false);
// invalid coords
assert.equal(trigger({ col: -1, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(trigger({ col: 500, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(trigger({ col: 0, row: -1, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(trigger({ col: 0, row: 500, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
});
describe('coords', () => {
it('DEFAULT encoding', () => {
mouseStateService.activeProtocol = 'ANY';
for (let i = 0; i < bufferService.cols; ++i) {
assert.equal(trigger({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
if (i > 222) {
// supress mouse reports if we are out of addressible range (max. 222)
assert.deepEqual(toBytes(reports.pop()), []);
} else {
assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]);
}
}
});
it('SGR encoding', () => {
mouseStateService.activeProtocol = 'ANY';
mouseStateService.activeEncoding = 'SGR';
for (let i = 0; i < bufferService.cols; ++i) {
assert.equal(trigger({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.deepEqual(reports.pop(), `\x1b[<0;${i + 1};1M`);
}
});
it('SGR_PIXELS encoding', () => {
mouseStateService.activeProtocol = 'ANY';
mouseStateService.activeEncoding = 'SGR_PIXELS';
for (let i = 0; i < 500; ++i) {
assert.equal(trigger({ col: 0, row: 0, x: i, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.deepEqual(reports.pop(), `\x1b[<0;${i};0M`);
}
});
});
it('eventCodes with modifiers (DEFAULT encoding)', () => {
// TODO: implement AUX button tests
mouseStateService.activeProtocol = 'ANY';
mouseStateService.activeEncoding = 'DEFAULT';
// all buttons + down + no modifer
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
assert.deepEqual(reports, ['\x1b[M !!', '\x1b[M!!!', '\x1b[M"!!', '\x1b[Ma!!']);
reports = [];
// all buttons + up + no modifier
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
assert.deepEqual(reports, ['\x1b[M#!!', '\x1b[M#!!', '\x1b[M#!!', '\x1b[M`!!']);
reports = [];
// all buttons + move + no modifier
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
assert.deepEqual(reports, ['\x1b[M@!!', '\x1b[MA!!', '\x1b[MB!!', '\x1b[MC!!']);
reports = [];
// button none + move + modifiers
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: false, shift: false }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: false }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: true }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: false }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: true }), true);
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: true }), true);
assert.deepEqual(reports, ['\x1b[MS!!', '\x1b[MK!!', '\x1b[MG!!', '\x1b[M[!!', '\x1b[MO!!', '\x1b[M_!!']);
reports = [];
});
});
+518 -26
View File
@@ -3,45 +3,537 @@
* @license MIT
*/
import { getWindow } from 'browser/Dom';
import { ICharSizeService, IRenderService, IMouseService } from './Services';
import { getCoords, getCoordsRelativeToElement } from 'browser/input/Mouse';
import { addDisposableListener } from 'browser/Dom';
import { IBufferService, IMouseStateService, ICoreService, ILogService, IOptionsService } from 'common/services/Services';
import { CoreMouseAction, CoreMouseButton, CoreMouseEventType, ICoreMouseEvent, IDisposable } from 'common/Types';
import { C0 } from 'common/data/EscapeSequences';
import { toDisposable } from 'common/Lifecycle';
import { ICoreBrowserService, IMouseCoordsService, IMouseService, IMouseServiceTarget, IRenderService, ISelectionService } from './Services';
import { Gesture, EventType as GestureEventType, IGestureEvent } from 'browser/scrollable/touch';
type RequestedMouseEvents = Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener | null>;
interface IMouseBindContext {
readonly target: IMouseServiceTarget;
readonly focus: () => void;
readonly requestedEvents: RequestedMouseEvents;
}
export class MouseService implements IMouseService {
public serviceBrand: undefined;
private _lastEvent: ICoreMouseEvent | null = null;
private _wheelPartialScroll: number = 0;
private _touchScrollAccumulator: number = 0;
constructor(
@IRenderService private readonly _renderService: IRenderService,
@ICharSizeService private readonly _charSizeService: ICharSizeService
@IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
@IMouseStateService private readonly _mouseStateService: IMouseStateService,
@ICoreService private readonly _coreService: ICoreService,
@IBufferService private readonly _bufferService: IBufferService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ISelectionService private readonly _selectionService: ISelectionService,
@ILogService private readonly _logService: ILogService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
) {
}
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
return getCoords(
window,
event,
element,
colCount,
rowCount,
this._charSizeService.hasValidSize,
this._renderService.dimensions.css.cell.width,
this._renderService.dimensions.css.cell.height,
isSelection
);
public bindMouse(target: IMouseServiceTarget, register: (disposable: IDisposable) => void, focus: () => void): void {
const { element, document } = target;
/**
* Event listener state handling.
* We listen to the onProtocolChange event of MouseStateService and put
* requested listeners in `requestedEvents`. With this the listeners
* have all bits to do the event listener juggling.
* Note: 'mousedown' currently is "always on" and not managed
* by onProtocolChange.
*/
const requestedEvents: RequestedMouseEvents = {
mouseup: null,
wheel: null,
mousedrag: null,
mousemove: null
};
const ctx: IMouseBindContext = { target, focus, requestedEvents };
const eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener> = {
mouseup: (ev: Event) => this._handleMouseUp(ctx, ev as MouseEvent),
wheel: (ev: Event) => this._handleWheel(ctx, ev as WheelEvent),
mousedrag: (ev: Event) => this._handleMouseDrag(ctx, ev as MouseEvent),
mousemove: (ev: Event) => this._handleMouseMove(ctx, ev as MouseEvent)
};
register(this._mouseStateService.onProtocolChange(events => {
this._handleProtocolChange(ctx, eventListeners, events);
}));
// force initial onProtocolChange so we dont miss early mouse requests
this._mouseStateService.activeProtocol = this._mouseStateService.activeProtocol;
// Ensure document-level listeners are removed on dispose
register(toDisposable(() => {
if (requestedEvents.mouseup) {
document.removeEventListener('mouseup', requestedEvents.mouseup);
}
if (requestedEvents.mousedrag) {
document.removeEventListener('mousemove', requestedEvents.mousedrag);
}
}));
/**
* "Always on" event listeners.
*/
register(addDisposableListener(element, 'mousedown', (ev: MouseEvent) => this._handleMouseDown(ctx, ev)));
register(addDisposableListener(element, 'wheel', (ev: WheelEvent) => this._handlePassiveWheel(ctx, ev), { passive: false }));
register(Gesture.addTarget(target.screenElement));
register(addDisposableListener(target.screenElement, GestureEventType.START, () => this._handleTouchStart()));
register(addDisposableListener(target.screenElement, GestureEventType.CHANGE, (e: IGestureEvent) => this._handleTouchChange(ctx, e)));
}
public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
const coords = getCoordsRelativeToElement(getWindow(element), event, element);
if (!this._charSizeService.hasValidSize) {
return undefined;
private _sendEvent(ctx: IMouseBindContext, ev: MouseEvent | WheelEvent): boolean {
// Get mouse coordinates
const pos = this._mouseCoordsService.getMouseReportCoords(ev as MouseEvent, ctx.target.screenElement);
if (!pos) {
return false;
}
coords[0] = Math.min(Math.max(coords[0], 0), this._renderService.dimensions.css.canvas.width - 1);
coords[1] = Math.min(Math.max(coords[1], 0), this._renderService.dimensions.css.canvas.height - 1);
let but: CoreMouseButton;
let action: CoreMouseAction | undefined;
switch ((ev as MouseEvent & { overrideType?: string }).overrideType || ev.type) {
case 'mousemove':
action = CoreMouseAction.MOVE;
if (ev.buttons === undefined) {
// buttons is not supported on macOS, try to get a value from button instead
but = CoreMouseButton.NONE;
if (ev.button !== undefined) {
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
}
} else {
// according to MDN buttons only reports up to button 5 (AUX2)
but = ev.buttons & 1 ? CoreMouseButton.LEFT :
ev.buttons & 4 ? CoreMouseButton.MIDDLE :
ev.buttons & 2 ? CoreMouseButton.RIGHT :
CoreMouseButton.NONE; // fallback to NONE
}
break;
case 'mouseup':
action = CoreMouseAction.UP;
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
break;
case 'mousedown':
action = CoreMouseAction.DOWN;
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
break;
case 'wheel':
if (!this._mouseStateService.allowCustomWheelEvent(ev as WheelEvent)) {
return false;
}
const deltaY = (ev as WheelEvent).deltaY;
if (deltaY === 0) {
return false;
}
const lines = this._consumeWheelEvent(
ev as WheelEvent,
this._renderService?.dimensions?.device?.cell?.height,
this._coreBrowserService?.dpr
);
if (lines === 0) {
return false;
}
action = deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
but = CoreMouseButton.WHEEL;
break;
default:
// dont handle other event types by accident
return false;
}
// exit if we cannot determine valid button/action values
// do nothing for higher buttons than wheel
if (action === undefined || but === undefined || but > CoreMouseButton.WHEEL) {
return false;
}
return this._triggerMouseEvent({
col: pos.col,
row: pos.row,
x: pos.x,
y: pos.y,
button: but,
action,
ctrl: ev.ctrlKey,
alt: ev.altKey,
shift: ev.shiftKey
});
}
private _handleMouseUp(ctx: IMouseBindContext, ev: MouseEvent): void {
this._sendEvent(ctx, ev);
if (!ev.buttons) {
// if no other button is held remove global handlers
if (ctx.requestedEvents.mouseup) {
ctx.target.document.removeEventListener('mouseup', ctx.requestedEvents.mouseup);
}
if (ctx.requestedEvents.mousedrag) {
ctx.target.document.removeEventListener('mousemove', ctx.requestedEvents.mousedrag);
}
}
}
private _handleWheel(ctx: IMouseBindContext, ev: WheelEvent): false {
this._sendEvent(ctx, ev);
ev.preventDefault();
ev.stopPropagation();
return false;
}
private _handleMouseDrag(ctx: IMouseBindContext, ev: MouseEvent): void {
// deal only with move while a button is held
if (ev.buttons) {
this._sendEvent(ctx, ev);
}
}
private _handleMouseMove(ctx: IMouseBindContext, ev: MouseEvent): void {
// deal only with move without any button
if (!ev.buttons) {
this._sendEvent(ctx, ev);
}
}
private _handleMouseDown(ctx: IMouseBindContext, ev: MouseEvent): void {
ev.preventDefault();
ctx.focus();
// Don't send the mouse button to the pty if mouse events are disabled or
// if the selection manager is having selection forced (ie. a modifier is
// held).
if (!this._mouseStateService.areMouseEventsActive || this._selectionService.shouldForceSelection(ev)) {
return;
}
this._sendEvent(ctx, ev);
// Register additional global handlers which should keep reporting outside
// of the terminal element.
// Note: Other emulators also do this for 'mousedown' while a button
// is held, we currently limit 'mousedown' to the terminal only.
if (ctx.requestedEvents.mouseup) {
ctx.target.document.addEventListener('mouseup', ctx.requestedEvents.mouseup);
}
if (ctx.requestedEvents.mousedrag) {
ctx.target.document.addEventListener('mousemove', ctx.requestedEvents.mousedrag);
}
}
private _handlePassiveWheel(ctx: IMouseBindContext, ev: WheelEvent): false | void {
// do nothing, if app side handles wheel itself
if (ctx.requestedEvents.wheel) {
return;
}
if (!this._mouseStateService.allowCustomWheelEvent(ev)) {
return false;
}
if (!this._bufferService.buffer.hasScrollback) {
// Convert wheel events into up/down events when the buffer does not have scrollback, this
// enables scrolling in apps hosted in the alt buffer such as vim or tmux even when mouse
// events are not enabled.
// This used implementation used get the actual lines/partial lines scrolled from the
// viewport but since moving to the new viewport implementation has been simplified to
// simply send a single up or down sequence.
// Do nothing if there's no vertical scroll
const deltaY = ev.deltaY;
if (deltaY === 0) {
return false;
}
const lines = this._consumeWheelEvent(
ev,
this._renderService?.dimensions?.device?.cell?.height,
this._coreBrowserService?.dpr
);
if (lines === 0) {
ev.preventDefault();
ev.stopPropagation();
return false;
}
// Construct and send sequences
const sequence = C0.ESC + (this._coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
this._coreService.triggerDataEvent(sequence, true);
ev.preventDefault();
ev.stopPropagation();
return false;
}
}
private _handleTouchStart(): void {
this._touchScrollAccumulator = 0;
}
private _handleTouchChange(ctx: IMouseBindContext, e: IGestureEvent): void {
e.preventDefault();
e.stopPropagation();
// When mouse protocol has wheel events active, send as mouse wheel events.
if (ctx.requestedEvents.wheel) {
this._handleTouchScrollAsWheel(ctx, e);
return;
}
// When in alt buffer (no scrollback), send up/down key sequences.
if (!this._bufferService.buffer.hasScrollback) {
this._handleTouchScrollAsKeys(e);
return;
}
// Normal scrollback: delegate to viewport scrolling when available.
ctx.target.handleTouchScroll?.(e.translationY);
}
private _handleTouchScrollAsKeys(e: IGestureEvent): void {
const cellHeight = this._renderService?.dimensions.css.cell.height;
if (!cellHeight) {
return;
}
this._touchScrollAccumulator -= e.translationY;
const lines = Math.trunc(this._touchScrollAccumulator / cellHeight);
if (lines === 0) {
return;
}
this._touchScrollAccumulator -= lines * cellHeight;
const sequence = C0.ESC
+ (this._coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[')
+ (lines < 0 ? 'A' : 'B');
for (let i = 0; i < Math.abs(lines); i++) {
this._coreService.triggerDataEvent(sequence, true);
}
}
private _handleTouchScrollAsWheel(ctx: IMouseBindContext, e: IGestureEvent): void {
const cellHeight = this._renderService?.dimensions.css.cell.height;
if (!cellHeight) {
return;
}
this._touchScrollAccumulator -= e.translationY;
const lines = Math.trunc(this._touchScrollAccumulator / cellHeight);
if (lines === 0) {
return;
}
this._touchScrollAccumulator -= lines * cellHeight;
const pos = this._mouseCoordsService.getMouseReportCoords(e, ctx.target.screenElement);
if (!pos) {
return;
}
for (let i = 0; i < Math.abs(lines); i++) {
this._triggerMouseEvent({
col: pos.col,
row: pos.row,
x: pos.x,
y: pos.y,
button: CoreMouseButton.WHEEL,
action: lines < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN,
ctrl: false,
alt: false,
shift: false
});
}
}
public reset(): void {
this._lastEvent = null;
this._wheelPartialScroll = 0;
this._touchScrollAccumulator = 0;
}
private _handleProtocolChange(ctx: IMouseBindContext, eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener>, events: CoreMouseEventType): void {
const { element, document } = ctx.target;
const { requestedEvents } = ctx;
// apply global changes on events
if (events) {
if (this._optionsService.rawOptions.logLevel === 'debug') {
this._logService.debug('Binding to mouse events:', this._explainEvents(events));
}
element.classList.add('enable-mouse-events');
this._selectionService.disable();
} else {
this._logService.debug('Unbinding from mouse events.');
element.classList.remove('enable-mouse-events');
this._selectionService.enable();
}
// add/remove handlers from requestedEvents
if (!(events & CoreMouseEventType.MOVE)) {
if (requestedEvents.mousemove) {
element.removeEventListener('mousemove', requestedEvents.mousemove);
}
requestedEvents.mousemove = null;
} else if (!requestedEvents.mousemove) {
element.addEventListener('mousemove', eventListeners.mousemove);
requestedEvents.mousemove = eventListeners.mousemove;
}
if (!(events & CoreMouseEventType.WHEEL)) {
if (requestedEvents.wheel) {
element.removeEventListener('wheel', requestedEvents.wheel);
}
requestedEvents.wheel = null;
} else if (!requestedEvents.wheel) {
element.addEventListener('wheel', eventListeners.wheel, { passive: false });
requestedEvents.wheel = eventListeners.wheel;
}
if (!(events & CoreMouseEventType.UP)) {
if (requestedEvents.mouseup) {
document.removeEventListener('mouseup', requestedEvents.mouseup);
}
requestedEvents.mouseup = null;
} else {
requestedEvents.mouseup ??= eventListeners.mouseup;
}
if (!(events & CoreMouseEventType.DRAG)) {
if (requestedEvents.mousedrag) {
document.removeEventListener('mousemove', requestedEvents.mousedrag);
}
requestedEvents.mousedrag = null;
} else {
requestedEvents.mousedrag ??= eventListeners.mousedrag;
}
}
private _applyScrollModifier(amount: number, ev: WheelEvent): number {
// Multiply the scroll speed when the modifier key is pressed
if (ev.altKey || ev.ctrlKey || ev.shiftKey) {
return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity;
}
return amount * this._optionsService.rawOptions.scrollSensitivity;
}
/**
* Processes a wheel event, accounting for partial scrolls for trackpad, mouse scrolls.
* This prevents hyper-sensitive scrolling in alt buffer.
*/
private _consumeWheelEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number {
// Do nothing if it's not a vertical scroll event
if (ev.deltaY === 0 || ev.shiftKey) {
return 0;
}
if (cellHeight === undefined || dpr === undefined) {
return 0;
}
const targetWheelEventPixels = cellHeight / dpr;
let amount = this._applyScrollModifier(ev.deltaY, ev);
if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
amount /= (targetWheelEventPixels + 0.0); // Prevent integer division
const isLikelyTrackpad = Math.abs(ev.deltaY) < 50;
if (isLikelyTrackpad) {
amount *= 0.3;
}
this._wheelPartialScroll += amount;
amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1);
this._wheelPartialScroll %= 1;
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
amount *= this._bufferService.rows;
}
return amount;
}
/**
* Triggers a mouse event to be sent.
*
* Returns true if the event passed all protocol restrictions and a report
* was sent, otherwise false. The return value may be used to decide whether
* the default event action in the browser component should be omitted.
*
* Note: The method will change values of the given event object
* to fulfill protocol and encoding restrictions.
*/
private _triggerMouseEvent(e: ICoreMouseEvent): boolean {
// range check for col/row
if (e.col < 0 || e.col >= this._bufferService.cols
|| e.row < 0 || e.row >= this._bufferService.rows) {
return false;
}
// filter nonsense combinations of button + action
if (e.button === CoreMouseButton.WHEEL && e.action === CoreMouseAction.MOVE) {
return false;
}
if (e.button === CoreMouseButton.NONE && e.action !== CoreMouseAction.MOVE) {
return false;
}
if (e.button !== CoreMouseButton.WHEEL && (e.action === CoreMouseAction.LEFT || e.action === CoreMouseAction.RIGHT)) {
return false;
}
// report 1-based coords
e.col++;
e.row++;
// debounce move events at grid or pixel level
if (e.action === CoreMouseAction.MOVE
&& this._lastEvent
&& this._equalEvents(this._lastEvent, e, this._mouseStateService.isPixelEncoding)
) {
return false;
}
// apply protocol restrictions
if (!this._mouseStateService.restrictMouseEvent(e)) {
return false;
}
// encode report and send
const report = this._mouseStateService.encodeMouseEvent(e);
if (report) {
if (this._mouseStateService.isDefaultEncoding) {
this._coreService.triggerBinaryEvent(report);
} else {
this._coreService.triggerDataEvent(report, true);
}
}
this._lastEvent = e;
return true;
}
private _explainEvents(events: CoreMouseEventType): { [event: string]: boolean } {
return {
col: Math.floor(coords[0] / this._renderService.dimensions.css.cell.width),
row: Math.floor(coords[1] / this._renderService.dimensions.css.cell.height),
x: Math.floor(coords[0]),
y: Math.floor(coords[1])
down: !!(events & CoreMouseEventType.DOWN),
up: !!(events & CoreMouseEventType.UP),
drag: !!(events & CoreMouseEventType.DRAG),
move: !!(events & CoreMouseEventType.MOVE),
wheel: !!(events & CoreMouseEventType.WHEEL)
};
}
private _equalEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent, pixels: boolean): boolean {
if (pixels) {
if (e1.x !== e2.x) return false;
if (e1.y !== e2.y) return false;
} else {
if (e1.col !== e2.col) return false;
if (e1.row !== e2.row) return false;
}
if (e1.button !== e2.button) return false;
if (e1.action !== e2.action) return false;
if (e1.ctrl !== e2.ctrl) return false;
if (e1.alt !== e2.alt) return false;
if (e1.shift !== e2.shift) return false;
return true;
}
}
@@ -7,7 +7,7 @@ import { assert } from 'chai';
import { SelectionService, SelectionMode } from './SelectionService';
import { SelectionModel } from 'browser/selection/SelectionModel';
import { IBufferLine } from 'common/Types';
import { MockBufferService, MockOptionsService, MockCoreService } from 'common/TestUtils.test';
import { MockBufferService, MockOptionsService, MockCoreService, createCellData } from 'common/TestUtils.test';
import { BufferLine } from 'common/buffer/BufferLine';
import { IBufferService, IOptionsService } from 'common/services/Services';
import { MockCoreBrowserService, MockMouseService, MockRenderService } from 'browser/TestUtils.test';
@@ -57,14 +57,14 @@ describe('SelectionService', () => {
function stringToRow(text: string): IBufferLine {
const result = new BufferLine(text.length);
for (let i = 0; i < text.length; i++) {
result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)]));
result.setCell(i, createCellData(0, text.charAt(i), 1));
}
return result;
}
function stringArrayToRow(chars: string[]): IBufferLine {
const line = new BufferLine(chars.length);
chars.map((c, idx) => line.setCell(idx, CellData.fromCharData([0, c, 1, c.charCodeAt(0)])));
chars.map((c, idx) => line.setCell(idx, createCellData(0, c, 1)));
return line;
}
+4 -4
View File
@@ -8,7 +8,7 @@ import { getCoordsRelativeToElement } from 'browser/input/Mouse';
import { moveToCellSequence } from 'browser/input/MoveToCell';
import { SelectionModel } from 'browser/selection/SelectionModel';
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
import { ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
import { ICoreBrowserService, IMouseCoordsService, IRenderService, ISelectionService } from 'browser/services/Services';
import { Disposable, toDisposable } from 'common/Lifecycle';
import * as Browser from 'common/Platform';
import { IBufferLine, ICellData, IDisposable } from 'common/Types';
@@ -126,7 +126,7 @@ export class SelectionService extends Disposable implements ISelectionService {
private readonly _linkifier: ILinkifier2,
@IBufferService private readonly _bufferService: IBufferService,
@ICoreService private readonly _coreService: ICoreService,
@IMouseService private readonly _mouseService: IMouseService,
@IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
@IOptionsService private readonly _optionsService: IOptionsService,
@IRenderService private readonly _renderService: IRenderService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
@@ -395,7 +395,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* @param event The mouse event.
*/
private _getMouseBufferCoords(event: MouseEvent): [number, number] | undefined {
const coords = this._mouseService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true);
const coords = this._mouseCoordsService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true);
if (!coords) {
return undefined;
}
@@ -715,7 +715,7 @@ export class SelectionService extends Disposable implements ISelectionService {
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME && event.altKey && this._optionsService.rawOptions.altClickMovesCursor) {
if (this._bufferService.buffer.ybase === this._bufferService.buffer.ydisp) {
const coordinates = this._mouseService.getCoords(
const coordinates = this._mouseCoordsService.getCoords(
event,
this._element,
this._bufferService.cols,
+16 -2
View File
@@ -49,14 +49,28 @@ export interface ICoreBrowserService {
readonly dpr: number;
}
export const IMouseService = createDecorator<IMouseService>('MouseService');
export interface IMouseService {
export const IMouseCoordsService = createDecorator<IMouseCoordsService>('MouseCoordsService');
export interface IMouseCoordsService {
serviceBrand: undefined;
getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined;
getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined;
}
export const IMouseService = createDecorator<IMouseService>('MouseService');
export interface IMouseService {
serviceBrand: undefined;
bindMouse(target: IMouseServiceTarget, register: (disposable: IDisposable) => void, focus: () => void): void;
reset(): void;
}
export interface IMouseServiceTarget {
element: HTMLElement;
screenElement: HTMLElement;
document: Document;
handleTouchScroll?(amount: number): void;
}
export const IRenderService = createDecorator<IRenderService>('RenderService');
export interface IRenderService extends IDisposable {
serviceBrand: undefined;
+3 -2
View File
@@ -9,9 +9,10 @@
"types": [
"../../node_modules/@types/mocha"
],
"baseUrl": "..",
"skipLibCheck": true,
"paths": {
"common/*": [ "./common/*" ]
"common/*": [ "./../common/*" ],
"*": [ "./../*" ]
}
},
"include": [
+7 -7
View File
@@ -21,14 +21,14 @@
* http://linux.die.net/man/7/urxvt
*/
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services';
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, IMouseStateService, IUnicodeService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services';
import { InstantiationService } from 'common/services/InstantiationService';
import { LogService } from 'common/services/LogService';
import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
import { OptionsService } from 'common/services/OptionsService';
import { IDisposable, IAttributeData, ICoreTerminal, IScrollEvent } from 'common/Types';
import { CoreService } from 'common/services/CoreService';
import { CoreMouseService } from 'common/services/CoreMouseService';
import { MouseStateService } from 'common/services/MouseStateService';
import { UnicodeService } from 'common/services/UnicodeService';
import { CharsetService } from 'common/services/CharsetService';
import { updateWindowsModeWrappedState } from 'common/WindowsMode';
@@ -50,7 +50,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
protected readonly _charsetService: ICharsetService;
protected readonly _oscLinkService: IOscLinkService;
public readonly coreMouseService: ICoreMouseService;
public readonly mouseStateService: IMouseStateService;
public readonly coreService: ICoreService;
public readonly unicodeService: IUnicodeService;
public readonly optionsService: IOptionsService;
@@ -113,8 +113,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this._instantiationService.setService(IBufferService, this._bufferService);
this.coreService = this._register(this._instantiationService.createInstance(CoreService));
this._instantiationService.setService(ICoreService, this.coreService);
this.coreMouseService = this._register(this._instantiationService.createInstance(CoreMouseService));
this._instantiationService.setService(ICoreMouseService, this.coreMouseService);
this.mouseStateService = this._register(this._instantiationService.createInstance(MouseStateService));
this._instantiationService.setService(IMouseStateService, this.mouseStateService);
this.unicodeService = this._register(this._instantiationService.createInstance(UnicodeService));
this._instantiationService.setService(IUnicodeService, this.unicodeService);
this._charsetService = this._instantiationService.createInstance(CharsetService);
@@ -124,7 +124,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
// Register input handler and handle/forward events
this._inputHandler = this._register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService));
this._inputHandler = this._register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.mouseStateService, this.unicodeService));
this._register(EventUtils.forward(this._inputHandler.onLineFeed, this._onLineFeed));
// Setup listeners
@@ -256,7 +256,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this._bufferService.reset();
this._charsetService.reset();
this.coreService.reset();
this.coreMouseService.reset();
this.mouseStateService.reset();
}
+16 -16
View File
@@ -11,7 +11,7 @@ import { CellData } from 'common/buffer/CellData';
import { Attributes, BgFlags, UnderlineStyle } from 'common/buffer/Constants';
import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData';
import { Params } from 'common/parser/Params';
import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test';
import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockMouseStateService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test';
import { IBufferService, ICoreService, type IOscLinkService } from 'common/services/Services';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { clone } from 'common/Clone';
@@ -70,7 +70,7 @@ describe('InputHandler', () => {
coreService = new CoreService(bufferService, new MockLogService(), optionsService);
oscLinkService = new OscLinkService(bufferService);
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, oscLinkService, new MockCoreMouseService(), new MockUnicodeService());
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, oscLinkService, new MockMouseStateService(), new MockUnicodeService());
});
describe('SL/SR/DECIC/DECDC', () => {
@@ -261,7 +261,7 @@ describe('InputHandler', () => {
describe('setMode', () => {
it('should toggle bracketedPasteMode', () => {
const coreService = new MockCoreService();
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService());
// Set bracketed paste mode
inputHandler.setModePrivate(Params.fromArray([2004]));
assert.equal(coreService.decPrivateModes.bracketedPasteMode, true);
@@ -272,7 +272,7 @@ describe('InputHandler', () => {
it('should toggle colorSchemeUpdates (DECSET 2031)', () => {
const coreService = new MockCoreService();
const optionsService = new MockOptionsService();
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService());
// Set color scheme updates mode (default colorSchemeQuery=true)
inputHandler.setModePrivate(Params.fromArray([2031]));
assert.equal(coreService.decPrivateModes.colorSchemeUpdates, true);
@@ -284,7 +284,7 @@ describe('InputHandler', () => {
const coreService = new MockCoreService();
const optionsService = new MockOptionsService();
optionsService.rawOptions.vtExtensions = { colorSchemeQuery: false };
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService());
// Attempt to set color scheme updates mode
inputHandler.setModePrivate(Params.fromArray([2031]));
assert.equal(coreService.decPrivateModes.colorSchemeUpdates, false);
@@ -306,7 +306,7 @@ describe('InputHandler', () => {
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockCoreMouseService(),
new MockMouseStateService(),
new MockUnicodeService()
);
@@ -352,7 +352,7 @@ describe('InputHandler', () => {
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockCoreMouseService(),
new MockMouseStateService(),
new MockUnicodeService()
);
@@ -402,7 +402,7 @@ describe('InputHandler', () => {
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockCoreMouseService(),
new MockMouseStateService(),
new MockUnicodeService()
);
@@ -439,7 +439,7 @@ describe('InputHandler', () => {
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockCoreMouseService(),
new MockMouseStateService(),
new MockUnicodeService()
);
@@ -488,7 +488,7 @@ describe('InputHandler', () => {
new MockLogService(),
new MockOptionsService({ scrollOnEraseInDisplay: true }),
new MockOscLinkService(),
new MockCoreMouseService(),
new MockMouseStateService(),
new MockUnicodeService()
);
const aLine = Array(bufferService.cols + 1).join('a');
@@ -520,7 +520,7 @@ describe('InputHandler', () => {
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockCoreMouseService(),
new MockMouseStateService(),
new MockUnicodeService()
);
@@ -646,7 +646,7 @@ describe('InputHandler', () => {
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
new MockCoreMouseService(),
new MockMouseStateService(),
new MockUnicodeService()
);
const container = new Uint32Array(10);
@@ -674,7 +674,7 @@ describe('InputHandler', () => {
beforeEach(() => {
bufferService = new MockBufferService(80, 30);
handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService());
});
it('should handle DECSET/DECRST 47 (alt screen buffer)', async () => {
await handler.parseP('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST');
@@ -887,7 +887,7 @@ describe('InputHandler', () => {
describe('colon notation', () => {
let inputHandler2: TestInputHandler;
beforeEach(() => {
inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService());
});
describe('should equal to semicolon', () => {
it('CSI 38:2::50:100:150 m', async () => {
@@ -2461,7 +2461,7 @@ describe('InputHandler', () => {
bufferService = new BufferService(optionsService, new MockLogService());
bufferService.resize(80, 30);
coreService = new CoreService(bufferService, new MockLogService(), optionsService);
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService());
});
describe('stack limit', () => {
@@ -2513,7 +2513,7 @@ describe('InputHandler', () => {
coreService = new CoreService(bufferService, new MockLogService(), optionsService);
coreService.onData(data => { console.log(data); });
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService());
});
it('async CUP with CPR check', async () => {
+13 -13
View File
@@ -15,7 +15,7 @@ import { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } fr
import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { AttributeData } from 'common/buffer/AttributeData';
import { ICoreService, IBufferService, IOptionsService, ILogService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services';
import { ICoreService, IBufferService, IOptionsService, ILogService, IMouseStateService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services';
import { UnicodeService } from 'common/services/UnicodeService';
import { OscHandler } from 'common/parser/OscParser';
import { DcsHandler } from 'common/parser/DcsParser';
@@ -178,7 +178,7 @@ export class InputHandler extends Disposable implements IInputHandler {
private readonly _logService: ILogService,
private readonly _optionsService: IOptionsService,
private readonly _oscLinkService: IOscLinkService,
private readonly _coreMouseService: ICoreMouseService,
private readonly _mouseStateService: IMouseStateService,
private readonly _unicodeService: IUnicodeService,
private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser()
) {
@@ -1987,19 +1987,19 @@ export class InputHandler extends Disposable implements IInputHandler {
break;
case 9: // X10 Mouse
// no release, no motion, no wheel, no modifiers.
this._coreMouseService.activeProtocol = 'X10';
this._mouseStateService.activeProtocol = 'X10';
break;
case 1000: // vt200 mouse
// no motion.
this._coreMouseService.activeProtocol = 'VT200';
this._mouseStateService.activeProtocol = 'VT200';
break;
case 1002: // button event mouse
this._coreMouseService.activeProtocol = 'DRAG';
this._mouseStateService.activeProtocol = 'DRAG';
break;
case 1003: // any event mouse
// any event - sends motion events,
// even if there is no button held down.
this._coreMouseService.activeProtocol = 'ANY';
this._mouseStateService.activeProtocol = 'ANY';
break;
case 1004: // send focusin/focusout events
// focusin: ^[[I
@@ -2011,13 +2011,13 @@ export class InputHandler extends Disposable implements IInputHandler {
this._logService.debug('DECSET 1005 not supported (see #2507)');
break;
case 1006: // sgr ext mode mouse
this._coreMouseService.activeEncoding = 'SGR';
this._mouseStateService.activeEncoding = 'SGR';
break;
case 1015: // urxvt ext mode mouse - removed in #2507
this._logService.debug('DECSET 1015 not supported (see #2507)');
break;
case 1016: // sgr pixels mode mouse
this._coreMouseService.activeEncoding = 'SGR_PIXELS';
this._mouseStateService.activeEncoding = 'SGR_PIXELS';
break;
case 25: // show cursor
this._coreService.isCursorHidden = false;
@@ -2248,7 +2248,7 @@ export class InputHandler extends Disposable implements IInputHandler {
case 1000: // vt200 mouse
case 1002: // button event mouse
case 1003: // any event mouse
this._coreMouseService.activeProtocol = 'NONE';
this._mouseStateService.activeProtocol = 'NONE';
break;
case 1004: // send focusin/focusout events
this._coreService.decPrivateModes.sendFocus = false;
@@ -2257,13 +2257,13 @@ export class InputHandler extends Disposable implements IInputHandler {
this._logService.debug('DECRST 1005 not supported (see #2507)');
break;
case 1006: // sgr ext mode mouse
this._coreMouseService.activeEncoding = 'DEFAULT';
this._mouseStateService.activeEncoding = 'DEFAULT';
break;
case 1015: // urxvt ext mode mouse - removed in #2507
this._logService.debug('DECRST 1015 not supported (see #2507)');
break;
case 1016: // sgr pixels mode mouse
this._coreMouseService.activeEncoding = 'DEFAULT';
this._mouseStateService.activeEncoding = 'DEFAULT';
break;
case 25: // hide cursor
this._coreService.isCursorHidden = true;
@@ -2357,7 +2357,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// access helpers
const dm = this._coreService.decPrivateModes;
const { activeProtocol: mouseProtocol, activeEncoding: mouseEncoding } = this._coreMouseService;
const { activeProtocol: mouseProtocol, activeEncoding: mouseEncoding } = this._mouseStateService;
const cs = this._coreService;
const { buffers, cols } = this._bufferService;
const { active, alt } = buffers;
@@ -3570,7 +3570,7 @@ export class InputHandler extends Disposable implements IInputHandler {
return true;
}
const flags = params.params[0] || 0;
const mode = params.params[1] || 1;
const mode = params.length > 1 ? (params.params[1] || 1) : 1;
const state = this._coreService.kittyKeyboard;
switch (mode) {
+16 -9
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, ICoreMouseService, ICharsetService, UnicodeCharProperties, UnicodeCharWidth, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService, type IBufferResizeEvent } from 'common/services/Services';
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IMouseStateService, ICharsetService, UnicodeCharProperties, UnicodeCharWidth, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService, type IBufferResizeEvent } from 'common/services/Services';
import { UnicodeService } from 'common/services/UnicodeService';
import { clone } from 'common/Clone';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
@@ -13,6 +13,14 @@ import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes
import { UnicodeV6 } from 'common/input/UnicodeV6';
import { IDecorationOptions, IDecoration } from '@xterm/xterm';
import { Emitter, type IEvent } from 'common/Event';
import { CellData } from 'common/buffer/CellData';
import { DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH } from 'common/buffer/Constants';
export function createCellData(attr: number, char: string, width: number): CellData {
return CellData.fromCharData([attr, char, width, char.length === 0 ? 0 : char.charCodeAt(0)]);
}
export const NULL_CELL_DATA = Object.freeze(createCellData(DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH));
export class MockBufferService implements IBufferService {
public serviceBrand: any;
@@ -58,22 +66,21 @@ export class MockBufferService implements IBufferService {
public reset(): void { }
}
export class MockCoreMouseService implements ICoreMouseService {
export class MockMouseStateService implements IMouseStateService {
public serviceBrand: any;
public areMouseEventsActive: boolean = false;
public activeEncoding: string = '';
public activeProtocol: string = '';
public isDefaultEncoding: boolean = true;
public isPixelEncoding: boolean = false;
public addEncoding(name: string): void { }
public addProtocol(name: string): void { }
public reset(): void { }
public triggerMouseEvent(event: ICoreMouseEvent): boolean { return false; }
public onProtocolChange: IEvent<CoreMouseEventType> = new Emitter<CoreMouseEventType>().event;
public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } {
throw new Error('Method not implemented.');
}
public consumeWheelEvent(ev: WheelEvent, cellHeight: number, dpr: number): number {
return 1;
}
public restrictMouseEvent(event: ICoreMouseEvent): boolean { return true; }
public encodeMouseEvent(event: ICoreMouseEvent): string { return ''; }
public setCustomWheelEventHandler(customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined): void { }
public allowCustomWheelEvent(ev: WheelEvent): boolean { return true; }
}
export class MockCharsetService implements ICharsetService {
+8 -8
View File
@@ -7,12 +7,12 @@ import { IDeleteEvent, IInsertEvent } from 'common/CircularList';
import { UnderlineStyle } from 'common/buffer/Constants';
import { IBufferSet } from 'common/buffer/Types';
import { IParams } from 'common/parser/Types';
import { ICoreMouseService, ICoreService, IOptionsService, IUnicodeService } from 'common/services/Services';
import { IMouseStateService, ICoreService, IOptionsService, IUnicodeService } from 'common/services/Services';
import { IFunctionIdentifier, ITerminalOptions as IPublicTerminalOptions } from '@xterm/xterm';
import type { Emitter, IEvent } from 'common/Event';
export interface ICoreTerminal {
coreMouseService: ICoreMouseService;
mouseStateService: IMouseStateService;
coreService: ICoreService;
optionsService: IOptionsService;
unicodeService: IUnicodeService;
@@ -101,13 +101,13 @@ export interface ICharset {
[key: string]: string | undefined;
}
export type CharData = [number, string, number, number];
export type CharData = [attr: number, char: string, width: number, code: number];
export interface IColor {
readonly css: string;
readonly rgba: number; // 32-bit int with rgba in each byte
}
export type IColorRGB = [number, number, number];
export type IColorRGB = [red: number, green: number, blue: number];
export interface IExtendedAttrs {
ext: number;
@@ -343,7 +343,7 @@ export interface ICoreMouseEvent {
* it is not possible to report multiple buttons at once.
* Wheel is treated as a button.
* There are invalid combinations of buttons and actions possible
* (like move + wheel), those are silently ignored by the CoreMouseService.
* (like move + wheel), those are silently ignored by the MouseStateService.
*/
button: CoreMouseButton;
action: CoreMouseAction;
@@ -360,7 +360,7 @@ export interface ICoreMouseEvent {
* CoreMouseEventType
* To be reported to the browser component which events a mouse
* protocol wants to be catched and forwarded as an ICoreMouseEvent
* to CoreMouseService.
* to MouseStateService.
*/
export const enum CoreMouseEventType {
NONE = 0,
@@ -378,7 +378,7 @@ export const enum CoreMouseEventType {
/**
* Mouse protocol interface.
* A mouse protocol can be registered and activated at the CoreMouseService.
* A mouse protocol can be registered and activated at the MouseStateService.
* `events` should contain a list of needed events as a hint for the browser component
* to install/remove the appropriate event handlers.
* `restrict` applies further protocol specific restrictions like not allowed
@@ -391,7 +391,7 @@ export interface ICoreMouseProtocol {
/**
* CoreMouseEncoding
* The tracking encoding can be registered and activated at the CoreMouseService.
* The tracking encoding can be registered and activated at the MouseStateService.
* If a ICoreMouseEvent passes all procotol restrictions it will be encoded
* with the active encoding and sent out.
* Note: Returning an empty string will supress sending a mouse report,
+18 -18
View File
@@ -6,7 +6,7 @@
import { assert } from 'chai';
import { Buffer } from 'common/buffer/Buffer';
import { CircularList } from 'common/CircularList';
import { MockOptionsService, MockBufferService, MockLogService } from 'common/TestUtils.test';
import { MockOptionsService, MockBufferService, MockLogService, createCellData } from 'common/TestUtils.test';
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { ExtendedAttrs } from 'common/buffer/AttributeData';
@@ -1105,10 +1105,10 @@ describe('Buffer', () => {
describe ('translateBufferLineToString', () => {
it('should handle selecting a section of ascii text', () => {
const line = new BufferLine(4);
line.setCell(0, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(1, CellData.fromCharData([ 0, 'b', 1, 'b'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([ 0, 'c', 1, 'c'.charCodeAt(0)]));
line.setCell(3, CellData.fromCharData([ 0, 'd', 1, 'd'.charCodeAt(0)]));
line.setCell(0, createCellData(0, 'a', 1));
line.setCell(1, createCellData(0, 'b', 1));
line.setCell(2, createCellData(0, 'c', 1));
line.setCell(3, createCellData(0, 'd', 1));
buffer.lines.set(0, line);
const str = buffer.translateBufferLineToString(0, true, 0, 2);
@@ -1117,9 +1117,9 @@ describe('Buffer', () => {
it('should handle a cut-off double width character by including it', () => {
const line = new BufferLine(3);
line.setCell(0, CellData.fromCharData([ 0, '語', 2, 35486 ]));
line.setCell(1, CellData.fromCharData([ 0, '', 0, 0]));
line.setCell(2, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(0, createCellData(0, '語', 2));
line.setCell(1, createCellData(0, '', 0));
line.setCell(2, createCellData(0, 'a', 1));
buffer.lines.set(0, line);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
@@ -1128,9 +1128,9 @@ describe('Buffer', () => {
it('should handle a zero width character in the middle of the string by not including it', () => {
const line = new BufferLine(3);
line.setCell(0, CellData.fromCharData([ 0, '語', 2, '語'.charCodeAt(0) ]));
line.setCell(1, CellData.fromCharData([ 0, '', 0, 0]));
line.setCell(2, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(0, createCellData(0, '語', 2));
line.setCell(1, createCellData(0, '', 0));
line.setCell(2, createCellData(0, 'a', 1));
buffer.lines.set(0, line);
const str0 = buffer.translateBufferLineToString(0, true, 0, 1);
@@ -1145,8 +1145,8 @@ describe('Buffer', () => {
it('should handle single width emojis', () => {
const line = new BufferLine(2);
line.setCell(0, CellData.fromCharData([ 0, '😁', 1, '😁'.charCodeAt(0) ]));
line.setCell(1, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(0, createCellData(0, '😁', 1));
line.setCell(1, createCellData(0, 'a', 1));
buffer.lines.set(0, line);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
@@ -1158,8 +1158,8 @@ describe('Buffer', () => {
it('should handle double width emojis', () => {
const line = new BufferLine(2);
line.setCell(0, CellData.fromCharData([ 0, '😁', 2, '😁'.charCodeAt(0) ]));
line.setCell(1, CellData.fromCharData([ 0, '', 0, 0]));
line.setCell(0, createCellData(0, '😁', 2));
line.setCell(1, createCellData(0, '', 0));
buffer.lines.set(0, line);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
@@ -1169,9 +1169,9 @@ describe('Buffer', () => {
assert.equal(str2, '😁');
const line2 = new BufferLine(3);
line2.setCell(0, CellData.fromCharData([ 0, '😁', 2, '😁'.charCodeAt(0) ]));
line2.setCell(1, CellData.fromCharData([ 0, '', 0, 0]));
line2.setCell(2, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)]));
line2.setCell(0, createCellData(0, '😁', 2));
line2.setCell(1, createCellData(0, '', 0));
line2.setCell(2, createCellData(0, 'a', 1));
buffer.lines.set(0, line2);
const str3 = buffer.translateBufferLineToString(0, true, 0, 3);
+218 -211
View File
@@ -5,9 +5,10 @@
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR, Content, UnderlineStyle, BgFlags, Attributes, FgFlags } from 'common/buffer/Constants';
import { BufferLine } from 'common/buffer//BufferLine';
import { CellData } from 'common/buffer/CellData';
import { CharData, IBufferLine } from '../Types';
import { CharData, IBufferLine, IExtendedAttrs } from '../Types';
import { assert } from 'chai';
import { AttributeData } from 'common/buffer/AttributeData';
import { createCellData, NULL_CELL_DATA } from 'common/TestUtils.test';
class TestBufferLine extends BufferLine {
@@ -24,6 +25,12 @@ class TestBufferLine extends BufferLine {
}
}
function extendedAttributes(line: IBufferLine, index: number): IExtendedAttrs | undefined {
const cell = new CellData();
line.loadCell(index, cell);
return cell.hasExtendedAttrs() !== 0 ? cell.extended : undefined;
}
describe('AttributeData', () => {
describe('extended attributes', () => {
it('hasExtendedAttrs', () => {
@@ -173,78 +180,78 @@ describe('BufferLine', function(): void {
assert.equal(line.length, 10);
assert.deepEqual(line.loadCell(0, new CellData()).getAsCharData(), [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
assert.equal(line.isWrapped, true);
line = new TestBufferLine(10, CellData.fromCharData([123, 'a', 456, 'a'.charCodeAt(0)]), true);
line = new TestBufferLine(10, createCellData(123, 'a', 456), true);
assert.equal(line.length, 10);
assert.deepEqual(line.loadCell(0, new CellData()).getAsCharData(), [123, 'a', 456, 'a'.charCodeAt(0)]);
assert.equal(line.isWrapped, true);
});
it('insertCells', function(): void {
const line = new TestBufferLine(3);
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
line.insertCells(1, 3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(1, createCellData(2, 'b', 1));
line.setCell(2, createCellData(3, 'c', 1));
line.insertCells(1, 3, createCellData(4, 'd', 1));
assert.deepEqual(line.toArray(), [
[1, 'a', 0, 'a'.charCodeAt(0)],
[4, 'd', 0, 'd'.charCodeAt(0)],
[4, 'd', 0, 'd'.charCodeAt(0)]
[1, 'a', 1, 'a'.charCodeAt(0)],
[4, 'd', 1, 'd'.charCodeAt(0)],
[4, 'd', 1, 'd'.charCodeAt(0)]
]);
});
it('deleteCells', function(): void {
const line = new TestBufferLine(5);
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
line.deleteCells(1, 2, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)]));
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(1, createCellData(2, 'b', 1));
line.setCell(2, createCellData(3, 'c', 1));
line.setCell(3, createCellData(4, 'd', 1));
line.setCell(4, createCellData(5, 'e', 1));
line.deleteCells(1, 2, createCellData(6, 'f', 1));
assert.deepEqual(line.toArray(), [
[1, 'a', 0, 'a'.charCodeAt(0)],
[4, 'd', 0, 'd'.charCodeAt(0)],
[5, 'e', 0, 'e'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)]
[1, 'a', 1, 'a'.charCodeAt(0)],
[4, 'd', 1, 'd'.charCodeAt(0)],
[5, 'e', 1, 'e'.charCodeAt(0)],
[6, 'f', 1, 'f'.charCodeAt(0)],
[6, 'f', 1, 'f'.charCodeAt(0)]
]);
});
it('replaceCells', function(): void {
const line = new TestBufferLine(5);
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
line.replaceCells(2, 4, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)]));
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(1, createCellData(2, 'b', 1));
line.setCell(2, createCellData(3, 'c', 1));
line.setCell(3, createCellData(4, 'd', 1));
line.setCell(4, createCellData(5, 'e', 1));
line.replaceCells(2, 4, createCellData(6, 'f', 1));
assert.deepEqual(line.toArray(), [
[1, 'a', 0, 'a'.charCodeAt(0)],
[2, 'b', 0, 'b'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)],
[5, 'e', 0, 'e'.charCodeAt(0)]
[1, 'a', 1, 'a'.charCodeAt(0)],
[2, 'b', 1, 'b'.charCodeAt(0)],
[6, 'f', 1, 'f'.charCodeAt(0)],
[6, 'f', 1, 'f'.charCodeAt(0)],
[5, 'e', 1, 'e'.charCodeAt(0)]
]);
});
it('fill', function(): void {
const line = new TestBufferLine(5);
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
line.fill(CellData.fromCharData([123, 'z', 0, 'z'.charCodeAt(0)]));
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(1, createCellData(2, 'b', 1));
line.setCell(2, createCellData(3, 'c', 1));
line.setCell(3, createCellData(4, 'd', 1));
line.setCell(4, createCellData(5, 'e', 1));
line.fill(createCellData(123, 'z', 1));
assert.deepEqual(line.toArray(), [
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)]
[123, 'z', 1, 'z'.charCodeAt(0)],
[123, 'z', 1, 'z'.charCodeAt(0)],
[123, 'z', 1, 'z'.charCodeAt(0)],
[123, 'z', 1, 'z'.charCodeAt(0)],
[123, 'z', 1, 'z'.charCodeAt(0)]
]);
});
it('clone', function(): void {
const line = new TestBufferLine(5, undefined, true);
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(1, createCellData(2, 'b', 1));
line.setCell(2, createCellData(3, 'c', 1));
line.setCell(3, createCellData(4, 'd', 1));
line.setCell(4, createCellData(5, 'e', 1));
const line2 = line.clone();
assert.deepEqual(TestBufferLine.prototype.toArray.apply(line2), line.toArray());
assert.equal(line2.length, line.length);
@@ -252,12 +259,12 @@ describe('BufferLine', function(): void {
});
it('copyFrom', function(): void {
const line = new TestBufferLine(5);
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), true);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(1, createCellData(2, 'b', 1));
line.setCell(2, createCellData(3, 'c', 1));
line.setCell(3, createCellData(4, 'd', 1));
line.setCell(4, createCellData(5, 'e', 1));
const line2 = new TestBufferLine(5, createCellData(1, 'a', 1), true);
line2.copyFrom(line);
assert.deepEqual(line2.toArray(), line.toArray());
assert.equal(line2.length, line.length);
@@ -267,9 +274,9 @@ describe('BufferLine', function(): void {
// CHAR_DATA_CODE_INDEX resembles current behavior in InputHandler.print
// --> set code to the last charCodeAt value of the string
// Note: needs to be fixed once the string pointer is in place
const line = new TestBufferLine(2, CellData.fromCharData([1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]));
assert.deepEqual(line.toArray(), [[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]);
const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, '\u0301'.charCodeAt(0)]), true);
const line = new TestBufferLine(2, createCellData(1, 'e\u0301', 1));
assert.deepEqual(line.toArray(), [[1, 'e\u0301', 1, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]]);
const line2 = new TestBufferLine(5, createCellData(1, 'a', 1), true);
line2.copyFrom(line);
assert.deepEqual(line2.toArray(), line.toArray());
const line3 = line.clone();
@@ -277,72 +284,72 @@ describe('BufferLine', function(): void {
});
describe('resize', function(): void {
it('enlarge(false)', function(): void {
const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
assert.deepEqual(line.toArray(), (Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
const line = new TestBufferLine(5, createCellData(1, 'a', 1), false);
line.resize(10, createCellData(1, 'a', 1));
assert.deepEqual(line.toArray(), (Array(10) as any).fill([1, 'a', 1, 'a'.charCodeAt(0)]));
});
it('enlarge(true)', function(): void {
const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
assert.deepEqual(line.toArray(), (Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
const line = new TestBufferLine(5, createCellData(1, 'a', 1), false);
line.resize(10, createCellData(1, 'a', 1));
assert.deepEqual(line.toArray(), (Array(10) as any).fill([1, 'a', 1, 'a'.charCodeAt(0)]));
});
it('shrink(true) - should apply new size', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
assert.deepEqual(line.toArray(), (Array(5) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
const line = new TestBufferLine(10, createCellData(1, 'a', 1), false);
line.resize(5, createCellData(1, 'a', 1));
assert.deepEqual(line.toArray(), (Array(5) as any).fill([1, 'a', 1, 'a'.charCodeAt(0)]));
});
it('shrink to 0 length', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
line.resize(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
assert.deepEqual(line.toArray(), (Array(0) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
const line = new TestBufferLine(10, createCellData(1, 'a', 1), false);
line.resize(0, createCellData(1, 'a', 1));
assert.deepEqual(line.toArray(), (Array(0) as any).fill([1, 'a', 1, 'a'.charCodeAt(0)]));
});
it('should remove combining data on replaced cells after shrinking then enlarging', () => {
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
const line = new TestBufferLine(10, createCellData(1, 'a', 1), false);
line.set(2, [ 0, '😁', 1, '😁'.charCodeAt(0) ]);
line.set(9, [ 0, '😁', 1, '😁'.charCodeAt(0) ]);
assert.equal(line.translateToString(), 'aa😁aaaaaa😁');
assert.equal(Object.keys(line.combined).length, 2);
line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
line.resize(5, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aa😁aa');
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
line.resize(10, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aa😁aaaaaaa');
assert.equal(Object.keys(line.combined).length, 1);
});
});
describe('getTrimLength', function(): void {
it('empty line', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
assert.equal(line.getTrimmedLength(), 0);
});
it('ASCII', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, 'a', 1));
assert.equal(line.getTrimmedLength(), 3);
});
it('surrogate', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, '𝄞', 1));
assert.equal(line.getTrimmedLength(), 3);
});
it('combining', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, 'e\u0301', 1));
assert.equal(line.getTrimmedLength(), 3);
});
it('fullwidth', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
line.setCell(3, CellData.fromCharData([0, '', 0, 0]));
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, '1', 2));
line.setCell(3, createCellData(0, '', 0));
assert.equal(line.getTrimmedLength(), 4); // also counts null cell after fullwidth
});
});
describe('translateToString with and w\'o trimming', function(): void {
it('empty line', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
const columns: number[] = [];
assert.equal(line.translateToString(false, undefined, undefined, columns), ' ');
assert.deepEqual(columns, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
@@ -351,11 +358,11 @@ describe('BufferLine', function(): void {
});
it('ASCII', function(): void {
const columns: number[] = [];
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, 'a', 1));
line.setCell(4, createCellData(1, 'a', 1));
line.setCell(5, createCellData(1, 'a', 1));
assert.equal(line.translateToString(false, undefined, undefined, columns), 'a a aa ');
assert.deepEqual(columns, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
assert.equal(line.translateToString(true, undefined, undefined, columns), 'a a aa');
@@ -372,11 +379,11 @@ describe('BufferLine', function(): void {
});
it('surrogate', function(): void {
const columns: number[] = [];
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
line.setCell(5, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, '𝄞', 1));
line.setCell(4, createCellData(1, '𝄞', 1));
line.setCell(5, createCellData(1, '𝄞', 1));
assert.equal(line.translateToString(false, undefined, undefined, columns), 'a 𝄞 𝄞𝄞 ');
assert.deepEqual(columns, [0, 1, 2, 2, 3, 4, 4, 5, 5, 6, 7, 8, 9, 10]);
assert.equal(line.translateToString(true, undefined, undefined, columns), 'a 𝄞 𝄞𝄞');
@@ -392,11 +399,11 @@ describe('BufferLine', function(): void {
});
it('combining', function(): void {
const columns: number[] = [];
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
line.setCell(5, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, 'e\u0301', 1));
line.setCell(4, createCellData(1, 'e\u0301', 1));
line.setCell(5, createCellData(1, 'e\u0301', 1));
assert.equal(line.translateToString(false, undefined, undefined, columns), 'a e\u0301 e\u0301e\u0301 ');
assert.deepEqual(columns, [0, 1, 2, 2, 3, 4, 4, 5, 5, 6, 7, 8, 9, 10]);
assert.equal(line.translateToString(true, undefined, undefined, columns), 'a e\u0301 e\u0301e\u0301');
@@ -412,14 +419,14 @@ describe('BufferLine', function(): void {
});
it('fullwidth', function(): void {
const columns: number[] = [];
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
line.setCell(3, CellData.fromCharData([0, '', 0, 0]));
line.setCell(5, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
line.setCell(6, CellData.fromCharData([0, '', 0, 0]));
line.setCell(7, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
line.setCell(8, CellData.fromCharData([0, '', 0, 0]));
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, '1', 2));
line.setCell(3, createCellData(0, '', 0));
line.setCell(5, createCellData(1, '1', 2));
line.setCell(6, createCellData(0, '', 0));
line.setCell(7, createCellData(1, '1', 2));
line.setCell(8, createCellData(0, '', 0));
assert.equal(line.translateToString(false, undefined, undefined, columns), 'a 1 11 ');
assert.deepEqual(columns, [0, 1, 2, 4, 5, 7, 9, 10]);
assert.equal(line.translateToString(true, undefined, undefined, columns), 'a 1 11');
@@ -441,12 +448,12 @@ describe('BufferLine', function(): void {
});
it('space at end', function(): void {
const columns: number[] = [];
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(6, CellData.fromCharData([1, ' ', 1, ' '.charCodeAt(0)]));
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
line.setCell(2, createCellData(1, 'a', 1));
line.setCell(4, createCellData(1, 'a', 1));
line.setCell(5, createCellData(1, 'a', 1));
line.setCell(6, createCellData(1, ' ', 1));
assert.equal(line.translateToString(false, undefined, undefined, columns), 'a a aa ');
assert.deepEqual(columns, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
assert.equal(line.translateToString(true, undefined, undefined, columns), 'a a aa ');
@@ -457,7 +464,7 @@ describe('BufferLine', function(): void {
// sanity check - broken line with invalid out of bound null width cells
// this can atm happen with deleting/inserting chars in inputhandler by "breaking"
// fullwidth pairs --> needs to be fixed after settling BufferLine impl
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
assert.equal(line.translateToString(false, undefined, undefined, columns), ' ');
assert.deepEqual(columns, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
assert.equal(line.translateToString(true, undefined, undefined, columns), '');
@@ -465,15 +472,15 @@ describe('BufferLine', function(): void {
});
it('should work with endCol=0', () => {
const columns: number[] = [];
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
line.setCell(0, createCellData(1, 'a', 1));
assert.equal(line.translateToString(true, 0, 0, columns), '');
assert.deepEqual(columns, [0]);
});
});
describe('addCharToCell', () => {
it('should set width to 1 for empty cell', () => {
const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
const line = new TestBufferLine(3, NULL_CELL_DATA, false);
line.addCodepointToCell(0, '\u0301'.charCodeAt(0), 0);
const cell = line.loadCell(0, new CellData());
// chars contains single combining char
@@ -483,7 +490,7 @@ describe('BufferLine', function(): void {
assert.equal(cell.isCombined(), 0);
});
it('should add char to combining string in cell', () => {
const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
const line = new TestBufferLine(3, NULL_CELL_DATA, false);
const cell = line .loadCell(0, new CellData());
cell.setFromCharData([123, 'e\u0301', 1, 'e\u0301'.charCodeAt(1)]);
line.setCell(0, cell);
@@ -496,7 +503,7 @@ describe('BufferLine', function(): void {
assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
});
it('should create combining string on taken cell', () => {
const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
const line = new TestBufferLine(3, NULL_CELL_DATA, false);
const cell = line .loadCell(0, new CellData());
cell.setFromCharData([123, 'e', 1, 'e'.charCodeAt(1)]);
line.setCell(0, cell);
@@ -511,98 +518,98 @@ describe('BufferLine', function(): void {
});
describe('correct fullwidth handling', () => {
function populate(line: BufferLine): void {
const cell = CellData.fromCharData([1, '¥', 2, '¥'.charCodeAt(0)]);
const cell = createCellData(1, '¥', 2);
for (let i = 0; i < line.length; i += 2) {
line.setCell(i, cell);
}
}
it('insert - wide char at pos', () => {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.insertCells(9, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.insertCells(9, 1, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), '¥¥¥¥ a');
line.insertCells(8, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.insertCells(8, 1, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), '¥¥¥¥a ');
line.insertCells(1, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.insertCells(1, 1, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' a ¥¥¥a');
});
it('insert - wide char at end', () => {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.insertCells(0, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.insertCells(0, 3, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aaa¥¥¥ ');
line.insertCells(4, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.insertCells(4, 1, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aaa a ¥¥');
line.insertCells(4, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.insertCells(4, 1, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aaa aa ¥ ');
});
it('delete', () => {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
const line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.deleteCells(0, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.deleteCells(0, 1, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' ¥¥¥¥a');
line.deleteCells(5, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.deleteCells(5, 2, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' ¥¥¥aaa');
line.deleteCells(0, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.deleteCells(0, 2, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' ¥¥aaaaa');
});
it('replace - start at 0', () => {
let line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
let line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(0, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.replaceCells(0, 1, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'a ¥¥¥¥');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(0, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.replaceCells(0, 2, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aa¥¥¥¥');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(0, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.replaceCells(0, 3, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aaa ¥¥¥');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(0, 8, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.replaceCells(0, 8, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aaaaaaaa¥');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(0, 9, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.replaceCells(0, 9, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aaaaaaaaa ');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(0, 10, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.replaceCells(0, 10, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), 'aaaaaaaaaa');
});
it('replace - start at 1', () => {
let line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
let line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(1, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.replaceCells(1, 2, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' a¥¥¥¥');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(1, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.replaceCells(1, 3, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' aa ¥¥¥');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(1, 4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.replaceCells(1, 4, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' aaa¥¥¥');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(1, 8, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.replaceCells(1, 8, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' aaaaaaa¥');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(1, 9, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.replaceCells(1, 9, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' aaaaaaaa ');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
line = new TestBufferLine(10, NULL_CELL_DATA, false);
populate(line);
line.replaceCells(1, 10, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.replaceCells(1, 10, createCellData(1, 'a', 1));
assert.equal(line.translateToString(), ' aaaaaaaaa');
});
});
describe('extended attributes', () => {
it('setCells', function(): void {
const line = new TestBufferLine(5);
const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]);
const cell = createCellData(1, 'a', 1);
// no eAttrs
line.setCell(0, cell);
@@ -612,7 +619,7 @@ describe('BufferLine', function(): void {
line.setCell(1, cell);
// same eAttr, different codepoint
cell.content = 65; // 'A'
cell.content = createCellData(1, 'A', 1).content;
line.setCell(2, cell);
// different eAttr
@@ -625,25 +632,25 @@ describe('BufferLine', function(): void {
line.setCell(4, cell);
assert.deepEqual(line.toArray(), [
[1, 'a', 0, 'a'.charCodeAt(0)],
[1, 'a', 0, 'a'.charCodeAt(0)],
[1, 'A', 0, 'A'.charCodeAt(0)],
[1, 'A', 0, 'A'.charCodeAt(0)],
[1, 'A', 0, 'A'.charCodeAt(0)]
[1, 'a', 1, 'a'.charCodeAt(0)],
[1, 'a', 1, 'a'.charCodeAt(0)],
[1, 'A', 1, 'A'.charCodeAt(0)],
[1, 'A', 1, 'A'.charCodeAt(0)],
[1, 'A', 1, 'A'.charCodeAt(0)]
]);
assert.equal((line as any)._extendedAttrs[0], undefined);
assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.CURLY);
assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.CURLY);
assert.equal((line as any)._extendedAttrs[3].underlineStyle, UnderlineStyle.DOTTED);
assert.equal((line as any)._extendedAttrs[4], undefined);
assert.equal(extendedAttributes(line, 0), undefined);
assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 3)?.underlineStyle, UnderlineStyle.DOTTED);
assert.equal(extendedAttributes(line, 4)?.underlineStyle, undefined);
// should be ref to the same object
assert.equal((line as any)._extendedAttrs[1], (line as any)._extendedAttrs[2]);
assert.equal(extendedAttributes(line, 1), extendedAttributes(line, 2));
// should be a different obj
assert.notEqual((line as any)._extendedAttrs[1], (line as any)._extendedAttrs[3]);
assert.notEqual(extendedAttributes(line, 1), extendedAttributes(line, 3));
});
it('loadCell', () => {
const line = new TestBufferLine(5);
const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]);
const cell = createCellData(1, 'a', 1);
// no eAttrs
line.setCell(0, cell);
@@ -686,65 +693,65 @@ describe('BufferLine', function(): void {
});
it('fill', () => {
const line = new TestBufferLine(3);
const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]);
const cell = createCellData(1, 'a', 1);
cell.extended.underlineStyle = UnderlineStyle.CURLY;
cell.bg |= BgFlags.HAS_EXTENDED;
line.fill(cell);
assert.equal((line as any)._extendedAttrs[0].underlineStyle, UnderlineStyle.CURLY);
assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.CURLY);
assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 0)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.CURLY);
});
it('insertCells', () => {
const line = new TestBufferLine(5);
const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]);
const cell = createCellData(1, 'a', 1);
cell.extended.underlineStyle = UnderlineStyle.CURLY;
cell.bg |= BgFlags.HAS_EXTENDED;
line.insertCells(1, 3, cell);
assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.CURLY);
assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.CURLY);
assert.equal((line as any)._extendedAttrs[3].underlineStyle, UnderlineStyle.CURLY);
assert.equal((line as any)._extendedAttrs[4], undefined);
assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 3)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 4), undefined);
cell.extended = cell.extended.clone();
cell.extended.underlineStyle = UnderlineStyle.DOTTED;
line.insertCells(2, 2, cell);
assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.CURLY);
assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.DOTTED);
assert.equal((line as any)._extendedAttrs[3].underlineStyle, UnderlineStyle.DOTTED);
assert.equal((line as any)._extendedAttrs[4].underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.DOTTED);
assert.equal(extendedAttributes(line, 3)?.underlineStyle, UnderlineStyle.DOTTED);
assert.equal(extendedAttributes(line, 4)?.underlineStyle, UnderlineStyle.CURLY);
});
it('deleteCells', () => {
const line = new TestBufferLine(5);
const fillCell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]);
const fillCell = createCellData(1, 'a', 1);
fillCell.extended.underlineStyle = UnderlineStyle.CURLY;
fillCell.bg |= BgFlags.HAS_EXTENDED;
line.fill(fillCell);
fillCell.extended = fillCell.extended.clone();
fillCell.extended.underlineStyle = UnderlineStyle.DOUBLE;
line.deleteCells(1, 3, fillCell);
assert.equal((line as any)._extendedAttrs[0].underlineStyle, UnderlineStyle.CURLY);
assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.CURLY);
assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.DOUBLE);
assert.equal((line as any)._extendedAttrs[3].underlineStyle, UnderlineStyle.DOUBLE);
assert.equal((line as any)._extendedAttrs[4].underlineStyle, UnderlineStyle.DOUBLE);
assert.equal(extendedAttributes(line, 0)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.DOUBLE);
assert.equal(extendedAttributes(line, 3)?.underlineStyle, UnderlineStyle.DOUBLE);
assert.equal(extendedAttributes(line, 4)?.underlineStyle, UnderlineStyle.DOUBLE);
});
it('replaceCells', () => {
const line = new TestBufferLine(5);
const fillCell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]);
const fillCell = createCellData(1, 'a', 1);
fillCell.extended.underlineStyle = UnderlineStyle.CURLY;
fillCell.bg |= BgFlags.HAS_EXTENDED;
line.fill(fillCell);
fillCell.extended = fillCell.extended.clone();
fillCell.extended.underlineStyle = UnderlineStyle.DOUBLE;
line.replaceCells(1, 3, fillCell);
assert.equal((line as any)._extendedAttrs[0].underlineStyle, UnderlineStyle.CURLY);
assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.DOUBLE);
assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.DOUBLE);
assert.equal((line as any)._extendedAttrs[3].underlineStyle, UnderlineStyle.CURLY);
assert.equal((line as any)._extendedAttrs[4].underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 0)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.DOUBLE);
assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.DOUBLE);
assert.equal(extendedAttributes(line, 3)?.underlineStyle, UnderlineStyle.CURLY);
assert.equal(extendedAttributes(line, 4)?.underlineStyle, UnderlineStyle.CURLY);
});
it('clone', () => {
const line = new TestBufferLine(5);
const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]);
const cell = createCellData(1, 'a', 1);
// no eAttrs
line.setCell(0, cell);
@@ -767,15 +774,15 @@ describe('BufferLine', function(): void {
line.setCell(4, cell);
const nLine = line.clone();
assert.equal((nLine as any)._extendedAttrs[0], (line as any)._extendedAttrs[0]);
assert.equal((nLine as any)._extendedAttrs[1], (line as any)._extendedAttrs[1]);
assert.equal((nLine as any)._extendedAttrs[2], (line as any)._extendedAttrs[2]);
assert.equal((nLine as any)._extendedAttrs[3], (line as any)._extendedAttrs[3]);
assert.equal((nLine as any)._extendedAttrs[4], (line as any)._extendedAttrs[4]);
assert.equal(extendedAttributes(nLine, 0), extendedAttributes(line, 0));
assert.equal(extendedAttributes(nLine, 1), extendedAttributes(line, 1));
assert.equal(extendedAttributes(nLine, 2), extendedAttributes(line, 2));
assert.equal(extendedAttributes(nLine, 3), extendedAttributes(line, 3));
assert.equal(extendedAttributes(nLine, 4), extendedAttributes(line, 4));
});
it('copyFrom', () => {
const initial = new TestBufferLine(5);
const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]);
const cell = createCellData(1, 'a', 1);
// no eAttrs
initial.setCell(0, cell);
@@ -798,13 +805,13 @@ describe('BufferLine', function(): void {
initial.setCell(4, cell);
const line = new TestBufferLine(5);
line.fill(CellData.fromCharData([1, 'b', 0, 'b'.charCodeAt(0)]));
line.fill(createCellData(1, 'b', 1));
line.copyFrom(initial);
assert.equal((line as any)._extendedAttrs[0], (initial as any)._extendedAttrs[0]);
assert.equal((line as any)._extendedAttrs[1], (initial as any)._extendedAttrs[1]);
assert.equal((line as any)._extendedAttrs[2], (initial as any)._extendedAttrs[2]);
assert.equal((line as any)._extendedAttrs[3], (initial as any)._extendedAttrs[3]);
assert.equal((line as any)._extendedAttrs[4], (initial as any)._extendedAttrs[4]);
assert.equal(extendedAttributes(line, 0), extendedAttributes(initial, 0));
assert.equal(extendedAttributes(line, 1), extendedAttributes(initial, 1));
assert.equal(extendedAttributes(line, 2), extendedAttributes(initial, 2));
assert.equal(extendedAttributes(line, 3), extendedAttributes(initial, 3));
assert.equal(extendedAttributes(line, 4), extendedAttributes(initial, 4));
});
});
});
+7 -1
View File
@@ -99,7 +99,13 @@ export function evaluateKeyboardEvent(
break;
case 13:
// return/enter
result.key = ev.altKey ? C0.ESC + C0.CR : C0.CR;
if (ev.key === 'c' && ev.ctrlKey) {
// HACK: Safari on iPad, iOS, AppleVisionPro sends key 13 when typing ctrl-c on hardware
// keyboard
result.key = C0.ETX;
} else {
result.key = ev.altKey ? C0.ESC + C0.CR : C0.CR;
}
result.cancel = true;
break;
case 27:
+23 -8
View File
@@ -100,24 +100,24 @@ describe('KittyKeyboard', () => {
assert.strictEqual(result.key, '\x1b[27u');
});
it('Enter → CSI 13 u', () => {
it('Enter → legacy \\r', () => {
const result = kitty.evaluate(createEvent({ key: 'Enter' }), flags);
assert.strictEqual(result.key, '\x1b[13u');
assert.strictEqual(result.key, '\r');
});
it('Tab → CSI 9 u', () => {
it('Tab → legacy \\t', () => {
const result = kitty.evaluate(createEvent({ key: 'Tab' }), flags);
assert.strictEqual(result.key, '\x1b[9u');
assert.strictEqual(result.key, '\t');
});
it('Backspace → CSI 127 u', () => {
it('Backspace → legacy \\x7f', () => {
const result = kitty.evaluate(createEvent({ key: 'Backspace' }), flags);
assert.strictEqual(result.key, '\x1b[127u');
assert.strictEqual(result.key, '\x7f');
});
it('Space → CSI 32 u', () => {
it('Space → plain space (text-generating key)', () => {
const result = kitty.evaluate(createEvent({ key: ' ' }), flags);
assert.strictEqual(result.key, '\x1b[32u');
assert.strictEqual(result.key, ' ');
});
it('Shift+Tab → CSI 9;2 u', () => {
@@ -134,6 +134,21 @@ describe('KittyKeyboard', () => {
const result = kitty.evaluate(createEvent({ key: 'Escape', altKey: true }), flags);
assert.strictEqual(result.key, '\x1b[27;3u');
});
it('Ctrl+Backspace → CSI 127;5 u', () => {
const result = kitty.evaluate(createEvent({ key: 'Backspace', ctrlKey: true }), flags);
assert.strictEqual(result.key, '\x1b[127;5u');
});
it('Ctrl+Space → CSI 32;5 u', () => {
const result = kitty.evaluate(createEvent({ key: ' ', ctrlKey: true }), flags);
assert.strictEqual(result.key, '\x1b[32;5u');
});
it('Alt+Space → CSI 32;3 u', () => {
const result = kitty.evaluate(createEvent({ key: ' ', altKey: true }), flags);
assert.strictEqual(result.key, '\x1b[32;3u');
});
});
describe('navigation keys', () => {
+9 -4
View File
@@ -457,9 +457,11 @@ export class KittyKeyboard {
} else if (reportEventTypes) {
useCsiU = true;
} else if (flags & KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES) {
if (keyCode === 27 || keyCode === 127 || keyCode === 13 || keyCode === 9 || keyCode === 32) {
useCsiU = true;
} else if (isFunc) {
// Per spec, Enter/Tab/Backspace "still generate the same bytes as in legacy
// mode" and consider space to be a text-generating key, so these skip the isFunc fast-path
// and only get CSI u when modifiers are present (handled below).
const isDisambiguateLegacy = keyCode === 13 || keyCode === 9 || keyCode === 127;
if (isFunc && !isDisambiguateLegacy) {
useCsiU = true;
} else if (modifiers > 0) {
if (ev.shiftKey && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.key.length === 1) {
@@ -474,7 +476,10 @@ export class KittyKeyboard {
result.key = this._buildCsiUSequence(ev, keyCode, modifiers, eventType, flags, isFunc, isMod);
result.cancel = true;
} else {
if (ev.key.length === 1 && !ev.ctrlKey && !ev.altKey && !ev.metaKey) {
const legacyByte = keyCode === 13 ? '\r' : keyCode === 9 ? '\t' : keyCode === 127 ? '\x7f' : undefined;
if (legacyByte) {
result.key = legacyByte;
} else if (ev.key.length === 1 && !ev.ctrlKey && !ev.altKey && !ev.metaKey) {
result.key = ev.key;
}
}
@@ -1,212 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { CoreMouseService } from 'common/services/CoreMouseService';
import { MockCoreService, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
import { assert } from 'chai';
import { ICoreMouseEvent, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
// needed mock services
const bufferService = new MockBufferService(300, 100);
const coreService = new MockCoreService();
const optionsService = new MockOptionsService();
function toBytes(s: string | undefined): number[] {
if (!s) {
return [];
}
const res: number[] = [];
for (let i = 0; i < s.length; ++i) {
res.push(s.charCodeAt(i));
}
return res;
}
describe('CoreMouseService', () => {
it('init', () => {
const cms = new CoreMouseService(bufferService, coreService, optionsService);
assert.equal(cms.activeEncoding, 'DEFAULT');
assert.equal(cms.activeProtocol, 'NONE');
});
it('default protocols - NONE, X10, VT200, DRAG, ANY', () => {
const cms = new CoreMouseService(bufferService, coreService, optionsService);
assert.deepEqual(Object.keys((cms as any)._protocols), ['NONE', 'X10', 'VT200', 'DRAG', 'ANY']);
});
it('default encodings - DEFAULT, SGR', () => {
const cms = new CoreMouseService(bufferService, coreService, optionsService);
assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'SGR', 'SGR_PIXELS']);
});
it('protocol/encoding setter, reset', () => {
const cms = new CoreMouseService(bufferService, coreService, optionsService);
cms.activeEncoding = 'SGR';
cms.activeProtocol = 'ANY';
assert.equal(cms.activeEncoding, 'SGR');
assert.equal(cms.activeProtocol, 'ANY');
cms.reset();
assert.equal(cms.activeEncoding, 'DEFAULT');
assert.equal(cms.activeProtocol, 'NONE');
assert.throws(() => { cms.activeEncoding = 'xyz'; }, 'unknown encoding "xyz"');
assert.throws(() => { cms.activeProtocol = 'xyz'; }, 'unknown protocol "xyz"');
});
it('addEncoding', () => {
const cms = new CoreMouseService(bufferService, coreService, optionsService);
cms.addEncoding('XYZ', (e: ICoreMouseEvent) => '');
cms.activeEncoding = 'XYZ';
assert.equal(cms.activeEncoding, 'XYZ');
});
it('addProtocol', () => {
const cms = new CoreMouseService(bufferService, coreService, optionsService);
cms.addProtocol('XYZ', { events: CoreMouseEventType.NONE, restrict: (e: ICoreMouseEvent) => false });
cms.activeProtocol = 'XYZ';
assert.equal(cms.activeProtocol, 'XYZ');
});
it('onProtocolChange', () => {
const cms = new CoreMouseService(bufferService, coreService, optionsService);
const wantedEvents: CoreMouseEventType[] = [];
cms.onProtocolChange(events => wantedEvents.push(events));
cms.activeProtocol = 'NONE';
assert.deepEqual(wantedEvents, [CoreMouseEventType.NONE]);
cms.activeProtocol = 'ANY';
assert.deepEqual(wantedEvents, [
CoreMouseEventType.NONE,
CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE
]);
});
describe('triggerMouseEvent', () => {
let cms: CoreMouseService;
let reports: string[];
beforeEach(() => {
cms = new CoreMouseService(bufferService, coreService, optionsService);
reports = [];
coreService.triggerDataEvent = (data: string, userInput?: boolean) => reports.push(data);
coreService.triggerBinaryEvent = (data: string) => reports.push(data);
});
it('NONE', () => {
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
});
it('X10', () => {
cms.activeProtocol = 'X10';
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
});
it('VT200', () => {
cms.activeProtocol = 'VT200';
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
});
it('DRAG', () => {
cms.activeProtocol = 'DRAG';
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
});
it('ANY', () => {
cms.activeProtocol = 'ANY';
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), true);
// should not report in any case
// invalid button + action combinations
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.MOVE }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.UP }), false);
// invalid coords
assert.equal(cms.triggerMouseEvent({ col: -1, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 500, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: -1, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 500, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
});
describe('coords', () => {
it('DEFAULT encoding', () => {
cms.activeProtocol = 'ANY';
for (let i = 0; i < bufferService.cols; ++i) {
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
if (i > 222) {
// supress mouse reports if we are out of addressible range (max. 222)
assert.deepEqual(toBytes(reports.pop()), []);
} else {
assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]);
}
}
});
it('SGR encoding', () => {
cms.activeProtocol = 'ANY';
cms.activeEncoding = 'SGR';
for (let i = 0; i < bufferService.cols; ++i) {
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.deepEqual(reports.pop(), `\x1b[<0;${i + 1};1M`);
}
});
it('SGR_PIXELS encoding', () => {
cms.activeProtocol = 'ANY';
cms.activeEncoding = 'SGR_PIXELS';
for (let i = 0; i < 500; ++i) {
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: i, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
assert.deepEqual(reports.pop(), `\x1b[<0;${i};0M`);
}
});
});
it('eventCodes with modifiers (DEFAULT encoding)', () => {
// TODO: implement AUX button tests
cms.activeProtocol = 'ANY';
cms.activeEncoding = 'DEFAULT';
// all buttons + down + no modifer
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
assert.deepEqual(reports, ['\x1b[M !!', '\x1b[M!!!', '\x1b[M"!!', '\x1b[Ma!!']);
reports = [];
// all buttons + up + no modifier
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
assert.deepEqual(reports, ['\x1b[M#!!', '\x1b[M#!!', '\x1b[M#!!', '\x1b[M`!!']);
reports = [];
// all buttons + move + no modifier
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
assert.deepEqual(reports, ['\x1b[M@!!', '\x1b[MA!!', '\x1b[MB!!', '\x1b[MC!!']);
reports = [];
// button none + move + modifiers
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: false, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: true }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: false }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: true }), true);
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: true }), true);
assert.deepEqual(reports, ['\x1b[MS!!', '\x1b[MK!!', '\x1b[MG!!', '\x1b[M[!!', '\x1b[MO!!', '\x1b[M_!!']);
reports = [];
});
});
});
@@ -0,0 +1,88 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { MouseStateService } from 'common/services/MouseStateService';
import { assert } from 'chai';
import { ICoreMouseEvent, CoreMouseEventType } from 'common/Types';
function toBytes(s: string | undefined): number[] {
if (!s) {
return [];
}
const res: number[] = [];
for (let i = 0; i < s.length; ++i) {
res.push(s.charCodeAt(i));
}
return res;
}
describe('MouseStateService', () => {
it('init', () => {
const cms = new MouseStateService();
assert.equal(cms.activeEncoding, 'DEFAULT');
assert.equal(cms.activeProtocol, 'NONE');
});
it('default protocols - NONE, X10, VT200, DRAG, ANY', () => {
const cms = new MouseStateService();
assert.deepEqual(Object.keys((cms as any)._protocols), ['NONE', 'X10', 'VT200', 'DRAG', 'ANY']);
});
it('default encodings - DEFAULT, SGR', () => {
const cms = new MouseStateService();
assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'SGR', 'SGR_PIXELS']);
});
it('protocol/encoding setter, reset', () => {
const cms = new MouseStateService();
cms.activeEncoding = 'SGR';
cms.activeProtocol = 'ANY';
assert.equal(cms.activeEncoding, 'SGR');
assert.equal(cms.activeProtocol, 'ANY');
cms.reset();
assert.equal(cms.activeEncoding, 'DEFAULT');
assert.equal(cms.activeProtocol, 'NONE');
assert.throws(() => { cms.activeEncoding = 'xyz'; }, 'unknown encoding "xyz"');
assert.throws(() => { cms.activeProtocol = 'xyz'; }, 'unknown protocol "xyz"');
});
it('addEncoding', () => {
const cms = new MouseStateService();
cms.addEncoding('XYZ', (e: ICoreMouseEvent) => '');
cms.activeEncoding = 'XYZ';
assert.equal(cms.activeEncoding, 'XYZ');
});
it('addProtocol', () => {
const cms = new MouseStateService();
cms.addProtocol('XYZ', { events: CoreMouseEventType.NONE, restrict: (e: ICoreMouseEvent) => false });
cms.activeProtocol = 'XYZ';
assert.equal(cms.activeProtocol, 'XYZ');
});
it('onProtocolChange', () => {
const cms = new MouseStateService();
const wantedEvents: CoreMouseEventType[] = [];
cms.onProtocolChange(events => wantedEvents.push(events));
cms.activeProtocol = 'NONE';
assert.deepEqual(wantedEvents, [CoreMouseEventType.NONE]);
cms.activeProtocol = 'ANY';
assert.deepEqual(wantedEvents, [
CoreMouseEventType.NONE,
CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE
]);
});
it('restrictMouseEvent/encodeMouseEvent', () => {
const cms = new MouseStateService();
const event: ICoreMouseEvent = {
col: 1,
row: 1,
x: 0,
y: 0,
button: 0,
action: 1,
ctrl: false,
alt: false,
shift: false
};
cms.activeProtocol = 'ANY';
cms.activeEncoding = 'DEFAULT';
assert.equal(cms.restrictMouseEvent(event), true);
assert.deepEqual(toBytes(cms.encodeMouseEvent(event)), [0x1b, 0x5b, 0x4d, 0x20, 0x21, 0x21]);
});
});
@@ -2,7 +2,7 @@
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IBufferService, ICoreService, ICoreMouseService, IOptionsService } from 'common/services/Services';
import { IMouseStateService } from 'common/services/Services';
import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
import { Disposable } from 'common/Lifecycle';
import { Emitter } from 'common/Event';
@@ -151,7 +151,7 @@ const DEFAULT_ENCODINGS: { [key: string]: CoreMouseEncoding } = {
};
/**
* CoreMouseService
* MouseStateService
*
* Provides mouse tracking reports with different protocols and encodings.
* - protocols: NONE (default), X10, VT200, DRAG, ANY
@@ -166,25 +166,21 @@ const DEFAULT_ENCODINGS: { [key: string]: CoreMouseEncoding } = {
* a tracking report to the backend based on protocol and encoding limitations.
* To send a mouse event call `triggerMouseEvent`.
*/
export class CoreMouseService extends Disposable implements ICoreMouseService {
export class MouseStateService extends Disposable implements IMouseStateService {
public serviceBrand: any;
private _protocols: { [name: string]: ICoreMouseProtocol } = {};
private _encodings: { [name: string]: CoreMouseEncoding } = {};
private _activeProtocol: string = '';
private _activeEncoding: string = '';
private _lastEvent: ICoreMouseEvent | null = null;
private _wheelPartialScroll: number = 0;
private _customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined;
private readonly _onProtocolChange = this._register(new Emitter<CoreMouseEventType>());
public readonly onProtocolChange = this._onProtocolChange.event;
constructor(
@IBufferService private readonly _bufferService: IBufferService,
@ICoreService private readonly _coreService: ICoreService,
@IOptionsService private readonly _optionsService: IOptionsService
) {
constructor() {
super();
// register default protocols and encodings
for (const name of Object.keys(DEFAULT_PROTOCOLS)) this.addProtocol(name, DEFAULT_PROTOCOLS[name]);
for (const name of Object.keys(DEFAULT_ENCODINGS)) this.addEncoding(name, DEFAULT_ENCODINGS[name]);
@@ -230,136 +226,29 @@ export class CoreMouseService extends Disposable implements ICoreMouseService {
public reset(): void {
this.activeProtocol = 'NONE';
this.activeEncoding = 'DEFAULT';
this._lastEvent = null;
this._wheelPartialScroll = 0;
}
/**
* Processes a wheel event, accounting for partial scrolls for trackpad, mouse scrolls.
* This prevents hyper-sensitive scrolling in alt buffer.
*/
public consumeWheelEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number {
// Do nothing if it's not a vertical scroll event
if (ev.deltaY === 0 || ev.shiftKey) {
return 0;
}
if (cellHeight === undefined || dpr === undefined) {
return 0;
}
const targetWheelEventPixels = cellHeight / dpr;
let amount = this._applyScrollModifier(ev.deltaY, ev);
if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
amount /= (targetWheelEventPixels + 0.0); // Prevent integer division
const isLikelyTrackpad = Math.abs(ev.deltaY) < 50;
if (isLikelyTrackpad) {
amount *= 0.3;
}
this._wheelPartialScroll += amount;
amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1);
this._wheelPartialScroll %= 1;
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
amount *= this._bufferService.rows;
}
return amount;
public setCustomWheelEventHandler(customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined): void {
this._customWheelEventHandler = customWheelEventHandler;
}
private _applyScrollModifier(amount: number, ev: WheelEvent): number {
// Multiply the scroll speed when the modifier key is pressed
if (ev.altKey || ev.ctrlKey || ev.shiftKey) {
return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity;
}
return amount * this._optionsService.rawOptions.scrollSensitivity;
public allowCustomWheelEvent(ev: WheelEvent): boolean {
return this._customWheelEventHandler ? this._customWheelEventHandler(ev) !== false : true;
}
/**
* Triggers a mouse event to be sent.
*
* Returns true if the event passed all protocol restrictions and a report
* was sent, otherwise false. The return value may be used to decide whether
* the default event action in the bowser component should be omitted.
*
* Note: The method will change values of the given event object
* to fullfill protocol and encoding restrictions.
*/
public triggerMouseEvent(e: ICoreMouseEvent): boolean {
// range check for col/row
if (e.col < 0 || e.col >= this._bufferService.cols
|| e.row < 0 || e.row >= this._bufferService.rows) {
return false;
}
// filter nonsense combinations of button + action
if (e.button === CoreMouseButton.WHEEL && e.action === CoreMouseAction.MOVE) {
return false;
}
if (e.button === CoreMouseButton.NONE && e.action !== CoreMouseAction.MOVE) {
return false;
}
if (e.button !== CoreMouseButton.WHEEL && (e.action === CoreMouseAction.LEFT || e.action === CoreMouseAction.RIGHT)) {
return false;
}
// report 1-based coords
e.col++;
e.row++;
// debounce move events at grid or pixel level
if (e.action === CoreMouseAction.MOVE
&& this._lastEvent
&& this._equalEvents(this._lastEvent, e, this._activeEncoding === 'SGR_PIXELS')
) {
return false;
}
// apply protocol restrictions
if (!this._protocols[this._activeProtocol].restrict(e)) {
return false;
}
// encode report and send
const report = this._encodings[this._activeEncoding](e);
if (report) {
// always send DEFAULT as binary data
if (this._activeEncoding === 'DEFAULT') {
this._coreService.triggerBinaryEvent(report);
} else {
this._coreService.triggerDataEvent(report, true);
}
}
this._lastEvent = e;
return true;
public restrictMouseEvent(e: ICoreMouseEvent): boolean {
return this._protocols[this._activeProtocol].restrict(e);
}
public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } {
return {
down: !!(events & CoreMouseEventType.DOWN),
up: !!(events & CoreMouseEventType.UP),
drag: !!(events & CoreMouseEventType.DRAG),
move: !!(events & CoreMouseEventType.MOVE),
wheel: !!(events & CoreMouseEventType.WHEEL)
};
public encodeMouseEvent(e: ICoreMouseEvent): string {
return this._encodings[this._activeEncoding](e);
}
private _equalEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent, pixels: boolean): boolean {
if (pixels) {
if (e1.x !== e2.x) return false;
if (e1.y !== e2.y) return false;
} else {
if (e1.col !== e2.col) return false;
if (e1.row !== e2.row) return false;
}
if (e1.button !== e2.button) return false;
if (e1.action !== e2.action) return false;
if (e1.ctrl !== e2.ctrl) return false;
if (e1.alt !== e2.alt) return false;
if (e1.shift !== e2.shift) return false;
return true;
public get isDefaultEncoding(): boolean {
return this._activeEncoding === 'DEFAULT';
}
public get isPixelEncoding(): boolean {
return this._activeEncoding === 'SGR_PIXELS';
}
}
+9 -25
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IDecoration, IDecorationOptions, ILinkHandler, ILogger, IWindowsPty, type IOverviewRulerOptions } from '@xterm/xterm';
import type { IDecoration, IDecorationOptions, ILinkHandler, ILogger, IWindowsPty, IOverviewRulerOptions } from '@xterm/xterm';
import { CoreMouseEncoding, CoreMouseEventType, CursorInactiveStyle, CursorStyle, IAttributeData, ICharset, IColor, ICoreMouseEvent, ICoreMouseProtocol, IDecPrivateModes, IDisposable, IKittyKeyboardState, IModes, IOscLinkData, IWindowOptions } from 'common/Types';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
@@ -33,8 +33,8 @@ export interface IBufferResizeEvent {
rowsChanged: boolean;
}
export const ICoreMouseService = createDecorator<ICoreMouseService>('CoreMouseService');
export interface ICoreMouseService {
export const IMouseStateService = createDecorator<IMouseStateService>('MouseStateService');
export interface IMouseStateService {
serviceBrand: undefined;
activeProtocol: string;
@@ -43,33 +43,17 @@ export interface ICoreMouseService {
addProtocol(name: string, protocol: ICoreMouseProtocol): void;
addEncoding(name: string, encoding: CoreMouseEncoding): void;
reset(): void;
/**
* Triggers a mouse event to be sent.
*
* Returns true if the event passed all protocol restrictions and a report
* was sent, otherwise false. The return value may be used to decide whether
* the default event action in the bowser component should be omitted.
*
* Note: The method will change values of the given event object
* to fullfill protocol and encoding restrictions.
*/
triggerMouseEvent(event: ICoreMouseEvent): boolean;
setCustomWheelEventHandler(customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined): void;
allowCustomWheelEvent(ev: WheelEvent): boolean;
/**
* Event to announce changes in mouse tracking.
*/
onProtocolChange: IEvent<CoreMouseEventType>;
/**
* Human readable version of mouse events.
*/
explainEvents(events: CoreMouseEventType): { [event: string]: boolean };
/**
* Process wheel event taking partial scroll into account.
*/
consumeWheelEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number;
restrictMouseEvent(event: ICoreMouseEvent): boolean;
encodeMouseEvent(event: ICoreMouseEvent): string;
readonly isDefaultEncoding: boolean;
readonly isPixelEncoding: boolean;
}
export const ICoreService = createDecorator<ICoreService>('CoreService');
+2 -2
View File
@@ -9,9 +9,9 @@
"types": [
"../../node_modules/@types/mocha"
],
"baseUrl": "..",
"paths": {
"common/*": [ "./common/*" ]
"common/*": [ "./../common/*" ],
"*": [ "./../*" ]
}
},
"include": [
+1 -1
View File
@@ -103,7 +103,7 @@ export class Terminal extends Disposable implements ITerminalApi {
public get modes(): IModes {
const m = this._core.coreService.decPrivateModes;
let mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any' = 'none';
switch (this._core.coreMouseService.activeProtocol) {
switch (this._core.mouseStateService.activeProtocol) {
case 'X10': mouseTrackingMode = 'x10'; break;
case 'VT200': mouseTrackingMode = 'vt200'; break;
case 'DRAG': mouseTrackingMode = 'drag'; break;
+2 -2
View File
@@ -10,9 +10,9 @@
"../../node_modules/@types/mocha",
"../../node_modules/@types/node"
],
"baseUrl": "../",
"paths": {
"common/*": [ "./common/*" ]
"common/*": [ "./../common/*" ],
"*": [ "./../*" ]
}
},
"include": [