Browse Source

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 <[email protected]>
Co-authored-by: Eric Wheeler <[email protected]>
KJ7LNW 10 months ago
parent
commit
5c2511e355

+ 64 - 0
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}`)

+ 47 - 0
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).

+ 5 - 3
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)
 	}