|
|
|
@@ -16,16 +16,19 @@ import { IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
|
|
|
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
|
|
|
|
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
|
|
|
|
import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry';
|
|
|
|
|
import { ISuggestController } from 'vs/workbench/contrib/terminal/browser/terminal';
|
|
|
|
|
import { ISuggestController, ITerminalConfigurationService } from 'vs/workbench/contrib/terminal/browser/terminal';
|
|
|
|
|
import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys';
|
|
|
|
|
import type { ITerminalAddon, Terminal } from '@xterm/xterm';
|
|
|
|
|
|
|
|
|
|
import { getListStyles } from 'vs/platform/theme/browser/defaultStyles';
|
|
|
|
|
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';
|
|
|
|
|
import type { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private';
|
|
|
|
|
|
|
|
|
|
const enum VSCodeOscPt {
|
|
|
|
|
Completions = 'Completions',
|
|
|
|
|
CompletionsPwshCommands = 'CompletionsPwshCommands',
|
|
|
|
|
CompletionsBash = 'CompletionsBash',
|
|
|
|
|
CompletionsBashFirstWord = 'CompletionsBashFirstWord'
|
|
|
|
|
}
|
|
|
|
@@ -96,7 +99,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly _capabilities: ITerminalCapabilityStore,
|
|
|
|
|
private readonly _terminalSuggestWidgetVisibleContextKey: IContextKey<boolean>,
|
|
|
|
|
@IInstantiationService private readonly _instantiationService: IInstantiationService
|
|
|
|
|
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
|
|
|
|
@ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService
|
|
|
|
|
) {
|
|
|
|
|
super();
|
|
|
|
|
|
|
|
|
@@ -134,7 +138,35 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
|
|
|
|
|
this._screen = screen;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _requestCompletions(): void {
|
|
|
|
|
// TODO: Debounce? Prevent this flooding the channel
|
|
|
|
|
// if (this._terminal.
|
|
|
|
|
this._onAcceptedCompletion.fire('\x1b[24~e');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _sync(promptInputState: IPromptInputModelState): void {
|
|
|
|
|
if (!this._terminalConfigurationService.config.shellIntegration?.suggestEnabled) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!this._terminalSuggestWidgetVisibleContextKey.get()) {
|
|
|
|
|
// If input has been added
|
|
|
|
|
if (!this._mostRecentPromptInputState || promptInputState.cursorIndex > this._mostRecentPromptInputState.cursorIndex) {
|
|
|
|
|
// Quick suggestions
|
|
|
|
|
if (promptInputState.cursorIndex === 1 || promptInputState.value.substring(0, promptInputState.cursorIndex).match(/\s[^\s]$/)) {
|
|
|
|
|
// TODO: Allow the user to configure terminal quickSuggestions
|
|
|
|
|
this._requestCompletions();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Trigger characters
|
|
|
|
|
const lastChar = promptInputState.value.at(promptInputState.cursorIndex - 1);
|
|
|
|
|
if (lastChar?.match(/[\\\/\-]/)) {
|
|
|
|
|
// TODO: Allow the user to configure terminal suggestOnTriggerCharacters
|
|
|
|
|
this._requestCompletions();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._mostRecentPromptInputState = promptInputState;
|
|
|
|
|
if (!this._promptInputModel || !this._terminal || !this._suggestWidget || !this._initialPromptInputState) {
|
|
|
|
|
return;
|
|
|
|
@@ -142,6 +174,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
|
|
|
|
|
|
|
|
|
|
this._currentPromptInputState = promptInputState;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Hide the widget if the cursor moves to the left of the initial position as the
|
|
|
|
|
// completions are no longer valid
|
|
|
|
|
if (this._currentPromptInputState.cursorIndex < this._initialPromptInputState.cursorIndex) {
|
|
|
|
@@ -152,19 +185,15 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
|
|
|
|
|
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;
|
|
|
|
@@ -191,6 +220,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
|
|
|
|
|
case VSCodeOscPt.Completions:
|
|
|
|
|
this._handleCompletionsSequence(this._terminal, data, command, args);
|
|
|
|
|
return true;
|
|
|
|
|
case VSCodeOscPt.CompletionsPwshCommands:
|
|
|
|
|
this._handleCompletionsPwshCommandsSequence(this._terminal, data, command, args);
|
|
|
|
|
case VSCodeOscPt.CompletionsBash:
|
|
|
|
|
this._handleCompletionsBashSequence(this._terminal, data, command, args);
|
|
|
|
|
return true;
|
|
|
|
@@ -204,18 +235,15 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
|
|
|
|
|
|
|
|
|
|
private _handleCompletionsSequence(terminal: Terminal, data: string, command: string, args: string[]): void {
|
|
|
|
|
// Nothing to handle if the terminal is not attached
|
|
|
|
|
if (!terminal.element || !this._enableWidget) {
|
|
|
|
|
if (!terminal.element || !this._enableWidget || !this._promptInputModel) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const replacementIndex = parseInt(args[0]);
|
|
|
|
|
const replacementLength = parseInt(args[1]);
|
|
|
|
|
if (!args[3]) {
|
|
|
|
|
this._onBell.fire();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let replacementIndex = 0;
|
|
|
|
|
let replacementLength = this._promptInputModel.cursorIndex;
|
|
|
|
|
|
|
|
|
|
let completionList: IPwshCompletion[] | IPwshCompletion = JSON.parse(data.slice(command.length + args[0].length + args[1].length + args[2].length + 4/*semi-colons*/));
|
|
|
|
|
const payload = data.slice(command.length + args[0].length + args[1].length + args[2].length + 4/*semi-colons*/);
|
|
|
|
|
let completionList: IPwshCompletion[] | IPwshCompletion = args.length === 0 || payload.length === 0 ? [] : JSON.parse(payload);
|
|
|
|
|
if (!Array.isArray(completionList)) {
|
|
|
|
|
completionList = [completionList];
|
|
|
|
|
}
|
|
|
|
@@ -227,19 +255,45 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this._leadingLineContent = completions[0].completion.label.slice(0, replacementLength);
|
|
|
|
|
this._leadingLineContent = this._promptInputModel.value.substring(0, this._promptInputModel.cursorIndex);
|
|
|
|
|
|
|
|
|
|
// If there's no space it means this is a command, add cached commands list to completions
|
|
|
|
|
if (!this._leadingLineContent.trim().includes(' ')) {
|
|
|
|
|
completions.push(...this._cachedPwshCommands);
|
|
|
|
|
} else {
|
|
|
|
|
replacementIndex = parseInt(args[0]);
|
|
|
|
|
replacementLength = parseInt(args[1]);
|
|
|
|
|
this._leadingLineContent = completions[0]?.completion.label.slice(0, replacementLength) ?? '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._cursorIndexDelta = 0;
|
|
|
|
|
const model = new SimpleCompletionModel(completions, new LineContext(this._leadingLineContent, replacementIndex), replacementIndex, replacementLength);
|
|
|
|
|
if (completions.length === 1) {
|
|
|
|
|
const insertText = completions[0].completion.label.substring(replacementLength);
|
|
|
|
|
if (insertText.length === 0) {
|
|
|
|
|
this._onBell.fire();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this._handleCompletionModel(model);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: These aren't persisted across reloads
|
|
|
|
|
private _cachedPwshCommands: Set<SimpleCompletionItem> = new Set();
|
|
|
|
|
private _handleCompletionsPwshCommandsSequence(terminal: Terminal, data: string, command: string, args: string[]): void {
|
|
|
|
|
const type = args[0];
|
|
|
|
|
let completionList: IPwshCompletion[] | IPwshCompletion = JSON.parse(data.slice(command.length + type.length + 2/*semi-colons*/));
|
|
|
|
|
if (!Array.isArray(completionList)) {
|
|
|
|
|
completionList = [completionList];
|
|
|
|
|
}
|
|
|
|
|
const set = this._cachedPwshCommands;
|
|
|
|
|
set.clear();
|
|
|
|
|
|
|
|
|
|
const completions = completionList.map((e: any) => {
|
|
|
|
|
return new SimpleCompletionItem({
|
|
|
|
|
label: e.CompletionText,
|
|
|
|
|
icon: pwshTypeToIconMap[e.ResultType],
|
|
|
|
|
detail: e.ToolTip
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
for (const c of completions) {
|
|
|
|
|
set.add(c);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: These aren't persisted across reloads
|
|
|
|
|
// TODO: Allow triggering anywhere in the first word based on the cached completions
|
|
|
|
|
private _cachedBashAliases: Set<SimpleCompletionItem> = new Set();
|
|
|
|
@@ -338,9 +392,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _getTerminalDimensions(): { width: number; height: number } {
|
|
|
|
|
const cssCellDims = (this._terminal as any as { _core: IXtermCore })._core._renderService.dimensions.css.cell;
|
|
|
|
|
return {
|
|
|
|
|
width: (this._terminal as any)._core._renderService.dimensions.css.cell.width,
|
|
|
|
|
height: (this._terminal as any)._core._renderService.dimensions.css.cell.height,
|
|
|
|
|
width: cssCellDims.width,
|
|
|
|
|
height: cssCellDims.height,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -384,14 +439,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
|
|
|
|
|
}));
|
|
|
|
|
this._register(this._suggestWidget.onDidSelect(async e => this.acceptSelectedSuggestion(e)));
|
|
|
|
|
this._register(this._suggestWidget.onDidHide(() => this._terminalSuggestWidgetVisibleContextKey.set(false)));
|
|
|
|
|
this._register(this._suggestWidget.onDidShow(() => {
|
|
|
|
|
this._initialPromptInputState = {
|
|
|
|
|
value: this._promptInputModel!.value,
|
|
|
|
|
cursorIndex: this._promptInputModel!.cursorIndex,
|
|
|
|
|
ghostTextIndex: this._promptInputModel!.ghostTextIndex
|
|
|
|
|
};
|
|
|
|
|
this._terminalSuggestWidgetVisibleContextKey.set(true);
|
|
|
|
|
}));
|
|
|
|
|
this._register(this._suggestWidget.onDidShow(() => this._terminalSuggestWidgetVisibleContextKey.set(true)));
|
|
|
|
|
}
|
|
|
|
|
return this._suggestWidget;
|
|
|
|
|
}
|
|
|
|
@@ -439,8 +487,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
|
|
|
|
|
|
|
|
|
|
// Send the completion
|
|
|
|
|
this._onAcceptedCompletion.fire([
|
|
|
|
|
// Disable suggestions
|
|
|
|
|
'\x1b[24~y',
|
|
|
|
|
// Backspace to remove all additional input
|
|
|
|
|
'\x7F'.repeat(additionalInput.length),
|
|
|
|
|
// Backspace to remove left side of completion
|
|
|
|
@@ -449,8 +495,6 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
|
|
|
|
|
finalCompletionLeftSide,
|
|
|
|
|
// Write the completion
|
|
|
|
|
finalCompletionRightSide,
|
|
|
|
|
// Enable suggestions
|
|
|
|
|
'\x1b[24~z',
|
|
|
|
|
].join(''));
|
|
|
|
|
|
|
|
|
|
this.hideSuggestWidget();
|
|
|
|
|