feat: compress terminal output with backspace characters (#2907)

Follow-up to #2562 adding support for backspace character compression.
Optimizes terminal output by handling backspace characters similar to
carriage returns, improving readability of progress spinners and other
terminal output that uses backspace for animation.

- Added processBackspaces function using efficient indexOf approach
- Added comprehensive test suite for backspace handling
- Integrated with terminal output compression pipeline

Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
Co-authored-by: Eric Wheeler <roo-code@z.ewheeler.org>
This commit is contained in:
KJ7LNW 2025-04-24 16:28:09 -07:00 committed by GitHub
parent 23aa3b636b
commit 5c2511e355
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 116 additions and 3 deletions

View file

@ -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}`)

View file

@ -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).

View file

@ -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)
}