-
Notifications
You must be signed in to change notification settings - Fork 37k
Fix terminal tab prompt input breaking when backticks are included #272425
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix terminal tab prompt input breaking when backticks are included #272425
Conversation
479b5dd to
6a9c04d
Compare
Abrifq
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OoT/pet peeve:
Prompt input sometimes gets an extra space appended.
Need to check _doSync() for what could cause it:
vscode/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts
Lines 273 to 449 in 55505bf
| private _doSync() { | |
| if (this._state !== PromptInputState.Input) { | |
| return; | |
| } | |
| let commandStartY = this._commandStartMarker?.line; | |
| if (commandStartY === undefined) { | |
| return; | |
| } | |
| const buffer = this._xterm.buffer.active; | |
| let line = buffer.getLine(commandStartY); | |
| const absoluteCursorY = buffer.baseY + buffer.cursorY; | |
| let cursorIndex: number | undefined; | |
| let commandLine = line?.translateToString(true, this._commandStartX); | |
| if (this._shellType === PosixShellType.Fish && (!line || !commandLine)) { | |
| commandStartY += 1; | |
| line = buffer.getLine(commandStartY); | |
| if (line) { | |
| commandLine = line.translateToString(true); | |
| cursorIndex = absoluteCursorY === commandStartY ? buffer.cursorX : commandLine?.trimEnd().length; | |
| } | |
| } | |
| if (line === undefined || commandLine === undefined) { | |
| this._logService.trace(`PromptInputModel#_sync: no line`); | |
| return; | |
| } | |
| let value = commandLine; | |
| let ghostTextIndex = -1; | |
| if (cursorIndex === undefined) { | |
| if (absoluteCursorY === commandStartY) { | |
| cursorIndex = Math.min(this._getRelativeCursorIndex(this._commandStartX, buffer, line), commandLine.length); | |
| } else { | |
| cursorIndex = commandLine.trimEnd().length; | |
| } | |
| } | |
| // From command start line to cursor line | |
| for (let y = commandStartY + 1; y <= absoluteCursorY; y++) { | |
| const nextLine = buffer.getLine(y); | |
| const lineText = nextLine?.translateToString(true); | |
| if (lineText && nextLine) { | |
| // Check if the line wrapped without a new line (continuation) or | |
| // we're on the last line and the continuation prompt is not present, so we need to add the value | |
| if (nextLine.isWrapped || (absoluteCursorY === y && this._continuationPrompt && !this._lineContainsContinuationPrompt(lineText))) { | |
| value += `${lineText}`; | |
| const relativeCursorIndex = this._getRelativeCursorIndex(0, buffer, nextLine); | |
| if (absoluteCursorY === y) { | |
| cursorIndex += relativeCursorIndex; | |
| } else { | |
| cursorIndex += lineText.length; | |
| } | |
| } else if (this._shellType === PosixShellType.Fish) { | |
| if (value.endsWith('\\')) { | |
| // Trim off the trailing backslash | |
| value = value.substring(0, value.length - 1); | |
| value += `${lineText.trim()}`; | |
| cursorIndex += lineText.trim().length - 1; | |
| } else { | |
| if (/^ {6,}/.test(lineText)) { | |
| // Was likely a new line | |
| value += `\n${lineText.trim()}`; | |
| cursorIndex += lineText.trim().length + 1; | |
| } else { | |
| value += lineText; | |
| cursorIndex += lineText.length; | |
| } | |
| } | |
| } | |
| // Verify continuation prompt if we have it, if this line doesn't have it then the | |
| // user likely just pressed enter. | |
| else if (this._continuationPrompt === undefined || this._lineContainsContinuationPrompt(lineText)) { | |
| const trimmedLineText = this._trimContinuationPrompt(lineText); | |
| value += `\n${trimmedLineText}`; | |
| if (absoluteCursorY === y) { | |
| const continuationCellWidth = this._getContinuationPromptCellWidth(nextLine, lineText); | |
| const relativeCursorIndex = this._getRelativeCursorIndex(continuationCellWidth, buffer, nextLine); | |
| cursorIndex += relativeCursorIndex + 1; | |
| } else { | |
| cursorIndex += trimmedLineText.length + 1; | |
| } | |
| } | |
| } | |
| } | |
| // Below cursor line | |
| for (let y = absoluteCursorY + 1; y < buffer.baseY + this._xterm.rows; y++) { | |
| const belowCursorLine = buffer.getLine(y); | |
| const lineText = belowCursorLine?.translateToString(true); | |
| if (lineText && belowCursorLine) { | |
| if (this._shellType === PosixShellType.Fish) { | |
| value += `${lineText}`; | |
| } else if (this._continuationPrompt === undefined || this._lineContainsContinuationPrompt(lineText)) { | |
| value += `\n${this._trimContinuationPrompt(lineText)}`; | |
| } else { | |
| value += lineText; | |
| } | |
| } else { | |
| break; | |
| } | |
| } | |
| if (this._logService.getLevel() === LogLevel.Trace) { | |
| this._logService.trace(`PromptInputModel#_sync: ${this.getCombinedString()}`); | |
| } | |
| // Adjust trailing whitespace | |
| { | |
| let trailingWhitespace = this._value.length - this._value.trimEnd().length; | |
| // Handle backspace key | |
| if (this._lastUserInput === '\x7F') { | |
| this._lastUserInput = ''; | |
| if (cursorIndex === this._cursorIndex - 1) { | |
| // If trailing whitespace is being increased by removing a non-whitespace character | |
| if (this._value.trimEnd().length > value.trimEnd().length && value.trimEnd().length <= cursorIndex) { | |
| trailingWhitespace = Math.max((this._value.length - 1) - value.trimEnd().length, 0); | |
| } | |
| // Standard case; subtract from trailing whitespace | |
| else { | |
| trailingWhitespace = Math.max(trailingWhitespace - 1, 0); | |
| } | |
| } | |
| } | |
| // Handle delete key | |
| if (this._lastUserInput === '\x1b[3~') { | |
| this._lastUserInput = ''; | |
| if (cursorIndex === this._cursorIndex) { | |
| trailingWhitespace = Math.max(trailingWhitespace - 1, 0); | |
| } | |
| } | |
| const valueLines = value.split('\n'); | |
| const isMultiLine = valueLines.length > 1; | |
| const valueEndTrimmed = value.trimEnd(); | |
| if (!isMultiLine) { | |
| // Adjust trimmed whitespace value based on cursor position | |
| if (valueEndTrimmed.length < value.length) { | |
| // Handle space key | |
| if (this._lastUserInput === ' ') { | |
| this._lastUserInput = ''; | |
| if (cursorIndex > valueEndTrimmed.length && cursorIndex > this._cursorIndex) { | |
| trailingWhitespace++; | |
| } | |
| } | |
| trailingWhitespace = Math.max(cursorIndex - valueEndTrimmed.length, trailingWhitespace, 0); | |
| } | |
| // Handle case where a non-space character is inserted in the middle of trailing whitespace | |
| const charBeforeCursor = cursorIndex === 0 ? '' : value[cursorIndex - 1]; | |
| if (trailingWhitespace > 0 && cursorIndex === this._cursorIndex + 1 && this._lastUserInput !== '' && charBeforeCursor !== ' ') { | |
| trailingWhitespace = this._value.length - this._cursorIndex; | |
| } | |
| } | |
| if (isMultiLine) { | |
| valueLines[valueLines.length - 1] = valueLines.at(-1)?.trimEnd() ?? ''; | |
| const continuationOffset = (valueLines.length - 1) * (this._continuationPrompt?.length ?? 0); | |
| trailingWhitespace = Math.max(0, cursorIndex - value.length - continuationOffset); | |
| } | |
| value = valueLines.map(e => e.trimEnd()).join('\n') + ' '.repeat(trailingWhitespace); | |
| } | |
| ghostTextIndex = this._scanForGhostText(buffer, line, cursorIndex); | |
| if (this._value !== value || this._cursorIndex !== cursorIndex || this._ghostTextIndex !== ghostTextIndex) { | |
| this._value = value; | |
| this._cursorIndex = cursorIndex; | |
| this._ghostTextIndex = ghostTextIndex; | |
| this._onDidChangeInput.fire(this._createStateObject()); | |
| } | |
| } |
6a9c04d to
7b0877c
Compare
|
Hi, I have tested my PR within codespaces' VNC, it does work now. I had to enable vscode/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts Lines 46 to 49 in 5e797a9
There might be a desync causing this, needs more investigation. As I started that comment, it is out of topic. |
ab88aa6 to
5e797a9
Compare
backtick wrapping with <code>wrapping</code>
5e797a9 to
fc5842d
Compare
|
Thanks for the PR. Can you pls provide a screenshot of this working for you in testing? |
fc5842d to
3a92ee4
Compare
This comment was marked as outdated.
This comment was marked as outdated.
3a92ee4 to
4e717e0
Compare
Update:Prefixing backslashes to backticks makes them show up again.
I'll also check if just applying db999f4 onto main works. |
644016f to
3ccfea3
Compare
I have tested (https://regexr.com/8hu9f) and it should be good for now. Testing with this input: echo `date`; echo </code>[escaped link?](https://youtu.be/dQw4w9WgXcQ "asdasdasd")Main Branch (124710c)
My patch (3ccfea3)
|
It does not. Until someone confirms that markdown rendering inside If I didn't misread the code when I first opened this issue/PR, the changes I made should run once per keystroke, so it should -hopefully- have only negligible performance impacts. |
3ccfea3 to
9030ea6
Compare
9030ea6 to
d30c10e
Compare
|
I guess feature release from Github Universe could have created some urgent backlog but is there anything blocking this PR from being reviewed? Edit: Well, the Monthly Endgame has started. Oh well. |
|
Hello @meganrogge, while waiting for this PR to get reviewed, should I update the base branch to test against newer base or should I leave as is? I'm asking this in general, it's not mentioned in contributing guide. |
|
|
||
| const shellProcessString = getShellProcessTooltip(instance, !!showDetailed); | ||
| const content = new MarkdownString(instance.title + shellProcessString + statusString, { supportThemeIcons: true }); | ||
| const content = new MarkdownString(instance.title + shellProcessString + statusString, { supportThemeIcons: true, supportHtml: true }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to supportHtml here to fix this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Current markdown parsing doesn't allow for escaping backticks via a backslash, and to workaround that I have changed the backtick wrapping with <code> wrapping.
Code wrapping does not work unless supportHtml is enabled, though. Which is why I enabled it.
| const escapedPromptInput = combinedString | ||
| .replaceAll('<', '<').replaceAll('>', '>') //Prevent escaping from wrapping | ||
| .replaceAll(/\((.+?)(\|?(?: (?:.+?)?)?)\)/g, '(<$1>$2)') //Escape links as clickable links | ||
| .replaceAll(/([\[\]\(\)\-\*\!\#\`])/g, '\\$1'); //Comment most of the markdown elements to not render them inside | ||
| detailedAdditions.push(`Prompt input: <code>\n${escapedPromptInput}\n</code>`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't this just as simple as escaping inner backticks like this?
combinedString.replaceAll('`', '\\`')There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have tried it and it didn't work that time
( #272425 (comment) )
https://github.com/Abrifq/vscode/tree/Abrifq/alt-wrap-fix
But I will update the branch & try again and report here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current proposal seems far too complex for what this actually does imo, all it's doing is making debug information show a little nicer. Does this helper do the job?
vscode/src/vs/base/common/htmlContent.ts
Lines 153 to 156 in 1d4fe40
| export function escapeMarkdownSyntaxTokens(text: string): string { | |
| // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash | |
| return text.replace(/[\\`*_{}[\]()#+\-!~]/g, '\\$&'); // CodeQL [SM02383] Backslash is escaped in the character class | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this helper do the job?
vscode/src/vs/base/common/htmlContent.ts
Lines 153 to 156 in 1d4fe40
export function escapeMarkdownSyntaxTokens(text: string): string { // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash return text.replace(/[\\`*_{}[\]()#+\-!~]/g, '\\$&'); // CodeQL [SM02383] Backslash is escaped in the character class }
Yeah, it should replace line 115. Thanks!
The current proposal seems far too complex for what this actually does imo, all it's doing is making debug information show a little nicer.
I need to check if lines 112, 113 & 116 themselves are good enough to ONLY fix the wrapping, but I faintly recall they were not.
As for line 114, I have stated in meganrogge's review that it's basically optional and can be removed.
I'll see what I can do within my lunch break, and thanks for the helper.
backtick wrapping with <code>wrapping</code>
Tyriar
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@meganrogge simplified it, basically handling the edge case where backtick is included specially instead of adding HTML support/complicated regexes. Could you check it out?
|
@Abrifq let me know if you're able to break this one, but I think it's solid |
meganrogge
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works for me
Sorry, I just became available to check, I will update but this feels weird on UX.
|
|
There is an even simpler version, I will make a new PR on top of this one. @@ -109,15 +109,8 @@ export function refreshShellIntegrationInfoStatus(instance: ITerminalInstance) {
}
const combinedString = instance.capabilities.get(TerminalCapability.CommandDetection)?.promptInputModel.getCombinedString();
if (combinedString !== undefined) {
- if (combinedString.includes('`')) {
- detailedAdditions.push('Prompt input:' + [
- '```',
- combinedString, // No new lines so no need to escape ```
- '```',
- ].map(e => `\n ${e}`).join(''));
- } else {
- detailedAdditions.push(`Prompt input: \`${combinedString.replaceAll('`', '`')}\``);
- }
+ // Wrap with triple backticks so that single backticks can show up (command substitution in bash uses backticks, for example)
+ detailedAdditions.push(`Prompt input: \`\`\`${combinedString}\`\`\``);
}
const detailedAdditionsString = detailedAdditions.length > 0
? '\n\n' + detailedAdditions.map(e => `- ${e}`).join('\n') |
Well, that's the simplest, the cleaner solution would be either adding some if not all css properties of |
|
@Abrifq we definitely don't want to support HTML there. I also don't want to get into trying to change the markdown rendering. Again this is just debug info that's hidden by default, handling all edge cases perfectly isn't too important. |
…t-input-visual-fix Cleanup #272425





Closes #272416.
I have tested the changes manually via codespaces.