@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
789 lines (788 loc) • 30.1 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate =
(this && this.__decorate) ||
function (decorators, target, key, desc) {
var c = arguments.length,
r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc,
d;
if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function')
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param =
(this && this.__param) ||
function (paramIndex, decorator) {
return function (target, key) {
decorator(target, key, paramIndex);
};
};
import { timeout } from '@sussudio/base/common/async.mjs';
import { debounce } from '@sussudio/base/common/decorators.mjs';
import { Emitter } from '@sussudio/base/common/event.mjs';
import { ILogService } from '../../../log/common/log.mjs';
let CommandDetectionCapability = class CommandDetectionCapability {
_terminal;
_logService;
type = 2 /* TerminalCapability.CommandDetection */;
_commands = [];
_exitCode;
_cwd;
_currentCommand = {};
_isWindowsPty = false;
_onCursorMoveListener;
_commandMarkers = [];
_dimensions;
__isCommandStorageDisabled = false;
_handleCommandStartOptions;
get commands() {
return this._commands;
}
get executingCommand() {
return this._currentCommand.command;
}
// TODO: as is unsafe here and it duplicates behavor of executingCommand
get executingCommandObject() {
if (this._currentCommand.commandStartMarker) {
return { marker: this._currentCommand.commandStartMarker };
}
return undefined;
}
get cwd() {
return this._cwd;
}
get _isInputting() {
return !!(this._currentCommand.commandStartMarker && !this._currentCommand.commandExecutedMarker);
}
get hasInput() {
if (!this._isInputting || !this._currentCommand?.commandStartMarker) {
return undefined;
}
if (
this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY ===
this._currentCommand.commandStartMarker?.line
) {
const line = this._terminal.buffer.active
.getLine(this._terminal.buffer.active.cursorY)
?.translateToString(true, this._currentCommand.commandStartX);
if (line === undefined) {
return undefined;
}
return line.length > 0;
}
return true;
}
_onCommandStarted = new Emitter();
onCommandStarted = this._onCommandStarted.event;
_onBeforeCommandFinished = new Emitter();
onBeforeCommandFinished = this._onBeforeCommandFinished.event;
_onCommandFinished = new Emitter();
onCommandFinished = this._onCommandFinished.event;
_onCommandExecuted = new Emitter();
onCommandExecuted = this._onCommandExecuted.event;
_onCommandInvalidated = new Emitter();
onCommandInvalidated = this._onCommandInvalidated.event;
_onCurrentCommandInvalidated = new Emitter();
onCurrentCommandInvalidated = this._onCurrentCommandInvalidated.event;
constructor(_terminal, _logService) {
this._terminal = _terminal;
this._logService = _logService;
this._dimensions = {
cols: this._terminal.cols,
rows: this._terminal.rows,
};
this._terminal.onResize((e) => this._handleResize(e));
this._terminal.onCursorMove(() => this._handleCursorMove());
this._setupClearListeners();
}
_handleResize(e) {
if (this._isWindowsPty) {
this._preHandleResizeWindows(e);
}
this._dimensions.cols = e.cols;
this._dimensions.rows = e.rows;
}
_handleCursorMove() {
// Early versions of conpty do not have real support for an alt buffer, in addition certain
// commands such as tsc watch will write to the top of the normal buffer. The following
// checks when the cursor has moved while the normal buffer is empty and if it is above the
// current command, all decorations within the viewport will be invalidated.
//
// This function is debounced so that the cursor is only checked when it is stable so
// conpty's screen reprinting will not trigger decoration clearing.
//
// This is mostly a workaround for Windows but applies to all OS' because of the tsc watch
// case.
if (this._terminal.buffer.active === this._terminal.buffer.normal && this._currentCommand.commandStartMarker) {
if (
this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY <
this._currentCommand.commandStartMarker.line
) {
this._clearCommandsInViewport();
this._currentCommand.isInvalid = true;
this._onCurrentCommandInvalidated.fire({ reason: 'windows' /* CommandInvalidationReason.Windows */ });
}
}
}
_setupClearListeners() {
// Setup listeners for when clear is run in the shell. Since we don't know immediately if
// this is a Windows pty, listen to both routes and do the Windows check inside them
// For a Windows backend we cannot listen to CSI J, instead we assume running clear or
// cls will clear all commands in the viewport. This is not perfect but it's right most
// of the time.
this.onBeforeCommandFinished((command) => {
if (this._isWindowsPty) {
if (command.command.trim().toLowerCase() === 'clear' || command.command.trim().toLowerCase() === 'cls') {
this._clearCommandsInViewport();
this._currentCommand.isInvalid = true;
this._onCurrentCommandInvalidated.fire({ reason: 'windows' /* CommandInvalidationReason.Windows */ });
}
}
});
// For non-Windows backends we can just listen to CSI J which is what the clear command
// typically emits.
this._terminal.parser.registerCsiHandler({ final: 'J' }, (params) => {
if (!this._isWindowsPty) {
if (params.length >= 1 && (params[0] === 2 || params[0] === 3)) {
this._clearCommandsInViewport();
}
}
// We don't want to override xterm.js' default behavior, just augment it
return false;
});
}
_preHandleResizeWindows(e) {
// Resize behavior is different under conpty; instead of bringing parts of the scrollback
// back into the viewport, new lines are inserted at the bottom (ie. the same behavior as if
// there was no scrollback).
//
// On resize this workaround will wait for a conpty reprint to occur by waiting for the
// cursor to move, it will then calculate the number of lines that the commands within the
// viewport _may have_ shifted. After verifying the content of the current line is
// incorrect, the line after shifting is checked and if that matches delete events are fired
// on the xterm.js buffer to move the markers.
//
// While a bit hacky, this approach is quite safe and seems to work great at least for pwsh.
const baseY = this._terminal.buffer.active.baseY;
const rowsDifference = e.rows - this._dimensions.rows;
// Only do when rows increase, do in the next frame as this needs to happen after
// conpty reprints the screen
if (rowsDifference > 0) {
this._waitForCursorMove().then(() => {
// Calculate the number of lines the content may have shifted, this will max out at
// scrollback count since the standard behavior will be used then
const potentialShiftedLineCount = Math.min(rowsDifference, baseY);
// For each command within the viewport, assume commands are in the correct order
for (let i = this.commands.length - 1; i >= 0; i--) {
const command = this.commands[i];
if (!command.marker || command.marker.line < baseY || command.commandStartLineContent === undefined) {
break;
}
const line = this._terminal.buffer.active.getLine(command.marker.line);
if (!line || line.translateToString(true) === command.commandStartLineContent) {
continue;
}
const shiftedY = command.marker.line - potentialShiftedLineCount;
const shiftedLine = this._terminal.buffer.active.getLine(shiftedY);
if (shiftedLine?.translateToString(true) !== command.commandStartLineContent) {
continue;
}
// HACK: xterm.js doesn't expose this by design as it's an internal core
// function an embedder could easily do damage with. Additionally, this
// can't really be upstreamed since the event relies on shell integration to
// verify the shifting is necessary.
this._terminal._core._bufferService.buffer.lines.onDeleteEmitter.fire({
index: this._terminal.buffer.active.baseY,
amount: potentialShiftedLineCount,
});
}
});
}
}
_clearCommandsInViewport() {
// Find the number of commands on the tail end of the array that are within the viewport
let count = 0;
for (let i = this._commands.length - 1; i >= 0; i--) {
const line = this._commands[i].marker?.line;
if (line && line < this._terminal.buffer.active.baseY) {
break;
}
count++;
}
// Remove them
if (count > 0) {
this._onCommandInvalidated.fire(this._commands.splice(this._commands.length - count, count));
}
}
_waitForCursorMove() {
const cursorX = this._terminal.buffer.active.cursorX;
const cursorY = this._terminal.buffer.active.cursorY;
let totalDelay = 0;
return new Promise((resolve, reject) => {
const interval = setInterval(() => {
if (cursorX !== this._terminal.buffer.active.cursorX || cursorY !== this._terminal.buffer.active.cursorY) {
resolve();
clearInterval(interval);
return;
}
totalDelay += 10;
if (totalDelay > 1000) {
clearInterval(interval);
resolve();
}
}, 10);
});
}
setCwd(value) {
this._cwd = value;
}
setIsWindowsPty(value) {
this._isWindowsPty = value;
}
setIsCommandStorageDisabled() {
this.__isCommandStorageDisabled = true;
}
getCwdForLine(line) {
// Handle the current partial command first, anything below it's prompt is considered part
// of the current command
if (this._currentCommand.promptStartMarker && line >= this._currentCommand.promptStartMarker?.line) {
return this._cwd;
}
// TODO: It would be more reliable to take the closest cwd above the line if it isn't found for the line
// TODO: Use a reverse for loop to find the line to avoid creating another array
const reversed = [...this._commands].reverse();
return reversed.find((c) => c.marker.line <= line - 1)?.cwd;
}
handlePromptStart(options) {
this._currentCommand.promptStartMarker = options?.marker || this._terminal.registerMarker(0);
this._logService.debug(
'CommandDetectionCapability#handlePromptStart',
this._terminal.buffer.active.cursorX,
this._currentCommand.promptStartMarker?.line,
);
}
handleContinuationStart() {
this._currentCommand.currentContinuationMarker = this._terminal.registerMarker(0);
this._logService.debug(
'CommandDetectionCapability#handleContinuationStart',
this._currentCommand.currentContinuationMarker,
);
}
handleContinuationEnd() {
if (!this._currentCommand.currentContinuationMarker) {
this._logService.warn('CommandDetectionCapability#handleContinuationEnd Received continuation end without start');
return;
}
if (!this._currentCommand.continuations) {
this._currentCommand.continuations = [];
}
this._currentCommand.continuations.push({
marker: this._currentCommand.currentContinuationMarker,
end: this._terminal.buffer.active.cursorX,
});
this._currentCommand.currentContinuationMarker = undefined;
this._logService.debug(
'CommandDetectionCapability#handleContinuationEnd',
this._currentCommand.continuations[this._currentCommand.continuations.length - 1],
);
}
handleRightPromptStart() {
this._currentCommand.commandRightPromptStartX = this._terminal.buffer.active.cursorX;
this._logService.debug(
'CommandDetectionCapability#handleRightPromptStart',
this._currentCommand.commandRightPromptStartX,
);
}
handleRightPromptEnd() {
this._currentCommand.commandRightPromptEndX = this._terminal.buffer.active.cursorX;
this._logService.debug(
'CommandDetectionCapability#handleRightPromptEnd',
this._currentCommand.commandRightPromptEndX,
);
}
handleCommandStart(options) {
this._handleCommandStartOptions = options;
// Only update the column if the line has already been set
this._currentCommand.commandStartMarker = options?.marker || this._currentCommand.commandStartMarker;
if (this._currentCommand.commandStartMarker?.line === this._terminal.buffer.active.cursorY) {
this._currentCommand.commandStartX = this._terminal.buffer.active.cursorX;
this._logService.debug(
'CommandDetectionCapability#handleCommandStart',
this._currentCommand.commandStartX,
this._currentCommand.commandStartMarker?.line,
);
return;
}
if (this._isWindowsPty) {
this._handleCommandStartWindows();
return;
}
this._currentCommand.commandStartX = this._terminal.buffer.active.cursorX;
this._currentCommand.commandStartMarker = options?.marker || this._terminal.registerMarker(0);
// Clear executed as it must happen after command start
this._currentCommand.commandExecutedMarker?.dispose();
this._currentCommand.commandExecutedMarker = undefined;
this._currentCommand.commandExecutedX = undefined;
for (const m of this._commandMarkers) {
m.dispose();
}
this._commandMarkers.length = 0;
this._onCommandStarted.fire({
marker: options?.marker || this._currentCommand.commandStartMarker,
markProperties: options?.markProperties,
});
this._logService.debug(
'CommandDetectionCapability#handleCommandStart',
this._currentCommand.commandStartX,
this._currentCommand.commandStartMarker?.line,
);
}
_handleCommandStartWindows() {
this._currentCommand.commandStartX = this._terminal.buffer.active.cursorX;
// On Windows track all cursor movements after the command start sequence
this._commandMarkers.length = 0;
// HACK: Fire command started on the following frame on Windows to allow the cursor
// position to update as conpty often prints the sequence on a different line to the
// actual line the command started on.
timeout(0).then(() => {
if (!this._currentCommand.commandExecutedMarker) {
this._onCursorMoveListener = this._terminal.onCursorMove(() => {
if (
this._commandMarkers.length === 0 ||
this._commandMarkers[this._commandMarkers.length - 1].line !== this._terminal.buffer.active.cursorY
) {
const marker = this._terminal.registerMarker(0);
if (marker) {
this._commandMarkers.push(marker);
}
}
});
}
this._currentCommand.commandStartMarker = this._terminal.registerMarker(0);
if (this._currentCommand.commandStartMarker) {
const line = this._terminal.buffer.active.getLine(this._currentCommand.commandStartMarker.line);
if (line) {
this._currentCommand.commandStartLineContent = line.translateToString(true);
}
}
this._onCommandStarted.fire({ marker: this._currentCommand.commandStartMarker });
this._logService.debug(
'CommandDetectionCapability#_handleCommandStartWindows',
this._currentCommand.commandStartX,
this._currentCommand.commandStartMarker?.line,
);
});
}
handleGenericCommand(options) {
if (options?.markProperties?.disableCommandStorage) {
this.setIsCommandStorageDisabled();
}
this.handlePromptStart(options);
this.handleCommandStart(options);
this.handleCommandExecuted(options);
this.handleCommandFinished(undefined, options);
}
handleCommandExecuted(options) {
if (this._isWindowsPty) {
this._handleCommandExecutedWindows();
return;
}
this._currentCommand.commandExecutedMarker = options?.marker || this._terminal.registerMarker(0);
this._currentCommand.commandExecutedX = this._terminal.buffer.active.cursorX;
this._logService.debug(
'CommandDetectionCapability#handleCommandExecuted',
this._currentCommand.commandExecutedX,
this._currentCommand.commandExecutedMarker?.line,
);
// Sanity check optional props
if (
!this._currentCommand.commandStartMarker ||
!this._currentCommand.commandExecutedMarker ||
this._currentCommand.commandStartX === undefined
) {
return;
}
// Calculate the command
this._currentCommand.command = this.__isCommandStorageDisabled
? ''
: this._terminal.buffer.active
.getLine(this._currentCommand.commandStartMarker.line)
?.translateToString(true, this._currentCommand.commandStartX, this._currentCommand.commandRightPromptStartX)
.trim();
let y = this._currentCommand.commandStartMarker.line + 1;
const commandExecutedLine = this._currentCommand.commandExecutedMarker.line;
for (; y < commandExecutedLine; y++) {
const line = this._terminal.buffer.active.getLine(y);
if (line) {
const continuation = this._currentCommand.continuations?.find((e) => e.marker.line === y);
if (continuation) {
this._currentCommand.command += '\n';
}
const startColumn = continuation?.end ?? 0;
this._currentCommand.command += line.translateToString(true, startColumn);
}
}
if (y === commandExecutedLine) {
this._currentCommand.command +=
this._terminal.buffer.active
.getLine(commandExecutedLine)
?.translateToString(true, undefined, this._currentCommand.commandExecutedX) || '';
}
this._onCommandExecuted.fire();
}
_handleCommandExecutedWindows() {
// On Windows, use the gathered cursor move markers to correct the command start and
// executed markers
this._onCursorMoveListener?.dispose();
this._onCursorMoveListener = undefined;
this._evaluateCommandMarkersWindows();
this._currentCommand.commandExecutedX = this._terminal.buffer.active.cursorX;
this._onCommandExecuted.fire();
this._logService.debug(
'CommandDetectionCapability#handleCommandExecuted',
this._currentCommand.commandExecutedX,
this._currentCommand.commandExecutedMarker?.line,
);
}
invalidateCurrentCommand(request) {
this._currentCommand.isInvalid = true;
this._onCurrentCommandInvalidated.fire(request);
}
handleCommandFinished(exitCode, options) {
if (this._isWindowsPty) {
this._preHandleCommandFinishedWindows();
}
this._currentCommand.commandFinishedMarker = options?.marker || this._terminal.registerMarker(0);
let command = this._currentCommand.command;
this._logService.debug(
'CommandDetectionCapability#handleCommandFinished',
this._terminal.buffer.active.cursorX,
this._currentCommand.commandFinishedMarker?.line,
this._currentCommand.command,
this._currentCommand,
);
this._exitCode = exitCode;
// HACK: Handle a special case on some versions of bash where identical commands get merged
// in the output of `history`, this detects that case and sets the exit code to the the last
// command's exit code. This covered the majority of cases but will fail if the same command
// runs with a different exit code, that will need a more robust fix where we send the
// command ID and exit code over to the capability to adjust there.
if (this._exitCode === undefined) {
const lastCommand = this.commands.length > 0 ? this.commands[this.commands.length - 1] : undefined;
if (command && command.length > 0 && lastCommand?.command === command) {
this._exitCode = lastCommand.exitCode;
}
}
if (this._currentCommand.commandStartMarker === undefined || !this._terminal.buffer.active) {
return;
}
// When the command finishes and executed never fires the placeholder selector should be used.
if (this._exitCode === undefined && command === undefined) {
command = '';
}
if ((command !== undefined && !command.startsWith('\\')) || this._handleCommandStartOptions?.ignoreCommandLine) {
const buffer = this._terminal.buffer.active;
const timestamp = Date.now();
const executedMarker = this._currentCommand.commandExecutedMarker;
const endMarker = this._currentCommand.commandFinishedMarker;
const newCommand = {
command: this._handleCommandStartOptions?.ignoreCommandLine ? '' : command || '',
marker: this._currentCommand.commandStartMarker,
endMarker,
executedMarker,
timestamp,
cwd: this._cwd,
exitCode: this._exitCode,
commandStartLineContent: this._currentCommand.commandStartLineContent,
hasOutput: () =>
!executedMarker?.isDisposed &&
!endMarker?.isDisposed &&
!!(executedMarker && endMarker && executedMarker?.line < endMarker.line),
getOutput: () => getOutputForCommand(executedMarker, endMarker, buffer),
getOutputMatch: (outputMatcher) =>
getOutputMatchForCommand(executedMarker, endMarker, buffer, this._terminal.cols, outputMatcher),
markProperties: options?.markProperties,
};
this._commands.push(newCommand);
this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand);
this._onBeforeCommandFinished.fire(newCommand);
if (!this._currentCommand.isInvalid) {
this._onCommandFinished.fire(newCommand);
}
}
this._currentCommand.previousCommandMarker = this._currentCommand.commandStartMarker;
this._currentCommand = {};
this._handleCommandStartOptions = undefined;
}
_preHandleCommandFinishedWindows() {
if (this._currentCommand.commandExecutedMarker) {
return;
}
// This is done on command finished just in case command executed never happens (for example
// PSReadLine tab completion)
if (this._commandMarkers.length === 0) {
// If the command start timeout doesn't happen before command finished, just use the
// current marker.
if (!this._currentCommand.commandStartMarker) {
this._currentCommand.commandStartMarker = this._terminal.registerMarker(0);
}
if (this._currentCommand.commandStartMarker) {
this._commandMarkers.push(this._currentCommand.commandStartMarker);
}
}
this._evaluateCommandMarkersWindows();
}
_evaluateCommandMarkersWindows() {
// On Windows, use the gathered cursor move markers to correct the command start and
// executed markers.
if (this._commandMarkers.length === 0) {
return;
}
this._commandMarkers = this._commandMarkers.sort((a, b) => a.line - b.line);
this._currentCommand.commandStartMarker = this._commandMarkers[0];
if (this._currentCommand.commandStartMarker) {
const line = this._terminal.buffer.active.getLine(this._currentCommand.commandStartMarker.line);
if (line) {
this._currentCommand.commandStartLineContent = line.translateToString(true);
}
}
this._currentCommand.commandExecutedMarker = this._commandMarkers[this._commandMarkers.length - 1];
}
setCommandLine(commandLine) {
this._logService.debug('CommandDetectionCapability#setCommandLine', commandLine);
this._currentCommand.command = commandLine;
}
serialize() {
const commands = this.commands.map((e) => {
return {
startLine: e.marker?.line,
startX: undefined,
endLine: e.endMarker?.line,
executedLine: e.executedMarker?.line,
command: this.__isCommandStorageDisabled ? '' : e.command,
cwd: e.cwd,
exitCode: e.exitCode,
commandStartLineContent: e.commandStartLineContent,
timestamp: e.timestamp,
markProperties: e.markProperties,
aliases: e.aliases,
};
});
if (this._currentCommand.commandStartMarker) {
commands.push({
startLine: this._currentCommand.commandStartMarker.line,
startX: this._currentCommand.commandStartX,
endLine: undefined,
executedLine: undefined,
command: '',
cwd: this._cwd,
exitCode: undefined,
commandStartLineContent: undefined,
timestamp: 0,
markProperties: undefined,
});
}
return {
isWindowsPty: this._isWindowsPty,
commands,
};
}
deserialize(serialized) {
if (serialized.isWindowsPty) {
this.setIsWindowsPty(serialized.isWindowsPty);
}
const buffer = this._terminal.buffer.normal;
for (const e of serialized.commands) {
const marker =
e.startLine !== undefined
? this._terminal.registerMarker(e.startLine - (buffer.baseY + buffer.cursorY))
: undefined;
// Check for invalid command
if (!marker) {
continue;
}
// Partial command
if (!e.endLine) {
this._currentCommand.commandStartMarker = marker;
this._currentCommand.commandStartX = e.startX;
this._cwd = e.cwd;
this._onCommandStarted.fire({ marker });
continue;
}
// Full command
const endMarker =
e.endLine !== undefined
? this._terminal.registerMarker(e.endLine - (buffer.baseY + buffer.cursorY))
: undefined;
const executedMarker =
e.executedLine !== undefined
? this._terminal.registerMarker(e.executedLine - (buffer.baseY + buffer.cursorY))
: undefined;
const newCommand = {
command: this.__isCommandStorageDisabled ? '' : e.command,
marker,
endMarker,
executedMarker,
timestamp: e.timestamp,
cwd: e.cwd,
commandStartLineContent: e.commandStartLineContent,
exitCode: e.exitCode,
hasOutput: () =>
!executedMarker?.isDisposed &&
!endMarker?.isDisposed &&
!!(executedMarker && endMarker && executedMarker.line < endMarker.line),
getOutput: () => getOutputForCommand(executedMarker, endMarker, buffer),
getOutputMatch: (outputMatcher) =>
getOutputMatchForCommand(executedMarker, endMarker, buffer, this._terminal.cols, outputMatcher),
markProperties: e.markProperties,
};
this._commands.push(newCommand);
this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand);
this._onCommandFinished.fire(newCommand);
}
}
};
__decorate([debounce(500)], CommandDetectionCapability.prototype, '_handleCursorMove', null);
CommandDetectionCapability = __decorate([__param(1, ILogService)], CommandDetectionCapability);
export { CommandDetectionCapability };
function getOutputForCommand(executedMarker, endMarker, buffer) {
if (!executedMarker || !endMarker) {
return undefined;
}
const startLine = executedMarker.line;
const endLine = endMarker.line;
if (startLine === endLine) {
return undefined;
}
let output = '';
let line;
for (let i = startLine; i < endLine; i++) {
line = buffer.getLine(i);
if (!line) {
continue;
}
output += line.translateToString(!line.isWrapped) + (line.isWrapped ? '' : '\n');
}
return output === '' ? undefined : output;
}
function getOutputMatchForCommand(executedMarker, endMarker, buffer, cols, outputMatcher) {
if (!executedMarker || !endMarker) {
return undefined;
}
const startLine = executedMarker.line;
const endLine = endMarker.line;
const matcher = outputMatcher.lineMatcher;
const linesToCheck = typeof matcher === 'string' ? 1 : outputMatcher.length || countNewLines(matcher);
const lines = [];
let match;
if (outputMatcher.anchor === 'bottom') {
for (let i = endLine - (outputMatcher.offset || 0); i >= startLine; i--) {
let wrappedLineStart = i;
const wrappedLineEnd = i;
while (wrappedLineStart >= startLine && buffer.getLine(wrappedLineStart)?.isWrapped) {
wrappedLineStart--;
}
i = wrappedLineStart;
lines.unshift(getXtermLineContent(buffer, wrappedLineStart, wrappedLineEnd, cols));
if (lines.length > linesToCheck) {
lines.pop();
}
if (!match) {
match = lines.join('\n').match(matcher);
}
}
} else {
for (let i = startLine + (outputMatcher.offset || 0); i < endLine; i++) {
const wrappedLineStart = i;
let wrappedLineEnd = i;
while (wrappedLineEnd + 1 < endLine && buffer.getLine(wrappedLineEnd + 1)?.isWrapped) {
wrappedLineEnd++;
}
i = wrappedLineEnd;
lines.push(getXtermLineContent(buffer, wrappedLineStart, wrappedLineEnd, cols));
if (lines.length === linesToCheck) {
lines.shift();
}
if (!match) {
match = lines.join('\n').match(matcher);
}
}
}
return match ? { regexMatch: match, outputLines: lines } : undefined;
}
export function getLinesForCommand(buffer, command, cols, outputMatcher) {
if (!outputMatcher) {
return undefined;
}
const executedMarker = command.executedMarker;
const endMarker = command.endMarker;
if (!executedMarker || !endMarker) {
throw new Error('No marker for command');
}
const startLine = executedMarker.line;
const endLine = endMarker.line;
const linesToCheck = outputMatcher.length;
const lines = [];
if (outputMatcher.anchor === 'bottom') {
for (let i = endLine - (outputMatcher.offset || 0); i >= startLine; i--) {
let wrappedLineStart = i;
const wrappedLineEnd = i;
while (wrappedLineStart >= startLine && buffer.getLine(wrappedLineStart)?.isWrapped) {
wrappedLineStart--;
}
i = wrappedLineStart;
lines.unshift(getXtermLineContent(buffer, wrappedLineStart, wrappedLineEnd, cols));
if (lines.length > linesToCheck) {
lines.pop();
}
}
} else {
for (let i = startLine + (outputMatcher.offset || 0); i < endLine; i++) {
const wrappedLineStart = i;
let wrappedLineEnd = i;
while (wrappedLineEnd + 1 < endLine && buffer.getLine(wrappedLineEnd + 1)?.isWrapped) {
wrappedLineEnd++;
}
i = wrappedLineEnd;
lines.push(getXtermLineContent(buffer, wrappedLineStart, wrappedLineEnd, cols));
if (lines.length === linesToCheck) {
lines.shift();
}
}
}
return lines;
}
function getXtermLineContent(buffer, lineStart, lineEnd, cols) {
// Cap the maximum number of lines generated to prevent potential performance problems. This is
// more of a sanity check as the wrapped line should already be trimmed down at this point.
const maxLineLength = Math.max((2048 / cols) * 2);
lineEnd = Math.min(lineEnd, lineStart + maxLineLength);
let content = '';
for (let i = lineStart; i <= lineEnd; i++) {
// Make sure only 0 to cols are considered as resizing when windows mode is enabled will
// retain buffer data outside of the terminal width as reflow is disabled.
const line = buffer.getLine(i);
if (line) {
content += line.translateToString(true, 0, cols);
}
}
return content;
}
function countNewLines(regex) {
if (!regex.multiline) {
return 1;
}
const source = regex.source;
let count = 1;
let i = source.indexOf('\\n');
while (i !== -1) {
count++;
i = source.indexOf('\\n', i + 1);
}
return count;
}