|
|
@@ -1,13 +1,24 @@
|
|
|
import { EventEmitter } from "events"
|
|
|
import stripAnsi from "strip-ansi"
|
|
|
import * as vscode from "vscode"
|
|
|
+import { inspect } from "util"
|
|
|
+
|
|
|
+import { ExitCodeDetails } from "./TerminalManager"
|
|
|
+import { TerminalInfo, TerminalRegistry } from "./TerminalRegistry"
|
|
|
|
|
|
export interface TerminalProcessEvents {
|
|
|
line: [line: string]
|
|
|
continue: []
|
|
|
- completed: []
|
|
|
+ completed: [output?: string]
|
|
|
error: [error: Error]
|
|
|
no_shell_integration: []
|
|
|
+ /**
|
|
|
+ * Emitted when a shell execution completes
|
|
|
+ * @param id The terminal ID
|
|
|
+ * @param exitDetails Contains exit code and signal information if process was terminated by signal
|
|
|
+ */
|
|
|
+ shell_execution_complete: [id: number, exitDetails: ExitCodeDetails]
|
|
|
+ stream_available: [id: number, stream: AsyncIterable<string>]
|
|
|
}
|
|
|
|
|
|
// how long to wait after a process outputs anything before we consider it "cool" again
|
|
|
@@ -17,104 +28,99 @@ const PROCESS_HOT_TIMEOUT_COMPILING = 15_000
|
|
|
export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
|
|
|
waitForShellIntegration: boolean = true
|
|
|
private isListening: boolean = true
|
|
|
- private buffer: string = ""
|
|
|
+ private terminalInfo: TerminalInfo | undefined
|
|
|
+ private lastEmitTime_ms: number = 0
|
|
|
private fullOutput: string = ""
|
|
|
private lastRetrievedIndex: number = 0
|
|
|
isHot: boolean = false
|
|
|
private hotTimer: NodeJS.Timeout | null = null
|
|
|
|
|
|
- // constructor() {
|
|
|
- // super()
|
|
|
-
|
|
|
async run(terminal: vscode.Terminal, command: string) {
|
|
|
if (terminal.shellIntegration && terminal.shellIntegration.executeCommand) {
|
|
|
- const execution = terminal.shellIntegration.executeCommand(command)
|
|
|
- const stream = execution.read()
|
|
|
- // todo: need to handle errors
|
|
|
- let isFirstChunk = true
|
|
|
- let didOutputNonCommand = false
|
|
|
- let didEmitEmptyLine = false
|
|
|
- for await (let data of stream) {
|
|
|
- // 1. Process chunk and remove artifacts
|
|
|
- if (isFirstChunk) {
|
|
|
- /*
|
|
|
- The first chunk we get from this stream needs to be processed to be more human readable, ie remove vscode's custom escape sequences and identifiers, removing duplicate first char bug, etc.
|
|
|
- */
|
|
|
-
|
|
|
- // bug where sometimes the command output makes its way into vscode shell integration metadata
|
|
|
- /*
|
|
|
- ]633 is a custom sequence number used by VSCode shell integration:
|
|
|
- - OSC 633 ; A ST - Mark prompt start
|
|
|
- - OSC 633 ; B ST - Mark prompt end
|
|
|
- - OSC 633 ; C ST - Mark pre-execution (start of command output)
|
|
|
- - OSC 633 ; D [; <exitcode>] ST - Mark execution finished with optional exit code
|
|
|
- - OSC 633 ; E ; <commandline> [; <nonce>] ST - Explicitly set command line with optional nonce
|
|
|
- */
|
|
|
- // if you print this data you might see something like "eecho hello worldo hello world;5ba85d14-e92a-40c4-b2fd-71525581eeb0]633;C" but this is actually just a bunch of escape sequences, ignore up to the first ;C
|
|
|
- /* ddateb15026-6a64-40db-b21f-2a621a9830f0]633;CTue Sep 17 06:37:04 EDT 2024 % ]633;D;0]633;P;Cwd=/Users/saoud/Repositories/test */
|
|
|
- // Gets output between ]633;C (command start) and ]633;D (command end)
|
|
|
- const outputBetweenSequences = this.removeLastLineArtifacts(
|
|
|
- data.match(/\]633;C([\s\S]*?)\]633;D/)?.[1] || "",
|
|
|
- ).trim()
|
|
|
-
|
|
|
- // Once we've retrieved any potential output between sequences, we can remove everything up to end of the last sequence
|
|
|
- // https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
|
|
|
- const vscodeSequenceRegex = /\x1b\]633;.[^\x07]*\x07/g
|
|
|
- const lastMatch = [...data.matchAll(vscodeSequenceRegex)].pop()
|
|
|
- if (lastMatch && lastMatch.index !== undefined) {
|
|
|
- data = data.slice(lastMatch.index + lastMatch[0].length)
|
|
|
- }
|
|
|
- // Place output back after removing vscode sequences
|
|
|
- if (outputBetweenSequences) {
|
|
|
- data = outputBetweenSequences + "\n" + data
|
|
|
- }
|
|
|
- // remove ansi
|
|
|
- data = stripAnsi(data)
|
|
|
- // Split data by newlines
|
|
|
- let lines = data ? data.split("\n") : []
|
|
|
- // Remove non-human readable characters from the first line
|
|
|
- if (lines.length > 0) {
|
|
|
- lines[0] = lines[0].replace(/[^\x20-\x7E]/g, "")
|
|
|
- }
|
|
|
- // Check if first two characters are the same, if so remove the first character
|
|
|
- if (lines.length > 0 && lines[0].length >= 2 && lines[0][0] === lines[0][1]) {
|
|
|
- lines[0] = lines[0].slice(1)
|
|
|
- }
|
|
|
- // Remove everything up to the first alphanumeric character for first two lines
|
|
|
- if (lines.length > 0) {
|
|
|
- lines[0] = lines[0].replace(/^[^a-zA-Z0-9]*/, "")
|
|
|
+ // Get terminal info to access stream
|
|
|
+ const terminalInfo = TerminalRegistry.getTerminalInfoByTerminal(terminal)
|
|
|
+ if (!terminalInfo) {
|
|
|
+ console.error("[TerminalProcess] Terminal not found in registry")
|
|
|
+ this.emit("no_shell_integration")
|
|
|
+ this.emit("completed")
|
|
|
+ this.emit("continue")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // When executeCommand() is called, onDidStartTerminalShellExecution will fire in TerminalManager
|
|
|
+ // which creates a new stream via execution.read() and emits 'stream_available'
|
|
|
+ const streamAvailable = new Promise<AsyncIterable<string>>((resolve) => {
|
|
|
+ this.once("stream_available", (id: number, stream: AsyncIterable<string>) => {
|
|
|
+ if (id === terminalInfo.id) {
|
|
|
+ resolve(stream)
|
|
|
}
|
|
|
- if (lines.length > 1) {
|
|
|
- lines[1] = lines[1].replace(/^[^a-zA-Z0-9]*/, "")
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ // Create promise that resolves when shell execution completes for this terminal
|
|
|
+ const shellExecutionComplete = new Promise<ExitCodeDetails>((resolve) => {
|
|
|
+ this.once("shell_execution_complete", (id: number, exitDetails: ExitCodeDetails) => {
|
|
|
+ if (id === terminalInfo.id) {
|
|
|
+ resolve(exitDetails)
|
|
|
}
|
|
|
- // Join lines back
|
|
|
- data = lines.join("\n")
|
|
|
- isFirstChunk = false
|
|
|
- } else {
|
|
|
- data = stripAnsi(data)
|
|
|
- }
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ // getUnretrievedOutput needs to know if streamClosed, so store this for later
|
|
|
+ this.terminalInfo = terminalInfo
|
|
|
+
|
|
|
+ // Execute command
|
|
|
+ terminal.shellIntegration.executeCommand(command)
|
|
|
+ this.isHot = true
|
|
|
+
|
|
|
+ // Wait for stream to be available
|
|
|
+ const stream = await streamAvailable
|
|
|
|
|
|
- // first few chunks could be the command being echoed back, so we must ignore
|
|
|
- // note this means that 'echo' commands wont work
|
|
|
- if (!didOutputNonCommand) {
|
|
|
- const lines = data.split("\n")
|
|
|
- for (let i = 0; i < lines.length; i++) {
|
|
|
- if (command.includes(lines[i].trim())) {
|
|
|
- lines.splice(i, 1)
|
|
|
- i-- // Adjust index after removal
|
|
|
- } else {
|
|
|
- didOutputNonCommand = true
|
|
|
- break
|
|
|
- }
|
|
|
+ let preOutput = ""
|
|
|
+ let commandOutputStarted = false
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Extract clean output from raw accumulated output. FYI:
|
|
|
+ * ]633 is a custom sequence number used by VSCode shell integration:
|
|
|
+ * - OSC 633 ; A ST - Mark prompt start
|
|
|
+ * - OSC 633 ; B ST - Mark prompt end
|
|
|
+ * - OSC 633 ; C ST - Mark pre-execution (start of command output)
|
|
|
+ * - OSC 633 ; D [; <exitcode>] ST - Mark execution finished with optional exit code
|
|
|
+ * - OSC 633 ; E ; <commandline> [; <nonce>] ST - Explicitly set command line with optional nonce
|
|
|
+ */
|
|
|
+
|
|
|
+ // Process stream data
|
|
|
+ for await (let data of stream) {
|
|
|
+ // Check for command output start marker
|
|
|
+ if (!commandOutputStarted) {
|
|
|
+ preOutput += data
|
|
|
+ const match = this.matchAfterVsceStartMarkers(data)
|
|
|
+ if (match !== undefined) {
|
|
|
+ commandOutputStarted = true
|
|
|
+ data = match
|
|
|
+ this.fullOutput = "" // Reset fullOutput when command actually starts
|
|
|
+ } else {
|
|
|
+ continue
|
|
|
}
|
|
|
- data = lines.join("\n")
|
|
|
}
|
|
|
|
|
|
- // FIXME: right now it seems that data chunks returned to us from the shell integration stream contains random commas, which from what I can tell is not the expected behavior. There has to be a better solution here than just removing all commas.
|
|
|
- data = data.replace(/,/g, "")
|
|
|
+ // Command output started, accumulate data without filtering.
|
|
|
+ // notice to future programmers: do not add escape sequence
|
|
|
+ // filtering here: fullOutput cannot change in length (see getUnretrievedOutput),
|
|
|
+ // and chunks may not be complete so you cannot rely on detecting or removing escape sequences mid-stream.
|
|
|
+ this.fullOutput += data
|
|
|
|
|
|
- // 2. Set isHot depending on the command
|
|
|
- // Set to hot to stall API requests until terminal is cool again
|
|
|
+ // For non-immediately returning commands we want to show loading spinner
|
|
|
+ // right away but this wouldnt happen until it emits a line break, so
|
|
|
+ // as soon as we get any output we emit to let webview know to show spinner
|
|
|
+ const now = Date.now()
|
|
|
+ if (this.isListening && (now - this.lastEmitTime_ms > 100 || this.lastEmitTime_ms === 0)) {
|
|
|
+ this.emitRemainingBufferIfListening()
|
|
|
+ this.lastEmitTime_ms = now
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. Set isHot depending on the command.
|
|
|
+ // This stalls API requests until terminal is cool again.
|
|
|
this.isHot = true
|
|
|
if (this.hotTimer) {
|
|
|
clearTimeout(this.hotTimer)
|
|
|
@@ -144,21 +150,37 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
|
|
|
},
|
|
|
isCompiling ? PROCESS_HOT_TIMEOUT_COMPILING : PROCESS_HOT_TIMEOUT_NORMAL,
|
|
|
)
|
|
|
+ }
|
|
|
|
|
|
- // For non-immediately returning commands we want to show loading spinner right away but this wouldnt happen until it emits a line break, so as soon as we get any output we emit "" to let webview know to show spinner
|
|
|
- if (!didEmitEmptyLine && !this.fullOutput && data) {
|
|
|
- this.emit("line", "") // empty line to indicate start of command output stream
|
|
|
- didEmitEmptyLine = true
|
|
|
- }
|
|
|
+ // Set streamClosed immediately after stream ends
|
|
|
+ if (this.terminalInfo) {
|
|
|
+ this.terminalInfo.streamClosed = true
|
|
|
+ }
|
|
|
|
|
|
- this.fullOutput += data
|
|
|
- if (this.isListening) {
|
|
|
- this.emitIfEol(data)
|
|
|
- this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
|
|
|
- }
|
|
|
+ // Wait for shell execution to complete and handle exit details
|
|
|
+ const exitDetails = await shellExecutionComplete
|
|
|
+ this.isHot = false
|
|
|
+
|
|
|
+ if (commandOutputStarted) {
|
|
|
+ // Emit any remaining output before completing
|
|
|
+ this.emitRemainingBufferIfListening()
|
|
|
+ } else {
|
|
|
+ console.error(
|
|
|
+ "[Terminal Process] VSCE output start escape sequence (]633;C or ]133;C) not received! VSCE Bug? preOutput: " +
|
|
|
+ inspect(preOutput, { colors: false, breakLength: Infinity }),
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ // console.debug("[Terminal Process] raw output: " + inspect(output, { colors: false, breakLength: Infinity }))
|
|
|
+
|
|
|
+ // fullOutput begins after C marker so we only need to trim off D marker
|
|
|
+ // (if D exists, see VSCode bug# 237208):
|
|
|
+ const match = this.matchBeforeVsceEndMarkers(this.fullOutput)
|
|
|
+ if (match !== undefined) {
|
|
|
+ this.fullOutput = match
|
|
|
}
|
|
|
|
|
|
- this.emitRemainingBufferIfListening()
|
|
|
+ // console.debug(`[Terminal Process] processed output via ${matchSource}: ` + inspect(output, { colors: false, breakLength: Infinity }))
|
|
|
|
|
|
// for now we don't want this delaying requests since we don't send diagnostics automatically anymore (previous: "even though the command is finished, we still want to consider it 'hot' in case so that api request stalls to let diagnostics catch up")
|
|
|
if (this.hotTimer) {
|
|
|
@@ -166,7 +188,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
|
|
|
}
|
|
|
this.isHot = false
|
|
|
|
|
|
- this.emit("completed")
|
|
|
+ this.emit("completed", this.removeEscapeSequences(this.fullOutput))
|
|
|
this.emit("continue")
|
|
|
} else {
|
|
|
terminal.sendText(command, true)
|
|
|
@@ -182,29 +204,12 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Inspired by https://github.com/sindresorhus/execa/blob/main/lib/transform/split.js
|
|
|
- private emitIfEol(chunk: string) {
|
|
|
- this.buffer += chunk
|
|
|
- let lineEndIndex: number
|
|
|
- while ((lineEndIndex = this.buffer.indexOf("\n")) !== -1) {
|
|
|
- let line = this.buffer.slice(0, lineEndIndex).trimEnd() // removes trailing \r
|
|
|
- // Remove \r if present (for Windows-style line endings)
|
|
|
- // if (line.endsWith("\r")) {
|
|
|
- // line = line.slice(0, -1)
|
|
|
- // }
|
|
|
- this.emit("line", line)
|
|
|
- this.buffer = this.buffer.slice(lineEndIndex + 1)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
private emitRemainingBufferIfListening() {
|
|
|
- if (this.buffer && this.isListening) {
|
|
|
- const remainingBuffer = this.removeLastLineArtifacts(this.buffer)
|
|
|
- if (remainingBuffer) {
|
|
|
+ if (this.isListening) {
|
|
|
+ const remainingBuffer = this.getUnretrievedOutput()
|
|
|
+ if (remainingBuffer !== "") {
|
|
|
this.emit("line", remainingBuffer)
|
|
|
}
|
|
|
- this.buffer = ""
|
|
|
- this.lastRetrievedIndex = this.fullOutput.length
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -215,22 +220,180 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
|
|
|
this.emit("continue")
|
|
|
}
|
|
|
|
|
|
+ // Returns complete lines with their carriage returns.
|
|
|
+ // The final line may lack a carriage return if the program didn't send one.
|
|
|
getUnretrievedOutput(): string {
|
|
|
- const unretrieved = this.fullOutput.slice(this.lastRetrievedIndex)
|
|
|
- this.lastRetrievedIndex = this.fullOutput.length
|
|
|
- return this.removeLastLineArtifacts(unretrieved)
|
|
|
+ // Get raw unretrieved output
|
|
|
+ let outputToProcess = this.fullOutput.slice(this.lastRetrievedIndex)
|
|
|
+
|
|
|
+ // Check for VSCE command end markers
|
|
|
+ const index633 = outputToProcess.indexOf("\x1b]633;D")
|
|
|
+ const index133 = outputToProcess.indexOf("\x1b]133;D")
|
|
|
+ let endIndex = -1
|
|
|
+
|
|
|
+ if (index633 !== -1 && index133 !== -1) {
|
|
|
+ endIndex = Math.min(index633, index133)
|
|
|
+ } else if (index633 !== -1) {
|
|
|
+ endIndex = index633
|
|
|
+ } else if (index133 !== -1) {
|
|
|
+ endIndex = index133
|
|
|
+ }
|
|
|
+
|
|
|
+ // If no end markers were found yet (possibly due to VSCode bug#237208):
|
|
|
+ // For active streams: return only complete lines (up to last \n).
|
|
|
+ // For closed streams: return all remaining content.
|
|
|
+ if (endIndex === -1) {
|
|
|
+ if (!this.terminalInfo?.streamClosed) {
|
|
|
+ // Stream still running - only process complete lines
|
|
|
+ endIndex = outputToProcess.lastIndexOf("\n")
|
|
|
+ if (endIndex === -1) {
|
|
|
+ // No complete lines
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+
|
|
|
+ // Include carriage return
|
|
|
+ endIndex++
|
|
|
+ } else {
|
|
|
+ // Stream closed - process all remaining output
|
|
|
+ endIndex = outputToProcess.length
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update index and slice output
|
|
|
+ this.lastRetrievedIndex += endIndex
|
|
|
+ outputToProcess = outputToProcess.slice(0, endIndex)
|
|
|
+
|
|
|
+ // Clean and return output
|
|
|
+ return this.removeEscapeSequences(outputToProcess)
|
|
|
}
|
|
|
|
|
|
- // some processing to remove artifacts like '%' at the end of the buffer (it seems that since vsode uses % at the beginning of newlines in terminal, it makes its way into the stream)
|
|
|
- // This modification will remove '%', '$', '#', or '>' followed by optional whitespace
|
|
|
- removeLastLineArtifacts(output: string) {
|
|
|
- const lines = output.trimEnd().split("\n")
|
|
|
- if (lines.length > 0) {
|
|
|
- const lastLine = lines[lines.length - 1]
|
|
|
- // Remove prompt characters and trailing whitespace from the last line
|
|
|
- lines[lines.length - 1] = lastLine.replace(/[%$#>]\s*$/, "")
|
|
|
+ private stringIndexMatch(
|
|
|
+ data: string,
|
|
|
+ prefix?: string,
|
|
|
+ suffix?: string,
|
|
|
+ bell: string = "\x07",
|
|
|
+ ): string | undefined {
|
|
|
+ let startIndex: number
|
|
|
+ let endIndex: number
|
|
|
+ let prefixLength: number
|
|
|
+
|
|
|
+ if (prefix === undefined) {
|
|
|
+ startIndex = 0
|
|
|
+ prefixLength = 0
|
|
|
+ } else {
|
|
|
+ startIndex = data.indexOf(prefix)
|
|
|
+ if (startIndex === -1) {
|
|
|
+ return undefined
|
|
|
+ }
|
|
|
+ if (bell.length > 0) {
|
|
|
+ // Find the bell character after the prefix
|
|
|
+ const bellIndex = data.indexOf(bell, startIndex + prefix.length)
|
|
|
+ if (bellIndex === -1) {
|
|
|
+ return undefined
|
|
|
+ }
|
|
|
+
|
|
|
+ const distanceToBell = bellIndex - startIndex
|
|
|
+
|
|
|
+ prefixLength = distanceToBell + bell.length
|
|
|
+ } else {
|
|
|
+ prefixLength = prefix.length
|
|
|
+ }
|
|
|
}
|
|
|
- return lines.join("\n").trimEnd()
|
|
|
+
|
|
|
+ const contentStart = startIndex + prefixLength
|
|
|
+
|
|
|
+ if (suffix === undefined) {
|
|
|
+ // When suffix is undefined, match to end
|
|
|
+ endIndex = data.length
|
|
|
+ } else {
|
|
|
+ endIndex = data.indexOf(suffix, contentStart)
|
|
|
+ if (endIndex === -1) {
|
|
|
+ return undefined
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return data.slice(contentStart, endIndex)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Removes ANSI escape sequences and VSCode-specific terminal control codes from output.
|
|
|
+ // While stripAnsi handles most ANSI codes, VSCode's shell integration adds custom
|
|
|
+ // escape sequences (OSC 633) that need special handling. These sequences control
|
|
|
+ // terminal features like marking command start/end and setting prompts.
|
|
|
+ //
|
|
|
+ // This method could be extended to handle other escape sequences, but any additions
|
|
|
+ // should be carefully considered to ensure they only remove control codes and don't
|
|
|
+ // alter the actual content or behavior of the output stream.
|
|
|
+ private removeEscapeSequences(str: string): string {
|
|
|
+ return stripAnsi(str.replace(/\x1b\]633;[^\x07]+\x07/gs, "").replace(/\x1b\]133;[^\x07]+\x07/gs, ""))
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Helper function to match VSCode shell integration start markers (C).
|
|
|
+ * Looks for content after ]633;C or ]133;C markers.
|
|
|
+ * If both exist, takes the content after the last marker found.
|
|
|
+ */
|
|
|
+ private matchAfterVsceStartMarkers(data: string): string | undefined {
|
|
|
+ return this.matchVsceMarkers(data, "\x1b]633;C", "\x1b]133;C", undefined, undefined)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Helper function to match VSCode shell integration end markers (D).
|
|
|
+ * Looks for content before ]633;D or ]133;D markers.
|
|
|
+ * If both exist, takes the content before the first marker found.
|
|
|
+ */
|
|
|
+ private matchBeforeVsceEndMarkers(data: string): string | undefined {
|
|
|
+ return this.matchVsceMarkers(data, undefined, undefined, "\x1b]633;D", "\x1b]133;D")
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Handles VSCode shell integration markers for command output:
|
|
|
+ *
|
|
|
+ * For C (Command Start):
|
|
|
+ * - Looks for content after ]633;C or ]133;C markers
|
|
|
+ * - These markers indicate the start of command output
|
|
|
+ * - If both exist, takes the content after the last marker found
|
|
|
+ * - This ensures we get the actual command output after any shell integration prefixes
|
|
|
+ *
|
|
|
+ * For D (Command End):
|
|
|
+ * - Looks for content before ]633;D or ]133;D markers
|
|
|
+ * - These markers indicate command completion
|
|
|
+ * - If both exist, takes the content before the first marker found
|
|
|
+ * - This ensures we don't include shell integration suffixes in the output
|
|
|
+ *
|
|
|
+ * In both cases, checks 633 first since it's more commonly used in VSCode shell integration
|
|
|
+ *
|
|
|
+ * @param data The string to search for markers in
|
|
|
+ * @param prefix633 The 633 marker to match after (for C markers)
|
|
|
+ * @param prefix133 The 133 marker to match after (for C markers)
|
|
|
+ * @param suffix633 The 633 marker to match before (for D markers)
|
|
|
+ * @param suffix133 The 133 marker to match before (for D markers)
|
|
|
+ * @returns The content between/after markers, or undefined if no markers found
|
|
|
+ *
|
|
|
+ * Note: Always makes exactly 2 calls to stringIndexMatch regardless of match results.
|
|
|
+ * Using string indexOf matching is ~500x faster than regular expressions, so even
|
|
|
+ * matching twice is still very efficient comparatively.
|
|
|
+ */
|
|
|
+ private matchVsceMarkers(
|
|
|
+ data: string,
|
|
|
+ prefix633: string | undefined,
|
|
|
+ prefix133: string | undefined,
|
|
|
+ suffix633: string | undefined,
|
|
|
+ suffix133: string | undefined,
|
|
|
+ ): string | undefined {
|
|
|
+ // Support both VSCode shell integration markers (633 and 133)
|
|
|
+ // Check 633 first since it's more commonly used in VSCode shell integration
|
|
|
+ let match133: string | undefined
|
|
|
+ const match633 = this.stringIndexMatch(data, prefix633, suffix633)
|
|
|
+
|
|
|
+ // Must check explicitly for undefined because stringIndexMatch can return empty strings
|
|
|
+ // that are valid matches (e.g., when a marker exists but has no content between markers)
|
|
|
+ if (match633 !== undefined) {
|
|
|
+ match133 = this.stringIndexMatch(match633, prefix133, suffix133)
|
|
|
+ } else {
|
|
|
+ match133 = this.stringIndexMatch(data, prefix133, suffix133)
|
|
|
+ }
|
|
|
+
|
|
|
+ return match133 !== undefined ? match133 : match633
|
|
|
}
|
|
|
}
|
|
|
|