Не подтверждена Коммит 8d40850a создал по автору Daniel Imms's avatar Daniel Imms
Просмотр файлов

Move terminal suggest to prompt input model instead of manual tracking

владелец 4f619fff
......@@ -1241,9 +1241,4 @@ export interface ISuggestController {
selectNextPageSuggestion(): void;
acceptSelectedSuggestion(suggestion?: Pick<ISimpleSelectedSuggestion, 'item' | 'model'>): void;
hideSuggestWidget(): void;
/**
* Handle data written to the terminal outside of xterm.js which has no corresponding
* `Terminal.onData` event.
*/
handleNonXtermData(data: string): void;
}
......@@ -68,7 +68,7 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo
return;
}
if (this._terminalSuggestWidgetVisibleContextKey) {
this._addon.value = this._instantiationService.createInstance(SuggestAddon, this._terminalSuggestWidgetVisibleContextKey);
this._addon.value = this._instantiationService.createInstance(SuggestAddon, this._instance.capabilities, this._terminalSuggestWidgetVisibleContextKey);
xterm.loadAddon(this._addon.value);
this._addon.value.setPanel(dom.findParentWithClass(xterm.element!, 'panel')!);
this._addon.value.setScreen(xterm.element!.querySelector('.xterm-screen')!);
......@@ -77,9 +77,7 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo
this._instance.focus();
this._instance.sendText(text, false);
}));
this.add(this._instance.onDidSendText((text) => {
this._addon.value?.handleNonXtermData(text);
}));
this.add(this._instance.onDidSendText(() => this._addon.value?.hideSuggestWidget()));
}
}
}
......
......@@ -8,8 +8,8 @@ import { SimpleCompletionItem } from 'vs/workbench/services/suggest/browser/simp
import { LineContext, SimpleCompletionModel } from 'vs/workbench/services/suggest/browser/simpleCompletionModel';
import { ISimpleSelectedSuggestion, SimpleSuggestWidget } from 'vs/workbench/services/suggest/browser/simpleSuggestWidget';
import { Codicon } from 'vs/base/common/codicons';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { combinedDisposable, Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { ThemeIcon } from 'vs/base/common/themables';
import { editorSuggestWidgetSelectedBackground } from 'vs/editor/contrib/suggest/browser/suggestWidget';
import { IContextKey } from 'vs/platform/contextkey/common/contextkey';
......@@ -20,11 +20,9 @@ import { ISuggestController } from 'vs/workbench/contrib/terminal/browser/termin
import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys';
import type { ITerminalAddon, Terminal } from '@xterm/xterm';
import { getListStyles } from 'vs/platform/theme/browser/defaultStyles';
const enum ShellIntegrationOscPs {
// TODO: Pull from elsewhere
VSCode = 633
}
import { TerminalCapability, type ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities';
import type { IPromptInputModel, IPromptInputModelState } from 'vs/platform/terminal/common/capabilities/commandDetection/promptInputModel';
import { ShellIntegrationOscPs } from 'vs/platform/terminal/common/xterm/shellIntegrationAddon';
const enum VSCodeOscPt {
Completions = 'Completions',
......@@ -73,14 +71,22 @@ const pwshTypeToIconMap: { [type: string]: ThemeIcon | undefined } = {
export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggestController {
private _terminal?: Terminal;
private _promptInputModel?: IPromptInputModel;
private readonly _promptInputModelSubscriptions = this._register(new MutableDisposable());
private _mostRecentPromptInputState?: IPromptInputModelState;
private _initialPromptInputState?: IPromptInputModelState;
private _currentPromptInputState?: IPromptInputModelState;
private _panel?: HTMLElement;
private _screen?: HTMLElement;
private _suggestWidget?: SimpleSuggestWidget;
private _enableWidget: boolean = true;
// TODO: Remove these in favor of prompt input state
private _leadingLineContent?: string;
private _additionalInput?: string;
private _cursorIndexDelta: number = 0;
private _inputQueue?: string[];
private readonly _onBell = this._register(new Emitter<void>());
readonly onBell = this._onBell.event;
......@@ -88,10 +94,29 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
readonly onAcceptedCompletion = this._onAcceptedCompletion.event;
constructor(
private readonly _capabilities: ITerminalCapabilityStore,
private readonly _terminalSuggestWidgetVisibleContextKey: IContextKey<boolean>,
@IInstantiationService private readonly _instantiationService: IInstantiationService
) {
super();
this._register(Event.runAndSubscribe(Event.any(
this._capabilities.onDidAddCapabilityType,
this._capabilities.onDidRemoveCapabilityType
), () => {
const commandDetection = this._capabilities.get(TerminalCapability.CommandDetection);
if (commandDetection) {
if (this._promptInputModel !== commandDetection.promptInputModel) {
this._promptInputModel = commandDetection.promptInputModel;
this._promptInputModelSubscriptions.value = combinedDisposable(
this._promptInputModel.onDidChangeInput(e => this._sync(e)),
this._promptInputModel.onDidFinishInput(() => this.hideSuggestWidget()),
);
}
} else {
this._promptInputModel = undefined;
}
}));
}
activate(xterm: Terminal): void {
......@@ -99,9 +124,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
this._register(xterm.parser.registerOscHandler(ShellIntegrationOscPs.VSCode, data => {
return this._handleVSCodeSequence(data);
}));
this._register(xterm.onData(e => {
this._handleTerminalInput(e);
}));
}
setPanel(panel: HTMLElement): void {
......@@ -112,6 +134,45 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
this._screen = screen;
}
private _sync(promptInputState: IPromptInputModelState): void {
this._mostRecentPromptInputState = promptInputState;
if (!this._promptInputModel || !this._terminal || !this._suggestWidget || !this._initialPromptInputState) {
return;
}
this._currentPromptInputState = promptInputState;
if (this._terminalSuggestWidgetVisibleContextKey.get()) {
const inputBeforeCursor = this._currentPromptInputState.value.substring(0, this._currentPromptInputState.cursorIndex);
this._cursorIndexDelta = this._currentPromptInputState.cursorIndex - this._initialPromptInputState.cursorIndex;
this._suggestWidget.setLineContext(new LineContext(inputBeforeCursor, this._cursorIndexDelta));
}
// Hide and clear model if there are no more items
if (!this._suggestWidget.hasCompletions()) {
this.hideSuggestWidget();
// TODO: Don't request every time; refine completions
// this._onAcceptedCompletion.fire('\x1b[24~e');
return;
}
// TODO: Expose on xterm.js
const dimensions = this._getTerminalDimensions();
if (!dimensions.width || !dimensions.height) {
return;
}
// TODO: What do frozen and auto do?
const xtermBox = this._screen!.getBoundingClientRect();
const panelBox = this._panel!.offsetParent!.getBoundingClientRect();
this._suggestWidget.showSuggestions(0, false, false, {
left: (xtermBox.left - panelBox.left) + this._terminal.buffer.active.cursorX * dimensions.width,
top: (xtermBox.top - panelBox.top) + this._terminal.buffer.active.cursorY * dimensions.height,
height: dimensions.height
});
}
private _handleVSCodeSequence(data: string): boolean {
if (!this._terminal) {
return false;
......@@ -277,7 +338,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
}
private _handleCompletionModel(model: SimpleCompletionModel): void {
if (model.items.length === 0 || !this._terminal?.element) {
if (model.items.length === 0 || !this._terminal?.element || !this._promptInputModel) {
return;
}
if (model.items.length === 1) {
......@@ -288,7 +349,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
return;
}
const suggestWidget = this._ensureSuggestWidget(this._terminal);
this._additionalInput = undefined;
const dimensions = this._getTerminalDimensions();
if (!dimensions.width || !dimensions.height) {
return;
......@@ -296,21 +356,17 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
// TODO: What do frozen and auto do?
const xtermBox = this._screen!.getBoundingClientRect();
const panelBox = this._panel!.offsetParent!.getBoundingClientRect();
this._initialPromptInputState = {
value: this._promptInputModel.value,
cursorIndex: this._promptInputModel.cursorIndex,
ghostTextIndex: this._promptInputModel.ghostTextIndex
};
suggestWidget.setCompletionModel(model);
suggestWidget.showSuggestions(0, false, false, {
left: (xtermBox.left - panelBox.left) + this._terminal.buffer.active.cursorX * dimensions.width,
top: (xtermBox.top - panelBox.top) + this._terminal.buffer.active.cursorY * dimensions.height,
height: dimensions.height
});
// Flush the input queue if any characters were typed after a trigger character
if (this._inputQueue) {
const inputQueue = this._inputQueue;
this._inputQueue = undefined;
for (const data of inputQueue) {
this._handleTerminalInput(data);
}
}
}
private _ensureSuggestWidget(terminal: Terminal): SimpleSuggestWidget {
......@@ -328,7 +384,14 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
}));
this._suggestWidget.onDidSelect(async e => this.acceptSelectedSuggestion(e));
this._suggestWidget.onDidHide(() => this._terminalSuggestWidgetVisibleContextKey.set(false));
this._suggestWidget.onDidShow(() => this._terminalSuggestWidgetVisibleContextKey.set(true));
this._suggestWidget.onDidShow(() => {
this._initialPromptInputState = {
value: this._promptInputModel!.value,
cursorIndex: this._promptInputModel!.cursorIndex,
ghostTextIndex: this._promptInputModel!.ghostTextIndex
};
this._terminalSuggestWidgetVisibleContextKey.set(true);
});
}
return this._suggestWidget;
}
......@@ -353,137 +416,39 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
if (!suggestion) {
suggestion = this._suggestWidget?.getFocusedItem();
}
if (suggestion && this._leadingLineContent) {
this._suggestWidget?.hide();
// Send the completion
this._onAcceptedCompletion.fire([
// Disable suggestions
'\x1b[24~y',
// Right arrow to the end of the additional input
'\x1b[C'.repeat(Math.max((this._additionalInput?.length ?? 0) - this._cursorIndexDelta, 0)),
// Backspace to remove additional input
'\x7F'.repeat(this._additionalInput?.length ?? 0),
// Backspace to remove the replacement
'\x7F'.repeat(suggestion.model.replacementLength),
// Write the completion
suggestion.item.completion.label,
// Enable suggestions
'\x1b[24~z',
].join(''));
const initialPromptInputState = this._initialPromptInputState ?? this._mostRecentPromptInputState;
if (!suggestion || !initialPromptInputState) {
return;
}
}
hideSuggestWidget(): void {
this._suggestWidget?.hide();
}
handleNonXtermData(data: string): void {
this._handleTerminalInput(data, true);
const currentPromptInputState = this._currentPromptInputState ?? initialPromptInputState;
const additionalInput = currentPromptInputState.value.substring(initialPromptInputState.cursorIndex, currentPromptInputState.cursorIndex);
// We could start from a common prefix to reduce the number of characters we need to send
const initialInput = initialPromptInputState.value.substring(0, initialPromptInputState.cursorIndex);
const lastSpaceIndex = initialInput.lastIndexOf(' ');
const finalCompletion = suggestion.item.completion.label.substring(initialPromptInputState.cursorIndex - (lastSpaceIndex === -1 ? 0 : lastSpaceIndex + 1));
// Send the completion
this._onAcceptedCompletion.fire([
// Disable suggestions
'\x1b[24~y',
// Backspace to remove all additional input
'\x7F'.repeat(additionalInput.length),
// Write the completion
finalCompletion,
// Enable suggestions
'\x1b[24~z',
].join(''));
this.hideSuggestWidget();
}
private _handleTerminalInput(data: string, nonUserInput?: boolean): void {
if (!this._terminal || !this._enableWidget || !this._terminalSuggestWidgetVisibleContextKey.get()) {
// HACK: Buffer any input to be evaluated when the completions come in, this is needed
// because conpty may "render" the completion request after input characters that
// actually come after it. This can happen when typing quickly after a trigger
// character, especially on a freshly launched session.
if (data === '-') {
this._inputQueue = [];
} else {
this._inputQueue?.push(data);
}
return;
}
let handled = false;
let handledCursorDelta = 0;
// Backspace
if (data === '\x7f') {
if (this._additionalInput && this._additionalInput.length > 0 && this._cursorIndexDelta > 0) {
handled = true;
this._additionalInput = this._additionalInput.substring(0, this._cursorIndexDelta - 1) + this._additionalInput.substring(this._cursorIndexDelta);
this._cursorIndexDelta--;
handledCursorDelta--;
}
}
// Delete
if (data === '\x1b[3~') {
if (this._additionalInput && this._additionalInput.length > 0 && this._cursorIndexDelta < this._additionalInput.length - 1) {
handled = true;
this._additionalInput = this._additionalInput.substring(0, this._cursorIndexDelta) + this._additionalInput.substring(this._cursorIndexDelta + 1);
}
}
// Left
else if (data === '\x1b[D') {
// If left goes beyond where the completion was requested, hide
if (this._cursorIndexDelta > 0) {
handled = true;
this._cursorIndexDelta--;
handledCursorDelta--;
}
}
// Right
else if (data === '\x1b[C') {
// If right requests beyond where the completion was requested (potentially accepting a shell completion), hide
if (this._additionalInput?.length !== this._cursorIndexDelta) {
handled = true;
this._cursorIndexDelta++;
handledCursorDelta++;
}
}
// Other CSI sequence (ignore)
else if (data.match(/^\x1b\[.+[a-z@\^`{\|}~]$/i)) {
handled = true;
}
if (data.match(/^[a-z0-9]$/i)) {
// TODO: There is a race here where the completions may come through after new character presses because of conpty's rendering!
handled = true;
if (this._additionalInput === undefined) {
this._additionalInput = '';
}
this._additionalInput += data;
this._cursorIndexDelta++;
handledCursorDelta++;
}
if (handled) {
// typed -> moved cursor RIGHT -> update UI
if (this._terminalSuggestWidgetVisibleContextKey.get()) {
this._suggestWidget?.setLineContext(new LineContext(this._leadingLineContent! + (this._additionalInput ?? ''), this._additionalInput?.length ?? 0));
}
// Hide and clear model if there are no more items
if (!this._suggestWidget?.hasCompletions() || nonUserInput) {
this._additionalInput = undefined;
this.hideSuggestWidget();
// TODO: Don't request every time; refine completions
// this._onAcceptedCompletion.fire('\x1b[24~e');
return;
}
// TODO: Expose on xterm.js
const dimensions = this._getTerminalDimensions();
if (!dimensions.width || !dimensions.height) {
return;
}
// TODO: What do frozen and auto do?
const xtermBox = this._screen!.getBoundingClientRect();
const panelBox = this._panel!.offsetParent!.getBoundingClientRect();
this._suggestWidget?.showSuggestions(0, false, false, {
left: (xtermBox.left - panelBox.left) + (this._terminal.buffer.active.cursorX + handledCursorDelta) * dimensions.width,
top: (xtermBox.top - panelBox.top) + this._terminal.buffer.active.cursorY * dimensions.height,
height: dimensions.height
});
} else {
this._additionalInput = undefined;
this.hideSuggestWidget();
// TODO: Don't request every time; refine completions
// this._onAcceptedCompletion.fire('\x1b[24~e');
}
hideSuggestWidget(): void {
this._initialPromptInputState = undefined;
this._currentPromptInputState = undefined;
this._suggestWidget?.hide();
}
}
......
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать