diff --git a/src/integrations/misc/__tests__/extract-text.test.ts b/src/integrations/misc/__tests__/extract-text.test.ts index c6bca0a88d9..04b06cfa836 100644 --- a/src/integrations/misc/__tests__/extract-text.test.ts +++ b/src/integrations/misc/__tests__/extract-text.test.ts @@ -5,6 +5,7 @@ import { truncateOutput, applyRunLengthEncoding, processCarriageReturns, + processBackspaces, } from "../extract-text" describe("addLineNumbers", () => { @@ -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}`) diff --git a/src/integrations/misc/extract-text.ts b/src/integrations/misc/extract-text.ts index 5bbbbf85140..596923d93e4 100644 --- a/src/integrations/misc/extract-text.ts +++ b/src/integrations/misc/extract-text.ts @@ -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). diff --git a/src/integrations/terminal/Terminal.ts b/src/integrations/terminal/Terminal.ts index 5f896c265d5..66eb6c68e17 100644 --- a/src/integrations/terminal/Terminal.ts +++ b/src/integrations/terminal/Terminal.ts @@ -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") @@ -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) }