mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge branch 'master' into tyriar/search-refactor
This commit is contained in:
@@ -63,6 +63,14 @@ buffer-crc32@~0.2.3:
|
||||
version "0.2.13"
|
||||
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
|
||||
|
||||
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
|
||||
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
function-bind "^1.1.2"
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
@@ -79,6 +87,42 @@ diff@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
|
||||
|
||||
dunder-proto@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
|
||||
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
gopd "^1.2.0"
|
||||
|
||||
es-define-property@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
|
||||
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
|
||||
|
||||
es-errors@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||
|
||||
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
|
||||
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
|
||||
es-set-tostringtag@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
|
||||
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.6"
|
||||
has-tostringtag "^1.0.2"
|
||||
hasown "^2.0.2"
|
||||
|
||||
fd-slicer@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
|
||||
@@ -115,22 +159,77 @@ font-ligatures@^1.4.1:
|
||||
opentype.js "^0.8.0"
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
|
||||
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
|
||||
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
es-set-tostringtag "^2.1.0"
|
||||
hasown "^2.0.2"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||
|
||||
get-intrinsic@^1.2.6:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
|
||||
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.2"
|
||||
es-define-property "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
es-object-atoms "^1.1.1"
|
||||
function-bind "^1.1.2"
|
||||
get-proto "^1.0.1"
|
||||
gopd "^1.2.0"
|
||||
has-symbols "^1.1.0"
|
||||
hasown "^2.0.2"
|
||||
math-intrinsics "^1.1.0"
|
||||
|
||||
get-proto@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
|
||||
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
|
||||
dependencies:
|
||||
dunder-proto "^1.0.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
get-system-fonts@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-system-fonts/-/get-system-fonts-2.0.0.tgz#a43b9a33f05c0715a60176d2aad5ce6e98f0a3c6"
|
||||
|
||||
gopd@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
|
||||
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
|
||||
|
||||
has-flag@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
||||
|
||||
has-symbols@^1.0.3, has-symbols@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
|
||||
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
|
||||
|
||||
has-tostringtag@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
|
||||
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
|
||||
dependencies:
|
||||
has-symbols "^1.0.3"
|
||||
|
||||
hasown@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
|
||||
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
isarray@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
|
||||
@@ -168,6 +267,11 @@ lru-cache@^6.0.0:
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
math-intrinsics@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
|
||||
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import type { Terminal, IDisposable, ITerminalAddon, IDecoration } from '@xterm/xterm';
|
||||
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchDecorationOptions } from '@xterm/addon-search';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { combinedDisposable, Disposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
interface IInternalSearchOptions {
|
||||
@@ -17,6 +17,11 @@ export interface ISearchPosition {
|
||||
startRow: number;
|
||||
}
|
||||
|
||||
export interface ISearchResultChangeEvent {
|
||||
resultIndex: number;
|
||||
resultCount: number;
|
||||
}
|
||||
|
||||
export interface ISearchAddonOptions {
|
||||
highlightLimit: number;
|
||||
}
|
||||
@@ -49,11 +54,32 @@ interface IMultiHighlight extends IDisposable {
|
||||
match: ISearchResult;
|
||||
}
|
||||
|
||||
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?';
|
||||
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
|
||||
const DEFAULT_HIGHLIGHT_LIMIT = 1000;
|
||||
/**
|
||||
* Configuration constants for the search addon functionality.
|
||||
*/
|
||||
const enum Constants {
|
||||
/**
|
||||
* Characters that are considered non-word characters for search boundary detection. These
|
||||
* characters are used to determine word boundaries when performing whole-word searches. Includes
|
||||
* common punctuation, symbols, and whitespace characters.
|
||||
*/
|
||||
NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?',
|
||||
|
||||
export class SearchAddon extends Disposable implements ITerminalAddon , ISearchApi {
|
||||
/**
|
||||
* Time-to-live for cached search results in milliseconds. After this duration, cached search
|
||||
* results will be invalidated to ensure they remain consistent with terminal content changes.
|
||||
*/
|
||||
LINES_CACHE_TIME_TO_LIVE = 15000,
|
||||
|
||||
/**
|
||||
* Default maximum number of search results to highlight simultaneously. This limit prevents
|
||||
* performance degradation when searching for very common terms that would result in excessive
|
||||
* highlighting decorations.
|
||||
*/
|
||||
DEFAULT_HIGHLIGHT_LIMIT = 1000
|
||||
}
|
||||
|
||||
export class SearchAddon extends Disposable implements ITerminalAddon, ISearchApi {
|
||||
private _terminal: Terminal | undefined;
|
||||
private _cachedSearchTerm: string | undefined;
|
||||
private _highlightedLines: Set<number> = new Set();
|
||||
@@ -72,13 +98,13 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
|
||||
private _linesCacheTimeoutId = 0;
|
||||
private _linesCacheDisposables = new MutableDisposable();
|
||||
|
||||
private readonly _onDidChangeResults = this._register(new Emitter<{ resultIndex: number, resultCount: number }>());
|
||||
public readonly onDidChangeResults = this._onDidChangeResults.event;
|
||||
private readonly _onDidChangeResults = this._register(new Emitter<ISearchResultChangeEvent>());
|
||||
public get onDidChangeResults(): Event<ISearchResultChangeEvent> { return this._onDidChangeResults.event; }
|
||||
|
||||
constructor(options?: Partial<ISearchAddonOptions>) {
|
||||
super();
|
||||
|
||||
this._highlightLimit = options?.highlightLimit ?? DEFAULT_HIGHLIGHT_LIMIT;
|
||||
this._highlightLimit = options?.highlightLimit ?? Constants.DEFAULT_HIGHLIGHT_LIMIT;
|
||||
}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
@@ -435,7 +461,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
|
||||
}
|
||||
|
||||
window.clearTimeout(this._linesCacheTimeoutId);
|
||||
this._linesCacheTimeoutId = window.setTimeout(() => this._destroyLinesCache(), LINES_CACHE_TIME_TO_LIVE);
|
||||
this._linesCacheTimeoutId = window.setTimeout(() => this._destroyLinesCache(), Constants.LINES_CACHE_TIME_TO_LIVE);
|
||||
}
|
||||
|
||||
private _destroyLinesCache(): void {
|
||||
@@ -455,8 +481,8 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
|
||||
* @param term the substring that starts at searchIndex
|
||||
*/
|
||||
private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
|
||||
return ((searchIndex === 0) || (NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) &&
|
||||
(((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));
|
||||
return ((searchIndex === 0) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) &&
|
||||
(((searchIndex + term.length) === line.length) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,8 +19,8 @@ test.afterAll(async () => await ctx.page.close());
|
||||
test.describe('Search Tests', () => {
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await ctx.proxy.reset();
|
||||
await ctx.page.evaluate(`
|
||||
window.term.reset()
|
||||
window.search?.dispose();
|
||||
window.search = new SearchAddon();
|
||||
window.term.loadAddon(window.search);
|
||||
|
||||
+16
-2
@@ -75,6 +75,21 @@ declare module '@xterm/addon-search' {
|
||||
activeMatchColorOverviewRuler: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event data fired when search results change.
|
||||
*/
|
||||
export interface ISearchResultChangeEvent {
|
||||
/**
|
||||
* The index of the currently active result, -1 when the threshold of matches is exceeded.
|
||||
*/
|
||||
resultIndex: number;
|
||||
|
||||
/**
|
||||
* The total number of search results found.
|
||||
*/
|
||||
resultCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the search addon.
|
||||
*/
|
||||
@@ -139,8 +154,7 @@ declare module '@xterm/addon-search' {
|
||||
/**
|
||||
* When decorations are enabled, fires when
|
||||
* the search results change.
|
||||
* @returns -1 for resultIndex when the threshold of matches is exceeded.
|
||||
*/
|
||||
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number }>;
|
||||
readonly onDidChangeResults: IEvent<ISearchResultChangeEvent>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,6 +646,14 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
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;
|
||||
@@ -817,6 +825,15 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lines = self.coreMouseService.consumeWheelEvent(
|
||||
ev as WheelEvent,
|
||||
self._renderService?.dimensions?.device?.cell?.height,
|
||||
self._coreBrowserService?.dpr
|
||||
);
|
||||
if (lines === 0) {
|
||||
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);
|
||||
|
||||
@@ -20,6 +20,7 @@ export class MockBufferService implements IBufferService {
|
||||
public buffers: IBufferSet = {} as any;
|
||||
public onResize: Event<{ cols: number, rows: number }> = new Emitter<{ cols: number, rows: number }>().event;
|
||||
public onScroll: Event<number> = new Emitter<number>().event;
|
||||
private readonly _onScroll = new Emitter<number>();
|
||||
public isUserScrolling: boolean = false;
|
||||
constructor(
|
||||
public cols: number,
|
||||
@@ -27,6 +28,10 @@ export class MockBufferService implements IBufferService {
|
||||
optionsService: IOptionsService = new MockOptionsService()
|
||||
) {
|
||||
this.buffers = new BufferSet(optionsService, this);
|
||||
// Listen to buffer activation events and automatically fire scroll events
|
||||
this.buffers.onBufferActivate(e => {
|
||||
this._onScroll.fire(e.activeBuffer.ydisp);
|
||||
});
|
||||
}
|
||||
public scrollPages(pageCount: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
@@ -66,6 +71,9 @@ export class MockCoreMouseService implements ICoreMouseService {
|
||||
public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public consumeWheelEvent(ev: WheelEvent, cellHeight: number, dpr: number): number {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockCharsetService implements ICharsetService {
|
||||
|
||||
@@ -37,6 +37,9 @@ export class BufferService extends Disposable implements IBufferService {
|
||||
this.cols = Math.max(optionsService.rawOptions.cols || 0, MINIMUM_COLS);
|
||||
this.rows = Math.max(optionsService.rawOptions.rows || 0, MINIMUM_ROWS);
|
||||
this.buffers = this._register(new BufferSet(optionsService, this));
|
||||
this._register(this.buffers.onBufferActivate(e => {
|
||||
this._onScroll.fire(e.activeBuffer.ydisp);
|
||||
}));
|
||||
}
|
||||
|
||||
public resize(cols: number, rows: number): void {
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
import { CoreMouseService } from 'common/services/CoreMouseService';
|
||||
import { MockCoreService, MockBufferService } from 'common/TestUtils.test';
|
||||
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) {
|
||||
@@ -24,20 +25,20 @@ function toBytes(s: string | undefined): number[] {
|
||||
|
||||
describe('CoreMouseService', () => {
|
||||
it('init', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
const cms = new CoreMouseService(bufferService, coreService, optionsService);
|
||||
cms.activeEncoding = 'SGR';
|
||||
cms.activeProtocol = 'ANY';
|
||||
assert.equal(cms.activeEncoding, 'SGR');
|
||||
@@ -49,19 +50,19 @@ describe('CoreMouseService', () => {
|
||||
assert.throws(() => { cms.activeProtocol = 'xyz'; }, 'unknown protocol "xyz"');
|
||||
});
|
||||
it('addEncoding', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
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);
|
||||
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);
|
||||
const cms = new CoreMouseService(bufferService, coreService, optionsService);
|
||||
const wantedEvents: CoreMouseEventType[] = [];
|
||||
cms.onProtocolChange(events => wantedEvents.push(events));
|
||||
cms.activeProtocol = 'NONE';
|
||||
@@ -76,7 +77,7 @@ describe('CoreMouseService', () => {
|
||||
let cms: CoreMouseService;
|
||||
let reports: string[];
|
||||
beforeEach(() => {
|
||||
cms = new CoreMouseService(bufferService, coreService);
|
||||
cms = new CoreMouseService(bufferService, coreService, optionsService);
|
||||
reports = [];
|
||||
coreService.triggerDataEvent = (data: string, userInput?: boolean) => reports.push(data);
|
||||
coreService.triggerBinaryEvent = (data: string) => reports.push(data);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services';
|
||||
import { IBufferService, ICoreService, ICoreMouseService, IOptionsService } from 'common/services/Services';
|
||||
import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
@@ -174,13 +174,15 @@ export class CoreMouseService extends Disposable implements ICoreMouseService {
|
||||
private _activeProtocol: string = '';
|
||||
private _activeEncoding: string = '';
|
||||
private _lastEvent: ICoreMouseEvent | null = null;
|
||||
private _wheelPartialScroll: number = 0;
|
||||
|
||||
private readonly _onProtocolChange = this._register(new Emitter<CoreMouseEventType>());
|
||||
public readonly onProtocolChange = this._onProtocolChange.event;
|
||||
public readonly onProtocolChange = this._onProtocolChange.event;
|
||||
|
||||
constructor(
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@ICoreService private readonly _coreService: ICoreService
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService
|
||||
) {
|
||||
super();
|
||||
// register default protocols and encodings
|
||||
@@ -229,6 +231,49 @@ export class CoreMouseService extends Disposable implements ICoreMouseService {
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -58,6 +58,11 @@ export interface ICoreMouseService {
|
||||
* 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;
|
||||
}
|
||||
|
||||
export const ICoreService = createDecorator<ICoreService>('CoreService');
|
||||
|
||||
Reference in New Issue
Block a user