Skip to content

Commit e1d3e77

Browse files
committed
Support wide and emoji chars in prompt input model
1 parent c2a14c8 commit e1d3e77

File tree

3 files changed

+188
-59
lines changed

3 files changed

+188
-59
lines changed

src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import { Emitter, type Event } from 'vs/base/common/event';
77
import { Disposable } from 'vs/base/common/lifecycle';
88
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
99
import type { ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities';
10+
import { debounce } from 'vs/base/common/decorators';
1011

1112
// Importing types is safe in any layer
1213
// eslint-disable-next-line local/code-import-patterns
13-
import type { Terminal, IMarker } from '@xterm/headless';
14-
import { debounce } from 'vs/base/common/decorators';
14+
import type { Terminal, IMarker, IBufferLine, IBuffer } from '@xterm/headless';
1515

1616
const enum PromptInputState {
1717
Unknown,
@@ -75,6 +75,8 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
7575
this._state = PromptInputState.Input;
7676
this._commandStartMarker = command.marker;
7777
this._commandStartX = this._xterm.buffer.active.cursorX;
78+
this._value = '';
79+
this._cursorIndex = 0;
7880
this._onDidStartInput.fire();
7981
}
8082

@@ -102,49 +104,53 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
102104
}
103105

104106
const commandStartY = this._commandStartMarker?.line;
105-
if (!commandStartY) {
107+
if (commandStartY === undefined) {
106108
return;
107109
}
108110

109111
const buffer = this._xterm.buffer.active;
110-
const commandLine = buffer.getLine(commandStartY)?.translateToString(true);
111-
if (!commandLine) {
112+
let line = buffer.getLine(commandStartY);
113+
const commandLine = line?.translateToString(true, this._commandStartX);
114+
if (!commandLine || !line) {
112115
this._logService.trace(`PromptInputModel#_sync: no line`);
113116
return;
114117
}
115118

116119
// Command start line
117-
this._value = commandLine.substring(this._commandStartX);
118-
this._cursorIndex = Math.max(buffer.cursorX - this._commandStartX, 0);
120+
this._value = commandLine;
121+
122+
// Get cursor index
123+
const absoluteCursorY = buffer.baseY + buffer.cursorY;
124+
this._cursorIndex = absoluteCursorY === commandStartY ? this._getRelativeCursorIndex(this._commandStartX, buffer, line) : commandLine.length + 1;
119125

120-
// IDEA: Reinforce knowledge of prompt to avoid incorrect commandStart
121-
// IDEA: Detect ghost text based on SGR and cursor
126+
// IDEA: Detect ghost text based on SGR and cursor. This might work by checking for italic
127+
// or dim only to avoid false positives from shells that do immediate coloring.
128+
// IDEA: Detect line continuation if it's not set
122129

123130
// From command start line to cursor line
124-
const absoluteCursorY = buffer.baseY + buffer.cursorY;
125131
for (let y = commandStartY + 1; y <= absoluteCursorY; y++) {
126-
let lineText = buffer.getLine(y)?.translateToString(true);
127-
if (lineText) {
132+
line = buffer.getLine(y);
133+
let lineText = line?.translateToString(true);
134+
if (lineText && line) {
128135
// Verify continuation prompt if we have it, if this line doesn't have it then the
129136
// user likely just pressed enter
130137
if (this._continuationPrompt === undefined || this._lineContainsContinuationPrompt(lineText)) {
131138
lineText = this._trimContinuationPrompt(lineText);
132139
this._value += `\n${lineText}`;
133-
if (y === absoluteCursorY) {
134-
// TODO: Wide/emoji length support
135-
this._cursorIndex = Math.max(this._value.length - lineText.length - (this._continuationPrompt?.length ?? 0) + buffer.cursorX, 0);
136-
}
140+
this._cursorIndex += (absoluteCursorY === y
141+
? this._getRelativeCursorIndex(this._getContinuationPromptCellWidth(line, lineText), buffer, line)
142+
: lineText.length + 1);
137143
} else {
138-
this._cursorIndex = this._value.length;
139144
break;
140145
}
141146
}
142147
}
143148

144149
// Below cursor line
145150
for (let y = absoluteCursorY + 1; y < buffer.baseY + this._xterm.rows; y++) {
146-
const lineText = buffer.getLine(y)?.translateToString(true);
147-
if (lineText) {
151+
line = buffer.getLine(y);
152+
const lineText = line?.translateToString(true);
153+
if (lineText && line) {
148154
if (this._continuationPrompt === undefined || this._lineContainsContinuationPrompt(lineText)) {
149155
this._value += `\n${this._trimContinuationPrompt(lineText)}`;
150156
} else {
@@ -161,7 +167,6 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
161167
}
162168

163169
private _trimContinuationPrompt(lineText: string): string {
164-
// TODO: Detect line continuation if it's not set
165170
if (this._lineContainsContinuationPrompt(lineText)) {
166171
lineText = lineText.substring(this._continuationPrompt!.length);
167172
}
@@ -171,4 +176,20 @@ export class PromptInputModel extends Disposable implements IPromptInputModel {
171176
private _lineContainsContinuationPrompt(lineText: string): boolean {
172177
return !!(this._continuationPrompt && lineText.startsWith(this._continuationPrompt));
173178
}
179+
180+
private _getContinuationPromptCellWidth(line: IBufferLine, lineText: string): number {
181+
if (!this._continuationPrompt || !lineText.startsWith(this._continuationPrompt)) {
182+
return 0;
183+
}
184+
let buffer: string = '';
185+
let x = 0;
186+
while (buffer !== this._continuationPrompt) {
187+
buffer += line.getCell(x++)!.getChars();
188+
}
189+
return x;
190+
}
191+
192+
private _getRelativeCursorIndex(startCellX: number, buffer: IBuffer, line: IBufferLine): number {
193+
return line?.translateToString(true, startCellX, buffer.cursorX).length ?? 0;
194+
}
174195
}

0 commit comments

Comments
 (0)