Merge branch 'master' into master

This commit is contained in:
Adrian Błasiak
2025-04-15 11:03:04 +02:00
committed by GitHub
67 changed files with 1345 additions and 330 deletions
+14 -14
View File
@@ -202,38 +202,38 @@ describe('InputHandler', () => {
describe('setCursorStyle', () => {
it('should call Terminal.setOption with correct params', () => {
inputHandler.setCursorStyle(Params.fromArray([0]));
assert.equal(optionsService.options['cursorStyle'], 'block');
assert.equal(optionsService.options['cursorBlink'], true);
assert.equal(coreService.decPrivateModes.cursorStyle, undefined);
assert.equal(coreService.decPrivateModes.cursorBlink, undefined);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([1]));
assert.equal(optionsService.options['cursorStyle'], 'block');
assert.equal(optionsService.options['cursorBlink'], true);
assert.equal(coreService.decPrivateModes.cursorStyle, 'block');
assert.equal(coreService.decPrivateModes.cursorBlink, true);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([2]));
assert.equal(optionsService.options['cursorStyle'], 'block');
assert.equal(optionsService.options['cursorBlink'], false);
assert.equal(coreService.decPrivateModes.cursorStyle, 'block');
assert.equal(coreService.decPrivateModes.cursorBlink, false);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([3]));
assert.equal(optionsService.options['cursorStyle'], 'underline');
assert.equal(optionsService.options['cursorBlink'], true);
assert.equal(coreService.decPrivateModes.cursorStyle, 'underline');
assert.equal(coreService.decPrivateModes.cursorBlink, true);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([4]));
assert.equal(optionsService.options['cursorStyle'], 'underline');
assert.equal(optionsService.options['cursorBlink'], false);
assert.equal(coreService.decPrivateModes.cursorStyle, 'underline');
assert.equal(coreService.decPrivateModes.cursorBlink, false);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([5]));
assert.equal(optionsService.options['cursorStyle'], 'bar');
assert.equal(optionsService.options['cursorBlink'], true);
assert.equal(coreService.decPrivateModes.cursorStyle, 'bar');
assert.equal(coreService.decPrivateModes.cursorBlink, true);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([6]));
assert.equal(optionsService.options['cursorStyle'], 'bar');
assert.equal(optionsService.options['cursorBlink'], false);
assert.equal(coreService.decPrivateModes.cursorStyle, 'bar');
assert.equal(coreService.decPrivateModes.cursorBlink, false);
});
});
describe('setMode', () => {
+29 -23
View File
@@ -2729,7 +2729,7 @@ export class InputHandler extends Disposable implements IInputHandler {
/**
* CSI Ps SP q Set cursor style (DECSCUSR, VT520).
* Ps = 0 -> blinking block.
* Ps = 0 -> reset to option.
* Ps = 1 -> blinking block (default).
* Ps = 2 -> steady block.
* Ps = 3 -> blinking underline.
@@ -2739,31 +2739,37 @@ export class InputHandler extends Disposable implements IInputHandler {
*
* @vt: #Y CSI DECSCUSR "Set Cursor Style" "CSI Ps SP q" "Set cursor style."
* Supported cursor styles:
* - empty, 0 or 1: steady block
* - 2: blink block
* - 3: steady underline
* - 4: blink underline
* - 5: steady bar
* - 6: blink bar
* - 0: reset to option
* - empty, 1: blinking block
* - 2: steady block
* - 3: blinking underline
* - 4: steady underline
* - 5: blinking bar
* - 6: steady bar
*/
public setCursorStyle(params: IParams): boolean {
const param = params.params[0] || 1;
switch (param) {
case 1:
case 2:
this._optionsService.options.cursorStyle = 'block';
break;
case 3:
case 4:
this._optionsService.options.cursorStyle = 'underline';
break;
case 5:
case 6:
this._optionsService.options.cursorStyle = 'bar';
break;
const param = params.length === 0 ? 1 : params.params[0];
if (param === 0) {
this._coreService.decPrivateModes.cursorStyle = undefined;
this._coreService.decPrivateModes.cursorBlink = undefined;
} else {
switch (param) {
case 1:
case 2:
this._coreService.decPrivateModes.cursorStyle = 'block';
break;
case 3:
case 4:
this._coreService.decPrivateModes.cursorStyle = 'underline';
break;
case 5:
case 6:
this._coreService.decPrivateModes.cursorStyle = 'bar';
break;
}
const isBlinking = param % 2 === 1;
this._coreService.decPrivateModes.cursorBlink = isBlinking;
}
const isBlinking = param % 2 === 1;
this._optionsService.options.cursorBlink = isBlinking;
return true;
}
+2
View File
@@ -89,6 +89,8 @@ export class MockCoreService implements ICoreService {
applicationCursorKeys: false,
applicationKeypad: false,
bracketedPasteMode: false,
cursorBlink: undefined,
cursorStyle: undefined,
origin: false,
reverseWraparound: false,
sendFocus: false,
+2
View File
@@ -268,6 +268,8 @@ export interface IDecPrivateModes {
applicationCursorKeys: boolean;
applicationKeypad: boolean;
bracketedPasteMode: boolean;
cursorBlink: boolean | undefined;
cursorStyle: CursorStyle | undefined;
origin: boolean;
reverseWraparound: boolean;
sendFocus: boolean;
+84 -7
View File
@@ -315,11 +315,42 @@ export class Buffer implements IBuffer {
}
private _reflowLarger(newCols: number, newRows: number): void {
const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA));
const reflowCursorLine = this._optionsService.rawOptions.reflowCursorLine;
const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA), reflowCursorLine);
if (toRemove.length > 0) {
const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove);
reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout);
// For conpty, it has its own copy of the buffer _without scrollback_ internally. Its behavior
// when reflowing larger is to insert empty lines at the bottom of the buffer as when lines
// unwrap conpty's view cannot pull scrollback down, so it adds empty lines at the end.
let removedInViewport = 0;
const isWindowsMode = this._optionsService.rawOptions.windowsMode || this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined;
if (isWindowsMode) {
for (let i = (toRemove.length / 2) - 1; i >= 0; i--) {
if (toRemove[i * 2 + 0] > this.ybase + removedInViewport) {
removedInViewport += toRemove[i * 2 + 1];
}
}
}
this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved);
// Apply empty lines for any removed in viewport for conpty.
if (isWindowsMode) {
if (removedInViewport > 0) {
for (let i = 0; i < removedInViewport; i++) {
// Just add the new missing rows on Windows as conpty reprints the screen with it's
// view of the world. Once a line enters scrollback for conpty it remains there
this.lines.push(new BufferLine(newCols, this.getNullCell(DEFAULT_ATTR_DATA)));
}
if (this.ybase === this.ydisp) {
this.ydisp += removedInViewport;
}
this.ybase += removedInViewport;
this.y -= removedInViewport;
}
}
}
}
@@ -347,10 +378,11 @@ export class Buffer implements IBuffer {
}
private _reflowSmaller(newCols: number, newRows: number): void {
const reflowCursorLine = this._optionsService.rawOptions.reflowCursorLine;
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
// Gather all BufferLines that need to be inserted into the Buffer here so that they can be
// batched up and only committed once
const toInsert = [];
const toInsert: { start: number, newLines: IBufferLine[] }[] = [];
let countToInsert = 0;
// Go backwards as many lines may be trimmed and this will avoid considering them
for (let y = this.lines.length - 1; y >= 0; y--) {
@@ -367,11 +399,13 @@ export class Buffer implements IBuffer {
wrappedLines.unshift(nextLine);
}
// If these lines contain the cursor don't touch them, the program will handle fixing up
// wrapped lines with the cursor
const absoluteY = this.ybase + this.y;
if (absoluteY >= y && absoluteY < y + wrappedLines.length) {
continue;
if (!reflowCursorLine) {
// If these lines contain the cursor don't touch them, the program will handle fixing up
// wrapped lines with the cursor
const absoluteY = this.ybase + this.y;
if (absoluteY >= y && absoluteY < y + wrappedLines.length) {
continue;
}
}
const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength();
@@ -463,6 +497,20 @@ export class Buffer implements IBuffer {
this.savedY = Math.min(this.savedY + linesToAdd, this.ybase + newRows - 1);
}
// For conpty, it has its own copy of the buffer _without scrollback_ internally. Its behavior
// when reflowing smaller is to reflow all lines inside the viewport, and removing empty or
// whitespace only lines from the bottom, until non-whitespace is hit in order to prevent
// content from being pushed into the scrollback.
let addedInViewport = 0;
const isWindowsMode = this._optionsService.rawOptions.windowsMode || this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined;
if (isWindowsMode) {
for (let i = toInsert.length - 1; i >= 0; i--) {
if (toInsert[i].start > this.ybase + addedInViewport) {
addedInViewport += toInsert[i].newLines.length;
}
}
}
// Rearrange lines in the buffer if there are any insertions, this is done at the end rather
// than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many
// costly calls to CircularList.splice.
@@ -516,6 +564,35 @@ export class Buffer implements IBuffer {
this.lines.onTrimEmitter.fire(amountToTrim);
}
}
// Apply empty lines to remove calculated earlier for conpty.
if (isWindowsMode) {
if (addedInViewport > 0) {
let emptyLinesAtBottom = 0;
for (let i = this.lines.length - 1; i >= this.ybase + this.y; i--) {
const line = this.lines.get(i) as BufferLine;
if (line.isWrapped || line.getTrimmedLength() > 0) {
break;
}
emptyLinesAtBottom++;
}
const emptyLinesToRemove = Math.min(addedInViewport, emptyLinesAtBottom);
if (emptyLinesToRemove > 0) {
for (let i = 0; i < emptyLinesToRemove; i++) {
this.lines.pop();
}
if (this.ybase === this.ydisp) {
this.ydisp -= emptyLinesToRemove;
}
this.ybase -= emptyLinesToRemove;
this.y += emptyLinesToRemove;
this.lines.onDeleteEmitter.fire({
index: this.lines.length - emptyLinesToRemove,
amount: emptyLinesToRemove
});
}
}
}
}
/**
+9 -6
View File
@@ -20,8 +20,9 @@ export interface INewLayoutResult {
* @param newCols The columns after resize.
* @param bufferAbsoluteY The absolute y position of the cursor (baseY + cursorY).
* @param nullCell The cell data to use when filling in empty cells.
* @param reflowCursorLine Whether to reflow the line containing the cursor.
*/
export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData): number[] {
export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData, reflowCursorLine: boolean): number[] {
// Gather all BufferLines that need to be removed from the Buffer here so that they can be
// batched up and only committed once
const toRemove: number[] = [];
@@ -41,11 +42,13 @@ export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, o
nextLine = lines.get(++i) as BufferLine;
}
// If these lines contain the cursor don't touch them, the program will handle fixing up wrapped
// lines with the cursor
if (bufferAbsoluteY >= y && bufferAbsoluteY < i) {
y += wrappedLines.length - 1;
continue;
if (!reflowCursorLine) {
// If these lines contain the cursor don't touch them, the program will handle fixing up
// wrapped lines with the cursor
if (bufferAbsoluteY >= y && bufferAbsoluteY < i) {
y += wrappedLines.length - 1;
continue;
}
}
// Copy buffer data to new locations
+2
View File
@@ -17,6 +17,8 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({
applicationCursorKeys: false,
applicationKeypad: false,
bracketedPasteMode: false,
cursorBlink: undefined,
cursorStyle: undefined,
origin: false,
reverseWraparound: false,
sendFocus: false,
+1 -1
View File
@@ -67,7 +67,7 @@ export class InstantiationService implements IInstantiationService {
for (const dependency of serviceDependencies) {
const service = this._services.get(dependency.id);
if (!service) {
throw new Error(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`);
throw new Error(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id._id}.`);
}
serviceArgs.push(service);
}
+2 -1
View File
@@ -21,7 +21,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
documentOverride: null,
fastScrollModifier: 'alt',
fastScrollSensitivity: 5,
fontFamily: 'courier-new, courier, monospace',
fontFamily: 'monospace',
fontSize: 15,
fontWeight: 'normal',
fontWeightBold: 'bold',
@@ -44,6 +44,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
allowTransparency: false,
tabStopWidth: 8,
theme: {},
reflowCursorLine: false,
rescaleOverlappingGlyphs: false,
rightClickSelectsWord: isMac,
windowOptions: {},
+1 -1
View File
@@ -33,7 +33,7 @@ export function createDecorator<T>(id: string): IServiceIdentifier<T> {
storeServiceDependency(decorator, target, index);
};
decorator.toString = () => id;
decorator._id = id;
serviceRegistry.set(id, decorator);
return decorator;
+2
View File
@@ -124,6 +124,7 @@ export interface ICharsetService {
export interface IServiceIdentifier<T> {
(...args: any[]): void;
type: T;
_id: string;
}
export interface IBrandedService {
@@ -237,6 +238,7 @@ export interface ITerminalOptions {
macOptionIsMeta?: boolean;
macOptionClickForcesSelection?: boolean;
minimumContrastRatio?: number;
reflowCursorLine?: boolean;
rescaleOverlappingGlyphs?: boolean;
rightClickSelectsWord?: boolean;
rows?: number;