Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src/integrations/misc/__tests__/extract-text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
truncateOutput,
applyRunLengthEncoding,
processCarriageReturns,
processBackspaces,
} from "../extract-text"

describe("addLineNumbers", () => {
Expand Down Expand Up @@ -229,6 +230,69 @@ describe("truncateOutput", () => {
expect(truncateOutput("single line", 10)).toBe("single line")
})

describe("processBackspaces", () => {
it("should handle basic backspace deletion", () => {
const input = "abc\b\bxy"
const expected = "axy"
expect(processBackspaces(input)).toBe(expected)
})

it("should handle backspaces at start of input", () => {
const input = "\b\babc"
const expected = "abc"
expect(processBackspaces(input)).toBe(expected)
})

it("should handle backspaces with newlines", () => {
const input = "abc\b\n123\b\b"
const expected = "ab\n1"
expect(processBackspaces(input)).toBe(expected)
})

it("should handle consecutive backspaces", () => {
const input = "abcdef\b\b\b\bxy"
const expected = "abxy"
expect(processBackspaces(input)).toBe(expected)
})

it("should handle backspaces at end of input", () => {
const input = "abc\b\b"
const expected = "a"
expect(processBackspaces(input)).toBe(expected)
})

it("should handle mixed backspaces and content", () => {
const input = "abc\bx\byz\b\b123"
const expected = "ab123"
expect(processBackspaces(input)).toBe(expected)
})

it("should handle multiple groups of consecutive backspaces", () => {
const input = "abc\b\bdef\b\b\bghi\b\b\b\bjkl"
const expected = "jkl"
expect(processBackspaces(input)).toBe(expected)
})

it("should handle backspaces with empty content between them", () => {
const input = "abc\b\b\b\b\b\bdef"
const expected = "def"
expect(processBackspaces(input)).toBe(expected)
})

it("should handle complex mixed content with backspaces", () => {
const input = "Loading[\b\b\b\b\b\b\b\bProgress[\b\b\b\b\b\b\b\b\bStatus: \b\b\b\b\b\b\b\bDone!"
// Technically terminal displays "Done!s: [" but we assume \b is destructive as an optimization
const expected = "Done!"
expect(processBackspaces(input)).toBe(expected)
})

it("should handle backspaces with special characters", () => {
const input = "abc😀\b\bdef🎉\b\b\bghi"
const expected = "abcdeghi"
expect(processBackspaces(input)).toBe(expected)
})
})

it("handles windows-style line endings", () => {
// Create content with windows line endings
const lines = Array.from({ length: 15 }, (_, i) => `line${i + 1}`)
Expand Down
47 changes: 47 additions & 0 deletions src/integrations/misc/extract-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,53 @@ export function processCarriageReturns(input: string): string {
return output
}

/**
* Processes backspace characters (\b) in terminal output using index operations.
* Uses indexOf to efficiently locate and handle backspaces.
*
* Technically terminal only moves the cursor and overwrites in-place,
* but we assume \b is destructive as an optimization which is acceptable
* for all progress spinner cases and most terminal output cases.
*
* @param input The terminal output to process
* @returns The processed output with backspaces handled
*/
export function processBackspaces(input: string): string {
let output = ""
let pos = 0
let bsPos = input.indexOf("\b")

while (bsPos !== -1) {
// Fast path: exclude char before backspace
output += input.substring(pos, bsPos - 1)

// Move past backspace
pos = bsPos + 1

// Count consecutive backspaces
let count = 0
while (input[pos] === "\b") {
count++
pos++
}

// Trim output mathematically for consecutive backspaces
if (count > 0 && output.length > 0) {
output = output.substring(0, Math.max(0, output.length - count))
}

// Find next backspace
bsPos = input.indexOf("\b", pos)
}

// Add remaining content
if (pos < input.length) {
output += input.substring(pos)
}

return output
}

/**
* Helper function to process a single line with carriage returns.
* Handles the overwrite logic for a line that contains one or more carriage returns (\r).
Expand Down
8 changes: 5 additions & 3 deletions src/integrations/terminal/Terminal.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as vscode from "vscode"
import pWaitFor from "p-wait-for"
import { ExitCodeDetails, mergePromise, TerminalProcess, TerminalProcessResultPromise } from "./TerminalProcess"
import { truncateOutput, applyRunLengthEncoding, processCarriageReturns } from "../misc/extract-text"
import { truncateOutput, applyRunLengthEncoding, processCarriageReturns, processBackspaces } from "../misc/extract-text"
// Import TerminalRegistry here to avoid circular dependencies
const { TerminalRegistry } = require("./TerminalRegistry")

Expand Down Expand Up @@ -296,9 +296,11 @@ export class Terminal {
public static compressTerminalOutput(input: string, lineLimit: number): string {
// Apply carriage return processing if the feature is enabled
let processedInput = input
if (Terminal.compressProgressBar && input.includes("\r")) {
processedInput = processCarriageReturns(input)
if (Terminal.compressProgressBar) {
processedInput = processCarriageReturns(processedInput)
processedInput = processBackspaces(processedInput)
}

return truncateOutput(applyRunLengthEncoding(processedInput), lineLimit)
}

Expand Down