Merge pull request #233776 from microsoft/merogge/terminal-suggest-wip

add initial set of terminal completions for zsh, bash, and fish
This commit is contained in:
Daniel Imms 2024-11-19 14:50:16 -08:00 коммит произвёл GitHub
Родитель 6c8a8e24d2 1d167f62c2
Коммит e57cc506b2
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
31 изменённых файлов: 2469 добавлений и 72 удалений

18
extensions/terminal-suggest/.vscode/launch.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,18 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/client/out/**/*.js"],
"preLaunchTask": "npm"
}
]
}

11
extensions/terminal-suggest/.vscode/tasks.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,11 @@
{
"version": "2.0.0",
"command": "npm",
"type": "shell",
"presentation": {
"reveal": "silent",
},
"args": ["run", "compile"],
"isBackground": true,
"problemMatcher": "$tsc-watch"
}

Просмотреть файл

@ -0,0 +1,7 @@
src/**
out/**
tsconfig.json
.vscode/**
extension.webpack.config.js
extension-browser.webpack.config.js
package-lock.json

Просмотреть файл

@ -0,0 +1,7 @@
# Terminal Suggestions
**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. To enable the completions from this extension, set `terminal.integrated.suggest.enabled` and `terminal.integrated.suggest.enableExtensionCompletions` to `true`.
## Features
Provides terminal suggestions for zsh, bash, and fish.

Просмотреть файл

@ -0,0 +1,32 @@
THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
For Microsoft terminal-suggest
This file is based on or incorporates material from the projects listed below ("Third Party OSS"). The original copyright
notice and the license under which Microsoft received such Third Party OSS, are set forth below. Such licenses and notice
are provided for informational purposes only. Microsoft licenses the Third Party OSS to you under the licensing terms for
the Microsoft product or service. Microsoft reserves all other rights not expressly granted under this agreement, whether
by implication, estoppel or otherwise.†
1. withfig/autocomplete - IDE-style autocomplete for your existing terminal & shell (https://github.com/withfig/autocomplete)
Copyright (c) 2021 Hercules Labs Inc. (Fig)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Просмотреть файл

@ -0,0 +1,19 @@
{
"registrations": [
{
"component": {
"type": "git",
"git": {
"repositoryUrl": "https://github.com/withfig/autocomplete",
"commitHash": "1cc34dc1ba530bb620bd380c25cfb9eb2264be89"
}
},
"version": "2.684.0",
"license": {
"type": "MIT",
"url": "https://github.com/withfig/autocomplete/blob/main/LICENSE.md"
},
"description": "IDE-style autocomplete for your existing terminal & shell from withfig/autocomplete."
}
]
}

Просмотреть файл

@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const withBrowserDefaults = require('../shared.webpack.config').browser;
module.exports = withBrowserDefaults({
context: __dirname,
entry: {
extension: './src/terminalSuggestMain.ts'
},
output: {
filename: 'terminalSuggestMain.js'
},
resolve: {
fallback: {
'child_process': false
}
}
});

Просмотреть файл

@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const withDefaults = require('../shared.webpack.config');
module.exports = withDefaults({
context: __dirname,
entry: {
extension: './src/terminalSuggestMain.ts'
},
output: {
filename: 'terminalSuggestMain.js'
},
resolve: {
mainFields: ['module', 'main'],
extensions: ['.ts', '.js'] // support ts-files and js-files
}
});

16
extensions/terminal-suggest/package-lock.json сгенерированный Normal file
Просмотреть файл

@ -0,0 +1,16 @@
{
"name": "terminal-suggest",
"version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "terminal-suggest",
"version": "1.0.1",
"license": "MIT",
"engines": {
"vscode": "^1.95.0"
}
}
}
}

Просмотреть файл

@ -0,0 +1,32 @@
{
"name": "terminal-suggest",
"publisher": "vscode",
"displayName": "%displayName%",
"description": "%description%",
"version": "1.0.1",
"private": true,
"license": "MIT",
"icon": "./src/media/icon.png",
"engines": {
"vscode": "^1.95.0"
},
"categories": [
"Other"
],
"enabledApiProposals": [
"terminalCompletionProvider"
],
"scripts": {
"compile": "npx gulp compile-extension:npm",
"watch": "npx gulp watch-extension:npm"
},
"main": "./out/terminalSuggestMain",
"activationEvents": [
"onTerminalCompletionsRequested"
],
"repository": {
"type": "git",
"url": "https://github.com/microsoft/vscode.git"
}
}

Просмотреть файл

@ -0,0 +1,5 @@
{
"description": "Extension to add terminal completions for zsh, bash, and fish terminals.",
"displayName": "Terminal Suggest for VS Code",
"view.name": "Terminal Suggest"
}

Просмотреть файл

@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import code from './code';
const codeInsidersCompletionSpec: Fig.Spec = {
...code,
name: 'code-insiders',
};
export default codeInsidersCompletionSpec;

Просмотреть файл

@ -0,0 +1,318 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const commonOptions: Fig.Option[] = [
{
name: '-',
description: `Read from stdin (e.g. 'ps aux | grep code | code -')`,
},
{
name: ['-d', '--diff'],
description: 'Compare two files with each other',
args: [
{
name: 'file',
template: 'filepaths',
},
{
name: 'file',
template: 'filepaths',
},
],
},
{
name: ['-m', '--merge'],
description:
'Perform a three-way merge by providing paths for two modified versions of a file, the common origin of both modified versions and the output file to save merge results',
args: [
{
name: 'path1',
template: 'filepaths',
},
{
name: 'path2',
template: 'filepaths',
},
{
name: 'base',
template: 'filepaths',
},
{
name: 'result',
template: 'filepaths',
},
],
},
{
name: ['-a', '--add'],
description: 'Add folder(s) to the last active window',
args: {
name: 'folder',
template: 'folders',
isVariadic: true,
},
},
{
name: ['-g', '--goto'],
description:
'Open a file at the path on the specified line and character position',
args: {
name: 'file:line[:character]',
// TODO: Support :line[:character] completion?
template: 'filepaths',
},
},
{
name: ['-n', '--new-window'],
description: 'Force to open a new window',
},
{
name: ['-r', '--reuse-window'],
description: 'Force to open a file or folder in an already opened window',
},
{
name: ['-w', '--wait'],
description: 'Wait for the files to be closed before returning',
},
{
name: '--locale',
description: 'The locale to use (e.g. en-US or zh-TW)',
args: {
name: 'locale',
suggestions: [
// Supported locales: https://code.visualstudio.com/docs/getstarted/locales#_available-locales
// allow-any-unicode-next-line
{ name: 'en', icon: '🇺🇸', description: 'English (US)' },
// allow-any-unicode-next-line
{ name: 'zh-CN', icon: '🇨🇳', description: 'Simplified Chinese' },
// allow-any-unicode-next-line
{ name: 'zh-TW', icon: '🇹🇼', description: 'Traditional Chinese' },
// allow-any-unicode-next-line
{ name: 'fr', icon: '🇫🇷', description: 'French' },
// allow-any-unicode-next-line
{ name: 'de', icon: '🇩🇪', description: 'German' },
// allow-any-unicode-next-line
{ name: 'it', icon: '🇮🇹', description: 'Italian' },
// allow-any-unicode-next-line
{ name: 'es', icon: '🇪🇸', description: 'Spanish' },
// allow-any-unicode-next-line
{ name: 'ja', icon: '🇯🇵', description: 'Japanese' },
// allow-any-unicode-next-line
{ name: 'ko', icon: '🇰🇷', description: 'Korean' },
// allow-any-unicode-next-line
{ name: 'ru', icon: '🇷🇺', description: 'Russian' },
// allow-any-unicode-next-line
{ name: 'bg', icon: '🇧🇬', description: 'Bulgarian' },
// allow-any-unicode-next-line
{ name: 'hu', icon: '🇭🇺', description: 'Hungarian' },
// allow-any-unicode-next-line
{ name: 'pt-br', icon: '🇧🇷', description: 'Portuguese (Brazil)' },
// allow-any-unicode-next-line
{ name: 'tr', icon: '🇹🇷', description: 'Turkish' },
],
},
},
{
name: '--user-data-dir',
description:
'Specifies the directory that user data is kept in. Can be used to open multiple distinct instances of Code',
args: {
name: 'dir',
template: 'folders',
},
},
{
name: '--profile',
description:
'Opens the provided folder or workspace with the given profile and associates the profile with the workspace. If the profile does not exist, a new empty one is created. A folder or workspace must be provided for the profile to take effect',
args: {
name: 'settingsProfileName',
},
},
{
name: ['-h', '--help'],
description: 'Print usage',
},
];
const extensionManagementOptions: Fig.Option[] = [
{
name: '--extensions-dir',
description: 'Set the root path for extensions',
args: {
name: 'dir',
template: 'folders',
},
},
{
name: '--list-extensions',
description: 'List the installed extensions',
},
{
name: '--show-versions',
description:
'Show versions of installed extensions, when using --list-extensions',
},
{
name: '--category',
description:
'Filters installed extensions by provided category, when using --list-extensions',
args: {
name: 'category',
suggestions: [
'azure',
'data science',
'debuggers',
'extension packs',
'education',
'formatters',
'keymaps',
'language packs',
'linters',
'machine learning',
'notebooks',
'programming languages',
'scm providers',
'snippets',
'testing',
'themes',
'visualization',
'other',
],
},
},
{
name: '--install-extension',
description:
`Installs or updates an extension. The argument is either an extension id or a path to a VSIX. The identifier of an extension is '\${ publisher }.\${ name }'. Use '--force' argument to update to latest version. To install a specific version provide '@\${version}'. For example: 'vscode.csharp@1.2.3'`,
args: {
// TODO: Create extension ID generator
name: 'extension-id[@version] | path-to-vsix',
},
},
{
name: '--pre-release',
description:
'Installs the pre-release version of the extension, when using --install-extension',
},
{
name: '--uninstall-extension',
description: 'Uninstalls an extension',
args: {
// TODO: Create extension ID generator
name: 'extension-id',
},
},
{
name: '--enable-proposed-api',
description:
'Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually',
},
];
const troubleshootingOptions: Fig.Option[] = [
{
name: ['-v', '--version'],
description: 'Print version',
},
{
name: '--verbose',
description: 'Print verbose output (implies --wait)',
},
{
name: '--log',
description: `Log level to use. Default is 'info' when unspecified`,
args: {
name: 'level',
default: 'info',
suggestions: [
'critical',
'error',
'warn',
'info',
'debug',
'trace',
'off',
],
},
},
{
name: ['-s', '--status'],
description: 'Print process usage and diagnostics information',
},
{
name: '--prof-startup',
description: 'Run CPU profiler during startup',
},
{
name: '--disable-extensions',
description: 'Disable all installed extensions',
},
{
name: '--disable-extension',
description: 'Disable an extension',
args: {
// TODO: Create extension ID generator
name: 'extension-id',
},
},
{
name: '--sync',
description: 'Turn sync on or off',
args: {
name: 'sync',
description: 'Whether to enable sync',
suggestions: ['on', 'off'],
},
},
{
name: '--inspect-extensions',
description:
'Allow debugging and profiling of extensions. Check the developer tools for the connection URI',
args: {
name: 'port',
},
},
{
name: '--inspect-brk-extensions',
description:
'Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI',
args: {
name: 'port',
},
},
{
name: '--disable-gpu',
description: 'Disable GPU hardware acceleration',
},
{
name: '--max-memory',
description: 'Max memory size for a window (in Mbytes)',
args: {
name: 'memory',
description: 'Memory in megabytes',
},
},
{
name: '--telemetry',
description: 'Shows all telemetry events which VS code collects',
},
];
const codeCompletionSpec: Fig.Spec = {
name: 'code',
description: 'Visual Studio Code',
args: {
template: ['filepaths', 'folders'],
isVariadic: true,
},
options: [
...commonOptions,
...extensionManagementOptions,
...troubleshootingOptions,
],
};
export default codeCompletionSpec;

1300
extensions/terminal-suggest/src/completions/index.d.ts поставляемый Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Двоичные данные
extensions/terminal-suggest/src/media/icon.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.2 KiB

Просмотреть файл

@ -0,0 +1,247 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as os from 'os';
import * as fs from 'fs/promises';
import * as path from 'path';
import { ExecOptionsWithStringEncoding, execSync } from 'child_process';
import codeInsidersCompletionSpec from './completions/code-insiders';
import codeCompletionSpec from './completions/code';
let cachedAvailableCommands: Set<string> | undefined;
let cachedBuiltinCommands: Map<string, string[]> | undefined;
function getBuiltinCommands(shell: string): string[] | undefined {
try {
const shellType = path.basename(shell);
const cachedCommands = cachedBuiltinCommands?.get(shellType);
if (cachedCommands) {
return cachedCommands;
}
const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell };
switch (shellType) {
case 'bash': {
const bashOutput = execSync('compgen -b', options);
const bashResult = bashOutput.split('\n').filter(cmd => cmd);
if (bashResult.length) {
cachedBuiltinCommands?.set(shellType, bashResult);
return bashResult;
}
break;
}
case 'zsh': {
const zshOutput = execSync('printf "%s\\n" ${(k)builtins}', options);
const zshResult = zshOutput.split('\n').filter(cmd => cmd);
if (zshResult.length) {
cachedBuiltinCommands?.set(shellType, zshResult);
return zshResult;
}
}
case 'fish': {
// TODO: ghost text in the command line prevents
// completions from working ATM for fish
const fishOutput = execSync('functions -n', options);
const fishResult = fishOutput.split(', ').filter(cmd => cmd);
if (fishResult.length) {
cachedBuiltinCommands?.set(shellType, fishResult);
return fishResult;
}
break;
}
}
// native pwsh completions are builtin to vscode
return;
} catch (error) {
console.error('Error fetching builtin commands:', error);
return;
}
}
export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.window.registerTerminalCompletionProvider({
id: 'terminal-suggest',
async provideTerminalCompletions(terminal: vscode.Terminal, terminalContext: { commandLine: string; cursorPosition: number }, token: vscode.CancellationToken): Promise<vscode.TerminalCompletionItem[] | undefined> {
if (token.isCancellationRequested) {
return;
}
const availableCommands = await getCommandsInPath();
if (!availableCommands) {
return;
}
// TODO: Leverage shellType when available https://github.com/microsoft/vscode/issues/230165
const shellPath = 'shellPath' in terminal.creationOptions ? terminal.creationOptions.shellPath : vscode.env.shell;
if (!shellPath) {
return;
}
const builtinCommands = getBuiltinCommands(shellPath);
builtinCommands?.forEach(command => availableCommands.add(command));
const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition);
let result: vscode.TerminalCompletionItem[] = [];
const specs = [codeCompletionSpec, codeInsidersCompletionSpec];
for (const spec of specs) {
const specName = getLabel(spec);
if (!specName || !availableCommands.has(specName)) {
continue;
}
if (terminalContext.commandLine.startsWith(specName)) {
if ('options' in codeInsidersCompletionSpec && codeInsidersCompletionSpec.options) {
for (const option of codeInsidersCompletionSpec.options) {
const optionLabel = getLabel(option);
if (!optionLabel) {
continue;
}
if (optionLabel.startsWith(prefix) || (prefix.length > specName.length && prefix.trim() === specName)) {
result.push(createCompletionItem(terminalContext.cursorPosition, prefix, optionLabel, option.description, false, vscode.TerminalCompletionItemKind.Flag));
}
if (option.args !== undefined) {
const args = Array.isArray(option.args) ? option.args : [option.args];
for (const arg of args) {
if (!arg) {
continue;
}
if (arg.template) {
// TODO: return file/folder completion items
if (arg.template === 'filepaths') {
// if (label.startsWith(prefix+\s*)) {
// result.push(FilePathCompletionItem)
// }
} else if (arg.template === 'folders') {
// if (label.startsWith(prefix+\s*)) {
// result.push(FolderPathCompletionItem)
// }
}
continue;
}
const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition);
const expectedText = `${optionLabel} `;
if (arg.suggestions?.length && precedingText.includes(expectedText)) {
// there are specific suggestions to show
result = [];
const indexOfPrecedingText = terminalContext.commandLine.lastIndexOf(expectedText);
const currentPrefix = precedingText.slice(indexOfPrecedingText + expectedText.length);
for (const suggestion of arg.suggestions) {
const suggestionLabel = getLabel(suggestion);
if (suggestionLabel && suggestionLabel.startsWith(currentPrefix)) {
const hasSpaceBeforeCursor = terminalContext.commandLine[terminalContext.cursorPosition - 1] === ' ';
// prefix will be '' if there is a space before the cursor
result.push(createCompletionItem(terminalContext.cursorPosition, precedingText, suggestionLabel, arg.name, hasSpaceBeforeCursor, vscode.TerminalCompletionItemKind.Argument));
}
}
if (result.length) {
return result;
}
}
}
}
}
}
}
}
for (const command of availableCommands) {
if (command.startsWith(prefix)) {
result.push(createCompletionItem(terminalContext.cursorPosition, prefix, command));
}
}
if (token.isCancellationRequested) {
return undefined;
}
const uniqueResults = new Map<string, vscode.TerminalCompletionItem>();
for (const item of result) {
if (!uniqueResults.has(item.label)) {
uniqueResults.set(item.label, item);
}
}
return uniqueResults.size ? Array.from(uniqueResults.values()) : undefined;
}
}));
}
function getLabel(spec: Fig.Spec | Fig.Arg | Fig.Suggestion | string): string | undefined {
if (typeof spec === 'string') {
return spec;
}
if (typeof spec.name === 'string') {
return spec.name;
}
if (!Array.isArray(spec.name) || spec.name.length === 0) {
return;
}
return spec.name[0];
}
function createCompletionItem(cursorPosition: number, prefix: string, label: string, description?: string, hasSpaceBeforeCursor?: boolean, kind?: vscode.TerminalCompletionItemKind): vscode.TerminalCompletionItem {
return {
label,
detail: description ?? '',
replacementIndex: hasSpaceBeforeCursor ? cursorPosition : cursorPosition - 1,
replacementLength: label.length - prefix.length,
kind: kind ?? vscode.TerminalCompletionItemKind.Method
};
}
async function getCommandsInPath(): Promise<Set<string> | undefined> {
if (cachedAvailableCommands) {
return cachedAvailableCommands;
}
const paths = os.platform() === 'win32' ? process.env.PATH?.split(';') : process.env.PATH?.split(':');
if (!paths) {
return;
}
const executables = new Set<string>();
for (const path of paths) {
try {
const dirExists = await fs.stat(path).then(stat => stat.isDirectory()).catch(() => false);
if (!dirExists) {
continue;
}
const files = await vscode.workspace.fs.readDirectory(vscode.Uri.file(path));
for (const [file, fileType] of files) {
if (fileType === vscode.FileType.File || fileType === vscode.FileType.SymbolicLink) {
executables.add(file);
}
}
} catch (e) {
// Ignore errors for directories that can't be read
continue;
}
}
cachedAvailableCommands = executables;
return executables;
}
function getPrefix(commandLine: string, cursorPosition: number): string {
// Return an empty string if the command line is empty after trimming
if (commandLine.trim() === '') {
return '';
}
// Check if cursor is not at the end and there's non-whitespace after the cursor
if (cursorPosition < commandLine.length && /\S/.test(commandLine[cursorPosition])) {
return '';
}
// Extract the part of the line up to the cursor position
const beforeCursor = commandLine.slice(0, cursorPosition);
// Find the last sequence of non-whitespace characters before the cursor
const match = beforeCursor.match(/(\S+)\s*$/);
// Return the match if found, otherwise undefined
return match ? match[0] : '';
}

Просмотреть файл

@ -0,0 +1,20 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out",
"types": [
"node"
],
"target": "es2020",
"module": "CommonJS",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
},
"include": [
"src/**/*",
"../../src/vscode-dts/vscode.d.ts",
"../../src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts"
]
}

Просмотреть файл

@ -343,6 +343,9 @@ const _allApiProposals = {
telemetry: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.telemetry.d.ts',
},
terminalCompletionProvider: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts',
},
terminalDataWriteEvent: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDataWriteEvent.d.ts',
},

Просмотреть файл

@ -24,6 +24,7 @@ import { ISerializableEnvironmentDescriptionMap, ISerializableEnvironmentVariabl
import { ITerminalLinkProviderService } from '../../contrib/terminalContrib/links/browser/links.js';
import { ITerminalQuickFixService, ITerminalQuickFix, TerminalQuickFixType } from '../../contrib/terminalContrib/quickFix/browser/quickFix.js';
import { TerminalCapability } from '../../../platform/terminal/common/capabilities/capabilities.js';
import { ITerminalCompletionService } from '../../contrib/terminalContrib/suggest/browser/terminalCompletionService.js';
@extHostNamedCustomer(MainContext.MainThreadTerminalService)
export class MainThreadTerminalService implements MainThreadTerminalServiceShape {
@ -39,6 +40,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
private readonly _extHostTerminals = new Map<string, Promise<ITerminalInstance>>();
private readonly _terminalProcessProxies = new Map<number, ITerminalProcessExtHostProxy>();
private readonly _profileProviders = new Map<string, IDisposable>();
private readonly _completionProviders = new Map<string, IDisposable>();
private readonly _quickFixProviders = new Map<string, IDisposable>();
private readonly _dataEventTracker = new MutableDisposable<TerminalDataEventTracker>();
private readonly _sendCommandEventListener = new MutableDisposable();
@ -65,7 +67,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
@ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService,
@ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService,
@ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService
@ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService,
@ITerminalCompletionService private readonly _terminalCompletionService: ITerminalCompletionService,
) {
this._proxy = _extHostContext.getProxy(ExtHostContext.ExtHostTerminalService);
@ -264,6 +267,20 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
this._terminalService.registerProcessSupport(isSupported);
}
public $registerCompletionProvider(id: string, extensionIdentifier: string, ...triggerCharacters: string[]): void {
this._completionProviders.set(id, this._terminalCompletionService.registerTerminalCompletionProvider(extensionIdentifier, id, {
id,
provideCompletions: async (commandLine, cursorPosition, token) => {
return await this._proxy.$provideTerminalCompletions(id, { commandLine, cursorPosition }, token);
}
}, ...triggerCharacters));
}
public $unregisterCompletionProvider(id: string): void {
this._completionProviders.get(id)?.dispose();
this._completionProviders.delete(id);
}
public $registerProfileProvider(id: string, extensionIdentifier: string): void {
// Proxy profile provider requests through the extension host
this._profileProviders.set(id, this._terminalProfileService.registerTerminalProfileProvider(extensionIdentifier, id, {
@ -482,3 +499,4 @@ function parseQuickFix(id: string, source: string, fix: TerminalQuickFix): ITerm
}
return { id, type, source, ...fix };
}

Просмотреть файл

@ -843,6 +843,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
registerTerminalProfileProvider(id: string, provider: vscode.TerminalProfileProvider): vscode.Disposable {
return extHostTerminalService.registerProfileProvider(extension, id, provider);
},
registerTerminalCompletionProvider(provider: vscode.TerminalCompletionProvider<vscode.TerminalCompletionItem>, ...triggerCharacters: string[]): vscode.Disposable {
checkProposedApiEnabled(extension, 'terminalCompletionProvider');
return extHostTerminalService.registerTerminalCompletionProvider(extension, provider, ...triggerCharacters);
},
registerTerminalQuickFixProvider(id: string, provider: vscode.TerminalQuickFixProvider): vscode.Disposable {
checkProposedApiEnabled(extension, 'terminalQuickFixProvider');
return extHostTerminalService.registerTerminalQuickFixProvider(id, extension.identifier.value, provider);
@ -1661,6 +1665,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
TerminalProfile: extHostTypes.TerminalProfile,
TerminalExitReason: extHostTypes.TerminalExitReason,
TerminalShellExecutionCommandLineConfidence: extHostTypes.TerminalShellExecutionCommandLineConfidence,
TerminalCompletionItem: extHostTypes.TerminalCompletionItem,
TerminalCompletionItemKind: extHostTypes.TerminalCompletionItemKind,
TextDocumentSaveReason: extHostTypes.TextDocumentSaveReason,
TextEdit: extHostTypes.TextEdit,
SnippetTextEdit: extHostTypes.SnippetTextEdit,

Просмотреть файл

@ -84,7 +84,7 @@ import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from '../../servic
import * as search from '../../services/search/common/search.js';
import { TextSearchCompleteMessage } from '../../services/search/common/searchExtTypes.js';
import { ISaveProfileResult } from '../../services/userDataProfile/common/userDataProfile.js';
import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js';
import { TerminalCompletionItem, TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js';
import * as tasks from './shared/tasks.js';
export interface IWorkspaceData extends IStaticWorkspaceData {
@ -537,6 +537,7 @@ export interface TerminalLaunchConfig {
isTransient?: boolean;
}
export interface MainThreadTerminalServiceShape extends IDisposable {
$createTerminal(extHostTerminalId: string, config: TerminalLaunchConfig): Promise<void>;
$dispose(id: ExtHostTerminalIdentifier): void;
@ -546,6 +547,8 @@ export interface MainThreadTerminalServiceShape extends IDisposable {
$registerProcessSupport(isSupported: boolean): void;
$registerProfileProvider(id: string, extensionIdentifier: string): void;
$unregisterProfileProvider(id: string): void;
$registerCompletionProvider(id: string, extensionIdentifier: string, ...triggerCharacters: string[]): void;
$unregisterCompletionProvider(id: string): void;
$registerQuickFixProvider(id: string, extensionIdentifier: string): void;
$unregisterQuickFixProvider(id: string): void;
$setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: ISerializableEnvironmentVariableCollection | undefined, descriptionMap: ISerializableEnvironmentDescriptionMap): void;
@ -2396,6 +2399,11 @@ export interface ITerminalCommandDto {
output: string | undefined;
}
export interface ITerminalCompletionContextDto {
commandLine: string;
cursorPosition: number;
}
export interface ExtHostTerminalServiceShape {
$acceptTerminalClosed(id: number, exitCode: number | undefined, exitReason: TerminalExitReason): void;
$acceptTerminalOpened(id: number, extHostTerminalId: string | undefined, name: string, shellLaunchConfig: IShellLaunchConfigDto): void;
@ -2422,6 +2430,7 @@ export interface ExtHostTerminalServiceShape {
$acceptDefaultProfile(profile: ITerminalProfile, automationProfile: ITerminalProfile): void;
$createContributedProfileTerminal(id: string, options: ICreateContributedTerminalProfileOptions): Promise<void>;
$provideTerminalQuickFixes(id: string, matchResult: TerminalCommandMatchResultDto, token: CancellationToken): Promise<SingleOrMany<TerminalQuickFix> | undefined>;
$provideTerminalCompletions(id: string, options: ITerminalCompletionContextDto, token: CancellationToken): Promise<TerminalCompletionItem[] | undefined>;
}
export interface ExtHostTerminalShellIntegrationShape {

Просмотреть файл

@ -5,12 +5,12 @@
import type * as vscode from 'vscode';
import { Event, Emitter } from '../../../base/common/event.js';
import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, ITerminalDimensionsDto, ITerminalLinkDto, ExtHostTerminalIdentifier, ICommandDto, ITerminalQuickFixOpenerDto, ITerminalQuickFixTerminalCommandDto, TerminalCommandMatchResultDto, ITerminalCommandDto } from './extHost.protocol.js';
import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, ITerminalDimensionsDto, ITerminalLinkDto, ExtHostTerminalIdentifier, ICommandDto, ITerminalQuickFixOpenerDto, ITerminalQuickFixTerminalCommandDto, TerminalCommandMatchResultDto, ITerminalCommandDto, ITerminalCompletionContextDto } from './extHost.protocol.js';
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
import { URI } from '../../../base/common/uri.js';
import { IExtHostRpcService } from './extHostRpcService.js';
import { IDisposable, DisposableStore, Disposable, MutableDisposable } from '../../../base/common/lifecycle.js';
import { Disposable as VSCodeDisposable, EnvironmentVariableMutatorType, TerminalExitReason } from './extHostTypes.js';
import { Disposable as VSCodeDisposable, EnvironmentVariableMutatorType, TerminalExitReason, TerminalCompletionItem } from './extHostTypes.js';
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
import { localize } from '../../../nls.js';
import { NotSupportedError } from '../../../base/common/errors.js';
@ -56,6 +56,7 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID
getEnvironmentVariableCollection(extension: IExtensionDescription): IEnvironmentVariableCollection;
getTerminalById(id: number): ExtHostTerminal | null;
getTerminalIdByApiObject(apiTerminal: vscode.Terminal): number | null;
registerTerminalCompletionProvider<T extends vscode.TerminalCompletionItem[]>(extension: IExtensionDescription, provider: vscode.TerminalCompletionProvider<vscode.TerminalCompletionItem>, ...triggerCharacters: string[]): vscode.Disposable;
}
interface IEnvironmentVariableCollection extends vscode.EnvironmentVariableCollection {
@ -397,6 +398,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
private readonly _bufferer: TerminalDataBufferer;
private readonly _linkProviders: Set<vscode.TerminalLinkProvider> = new Set();
private readonly _completionProviders: Map<string, vscode.TerminalCompletionProvider<vscode.TerminalCompletionItem>> = new Map();
private readonly _profileProviders: Map<string, vscode.TerminalProfileProvider> = new Map();
private readonly _quickFixProviders: Map<string, vscode.TerminalQuickFixProvider> = new Map();
private readonly _terminalLinkCache: Map<number, Map<number, ICachedLinkEntry>> = new Map();
@ -719,19 +721,6 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
return Promise.resolve(id);
}
public registerLinkProvider(provider: vscode.TerminalLinkProvider): vscode.Disposable {
this._linkProviders.add(provider);
if (this._linkProviders.size === 1) {
this._proxy.$startLinkProvider();
}
return new VSCodeDisposable(() => {
this._linkProviders.delete(provider);
if (this._linkProviders.size === 0) {
this._proxy.$stopLinkProvider();
}
});
}
public registerProfileProvider(extension: IExtensionDescription, id: string, provider: vscode.TerminalProfileProvider): vscode.Disposable {
if (this._profileProviders.has(id)) {
@ -745,6 +734,37 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
});
}
public registerTerminalCompletionProvider(extension: IExtensionDescription, provider: vscode.TerminalCompletionProvider<TerminalCompletionItem>, ...triggerCharacters: string[]): vscode.Disposable {
if (this._completionProviders.has(provider.id)) {
throw new Error(`Terminal completion provider "${provider.id}" already registered`);
}
this._completionProviders.set(provider.id, provider);
this._proxy.$registerCompletionProvider(provider.id, extension.identifier.value, ...triggerCharacters);
return new VSCodeDisposable(() => {
this._completionProviders.delete(provider.id);
this._proxy.$unregisterCompletionProvider(provider.id);
});
}
public async $provideTerminalCompletions(id: string, options: ITerminalCompletionContextDto): Promise<vscode.TerminalCompletionItem[] | undefined> {
const token = new CancellationTokenSource().token;
if (token.isCancellationRequested || !this.activeTerminal) {
return undefined;
}
const provider = this._completionProviders.get(id);
if (!provider) {
return;
}
const completions = await provider.provideTerminalCompletions(this.activeTerminal, options, token);
if (completions === null || completions === undefined) {
return;
}
return completions;
}
public registerTerminalQuickFixProvider(id: string, extensionId: string, provider: vscode.TerminalQuickFixProvider): vscode.Disposable {
if (this._quickFixProviders.has(id)) {
throw new Error(`Terminal quick fix provider "${id}" is already registered`);
@ -811,6 +831,19 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
this.createTerminalFromOptions(profile.options, options);
}
public registerLinkProvider(provider: vscode.TerminalLinkProvider): vscode.Disposable {
this._linkProviders.add(provider);
if (this._linkProviders.size === 1) {
this._proxy.$startLinkProvider();
}
return new VSCodeDisposable(() => {
this._linkProviders.delete(provider);
if (this._linkProviders.size === 0) {
this._proxy.$stopLinkProvider();
}
});
}
public async $provideLinks(terminalId: number, line: string): Promise<ITerminalLinkDto[]> {
const terminal = this.getTerminalById(terminalId);
if (!terminal) {

Просмотреть файл

@ -2114,6 +2114,37 @@ export class TerminalProfile implements vscode.TerminalProfile {
}
}
export enum TerminalCompletionItemKind {
File = 0,
Folder = 1,
Flag = 2,
Method = 3,
Argument = 4
}
export class TerminalCompletionItem implements vscode.TerminalCompletionItem {
label: string;
icon?: ThemeIcon | undefined;
detail?: string | undefined;
isFile?: boolean | undefined;
isDirectory?: boolean | undefined;
isKeyword?: boolean | undefined;
replacementIndex: number;
replacementLength: number;
constructor(label: string, icon?: ThemeIcon, detail?: string, isFile?: boolean, isDirectory?: boolean, isKeyword?: boolean, replacementIndex?: number, replacementLength?: number) {
this.label = label;
this.icon = icon;
this.detail = detail;
this.isFile = isFile;
this.isDirectory = isDirectory;
this.isKeyword = isKeyword;
this.replacementIndex = replacementIndex ?? 0;
this.replacementLength = replacementLength ?? 0;
}
}
export enum TaskRevealKind {
Always = 1,

Просмотреть файл

@ -3,8 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITerminalCompletionProvider } from './terminalCompletionService.js';
import { ISimpleCompletion } from '../../../../services/suggest/browser/simpleCompletionItem.js';
import { ITerminalCompletion, ITerminalCompletionProvider } from './terminalCompletionService.js';
import { Disposable } from '../../../../../base/common/lifecycle.js';
import type { ITerminalAddon, Terminal } from '@xterm/xterm';
import { Event, Emitter } from '../../../../../base/common/event.js';
@ -21,6 +20,7 @@ import { GeneralShellType } from '../../../../../platform/terminal/common/termin
import { ITerminalCapabilityStore, TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
import { DeferredPromise } from '../../../../../base/common/async.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
export const enum VSCodeSuggestOscPt {
Completions = 'Completions',
@ -53,19 +53,22 @@ const enum RequestCompletionsSequence {
}
export class PwshCompletionProviderAddon extends Disposable implements ITerminalAddon, ITerminalCompletionProvider {
id: string = PwshCompletionProviderAddon.ID;
triggerCharacters?: string[] | undefined;
isBuiltin?: boolean = true;
static readonly ID = 'terminal.pwshCompletionProvider';
static cachedPwshCommands: Set<ISimpleCompletion>;
static cachedPwshCommands: Set<ITerminalCompletion>;
readonly shellTypes = [GeneralShellType.PowerShell];
private _codeCompletionsRequested: boolean = false;
private _gitCompletionsRequested: boolean = false;
private _lastUserDataTimestamp: number = 0;
private _terminal?: Terminal;
private _mostRecentCompletion?: ISimpleCompletion;
private _mostRecentCompletion?: ITerminalCompletion;
private _promptInputModel?: IPromptInputModel;
private _currentPromptInputState?: IPromptInputModelState;
private _enableWidget: boolean = true;
isPasting: boolean = false;
private _completionsDeferred: DeferredPromise<ISimpleCompletion[] | undefined> | null = null;
private _completionsDeferred: DeferredPromise<ITerminalCompletion[] | undefined> | null = null;
private readonly _onBell = this._register(new Emitter<void>());
readonly onBell = this._onBell.event;
private readonly _onAcceptedCompletion = this._register(new Emitter<string>());
@ -76,7 +79,7 @@ export class PwshCompletionProviderAddon extends Disposable implements ITerminal
readonly onDidRequestSendText = this._onDidRequestSendText.event;
constructor(
providedPwshCommands: Set<ISimpleCompletion> | undefined,
providedPwshCommands: Set<ITerminalCompletion> | undefined,
capabilities: ITerminalCapabilityStore,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IStorageService private readonly _storageService: IStorageService
@ -227,7 +230,7 @@ export class PwshCompletionProviderAddon extends Disposable implements ITerminal
return true;
}
private _resolveCompletions(result: ISimpleCompletion[] | undefined) {
private _resolveCompletions(result: ITerminalCompletion[] | undefined) {
if (!this._completionsDeferred) {
return;
}
@ -236,12 +239,12 @@ export class PwshCompletionProviderAddon extends Disposable implements ITerminal
this._completionsDeferred = null;
}
private _getCompletionsPromise(): Promise<ISimpleCompletion[] | undefined> {
this._completionsDeferred = new DeferredPromise<ISimpleCompletion[] | undefined>();
private _getCompletionsPromise(): Promise<ITerminalCompletion[] | undefined> {
this._completionsDeferred = new DeferredPromise<ITerminalCompletion[] | undefined>();
return this._completionsDeferred.p;
}
provideCompletions(value: string): Promise<ISimpleCompletion[] | undefined> {
provideCompletions(value: string, cursorPosition: number, token: CancellationToken): Promise<ITerminalCompletion[] | undefined> {
const builtinCompletionsConfig = this._configurationService.getValue<ITerminalSuggestConfiguration>(terminalSuggestConfigSection).builtinCompletions;
if (!this._codeCompletionsRequested && builtinCompletionsConfig.pwshCode) {
this._onDidRequestSendText.fire(RequestCompletionsSequence.Code);
@ -262,11 +265,27 @@ export class PwshCompletionProviderAddon extends Disposable implements ITerminal
if (this._lastUserDataTimestamp > SuggestAddon.lastAcceptedCompletionTimestamp) {
this._onDidRequestSendText.fire(RequestCompletionsSequence.Contextual);
}
return this._getCompletionsPromise();
if (token.isCancellationRequested) {
return Promise.resolve(undefined);
}
return new Promise((resolve) => {
const completionPromise = this._getCompletionsPromise();
this._register(token.onCancellationRequested(() => {
this._resolveCompletions(undefined);
}));
completionPromise.then(result => {
if (token.isCancellationRequested) {
resolve(undefined);
} else {
resolve(result);
}
});
});
}
}
export function parseCompletionsFromShell(rawCompletions: PwshCompletion | PwshCompletion[] | CompressedPwshCompletion[] | CompressedPwshCompletion, replacementIndex: number, replacementLength: number): ISimpleCompletion[] {
export function parseCompletionsFromShell(rawCompletions: PwshCompletion | PwshCompletion[] | CompressedPwshCompletion[] | CompressedPwshCompletion, replacementIndex: number, replacementLength: number): ITerminalCompletion[] {
if (!rawCompletions) {
return [];
}
@ -295,10 +314,10 @@ export function parseCompletionsFromShell(rawCompletions: PwshCompletion | PwshC
typedRawCompletions = rawCompletions as PwshCompletion[];
}
}
return typedRawCompletions.map(e => rawCompletionToISimpleCompletion(e, replacementIndex, replacementLength));
return typedRawCompletions.map(e => rawCompletionToITerminalCompletion(e, replacementIndex, replacementLength));
}
function rawCompletionToISimpleCompletion(rawCompletion: PwshCompletion, replacementIndex: number, replacementLength: number): ISimpleCompletion {
function rawCompletionToITerminalCompletion(rawCompletion: PwshCompletion, replacementIndex: number, replacementLength: number): ITerminalCompletion {
// HACK: Somewhere along the way from the powershell script to here, the path separator at the
// end of directories may go missing, likely because `\"` -> `"`. As a result, make sure there
// is a trailing separator at the end of all directory completions. This should not be done for

Просмотреть файл

@ -2,38 +2,59 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
import { TerminalSettingId, TerminalShellType } from '../../../../../platform/terminal/common/terminal.js';
import { ISimpleCompletion } from '../../../../services/suggest/browser/simpleCompletionItem.js';
import { ITerminalSuggestConfiguration, terminalSuggestConfigSection } from '../common/terminalSuggestConfiguration.js';
export const ITerminalCompletionService = createDecorator<ITerminalCompletionService>('terminalCompletionService');
export enum ISimpleCompletionKind {
export enum TerminalCompletionItemKind {
File = 0,
Folder = 1,
Flag = 2,
Method = 3
Method = 3,
Argument = 4
}
export interface ITerminalCompletion extends ISimpleCompletion {
kind?: TerminalCompletionItemKind;
}
export interface ITerminalCompletionProvider {
id: string;
shellTypes?: TerminalShellType[];
provideCompletions(value: string, cursorPosition: number): Promise<ISimpleCompletion[] | undefined>;
provideCompletions(value: string, cursorPosition: number, token: CancellationToken): Promise<ISimpleCompletion[] | undefined>;
triggerCharacters?: string[];
isBuiltin?: boolean;
}
export interface ITerminalCompletionService {
_serviceBrand: undefined;
readonly providers: IterableIterator<ITerminalCompletionProvider>;
registerTerminalCompletionProvider(extensionIdentifier: string, id: string, provider: ITerminalCompletionProvider, ...triggerCharacters: string[]): IDisposable;
provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType): Promise<ISimpleCompletion[] | undefined>;
provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, token: CancellationToken, triggerCharacter?: boolean): Promise<ITerminalCompletion[] | undefined>;
}
// TODO: make name consistent
export class TerminalCompletionService extends Disposable implements ITerminalCompletionService {
declare _serviceBrand: undefined;
private readonly _providers: Map</*ext id*/string, Map</*provider id*/string, ITerminalCompletionProvider>> = new Map();
get providers(): IterableIterator<ITerminalCompletionProvider> {
return this._providersGenerator();
}
private *_providersGenerator(): IterableIterator<ITerminalCompletionProvider> {
for (const providerMap of this._providers.values()) {
for (const provider of providerMap.values()) {
yield provider;
}
}
}
constructor(@IConfigurationService private readonly _configurationService: IConfigurationService) {
super();
}
@ -45,6 +66,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
this._providers.set(extensionIdentifier, extMap);
}
provider.triggerCharacters = triggerCharacters;
provider.id = id;
extMap.set(id, provider);
return toDisposable(() => {
const extMap = this._providers.get(extensionIdentifier);
@ -57,31 +79,62 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
});
}
async provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType): Promise<ISimpleCompletion[] | undefined> {
async provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, token: CancellationToken, triggerCharacter?: boolean): Promise<ISimpleCompletion[] | undefined> {
const completionItems: ISimpleCompletion[] = [];
if (!this._providers || !this._providers.values) {
return undefined;
}
// TODO: Use Promise.all so all providers are called in parallel
for (const providerMap of this._providers.values()) {
for (const [extensionId, provider] of providerMap) {
if (provider.shellTypes && !provider.shellTypes.includes(shellType)) {
const extensionCompletionsEnabled = this._configurationService.getValue<ITerminalSuggestConfiguration>(terminalSuggestConfigSection).enableExtensionCompletions;
let providers;
if (triggerCharacter) {
const providersToRequest: ITerminalCompletionProvider[] = [];
for (const provider of this.providers) {
if (!provider.triggerCharacters) {
continue;
}
const completions = await provider.provideCompletions(promptValue, cursorPosition);
const devModeEnabled = this._configurationService.getValue(TerminalSettingId.DevMode);
if (completions) {
for (const completion of completions) {
if (devModeEnabled && !completion.detail?.includes(extensionId)) {
completion.detail = `(${extensionId}) ${completion.detail ?? ''}`;
}
completionItems.push(completion);
for (const char of provider.triggerCharacters) {
if (promptValue.substring(0, cursorPosition)?.endsWith(char)) {
providersToRequest.push(provider);
break;
}
}
}
providers = providersToRequest;
} else {
providers = [...this._providers.values()].flatMap(providerMap => [...providerMap.values()]);
}
if (!extensionCompletionsEnabled) {
providers = providers.filter(p => p.isBuiltin);
}
await this._collectCompletions(providers, shellType, promptValue, cursorPosition, completionItems, token);
return completionItems.length > 0 ? completionItems : undefined;
}
private async _collectCompletions(providers: ITerminalCompletionProvider[], shellType: TerminalShellType, promptValue: string, cursorPosition: number, completionItems: ISimpleCompletion[], token: CancellationToken) {
const completionPromises = providers.map(async provider => {
if (provider.shellTypes && !provider.shellTypes.includes(shellType)) {
return [];
}
const completions = await provider.provideCompletions(promptValue, cursorPosition, token);
const devModeEnabled = this._configurationService.getValue(TerminalSettingId.DevMode);
if (completions) {
return completions.map(completion => {
if (devModeEnabled && !completion.detail?.includes(provider.id)) {
completion.detail = `(${provider.id}) ${completion.detail ?? ''}`;
}
return completion;
});
}
return [];
});
const results = await Promise.all(completionPromises);
results.forEach(completions => completionItems.push(...completions));
}
}

Просмотреть файл

@ -23,13 +23,15 @@ import { ITerminalConfigurationService } from '../../../terminal/browser/termina
import type { IXtermCore } from '../../../terminal/browser/xterm-private.js';
import { TerminalStorageKeys } from '../../../terminal/common/terminalStorageKeys.js';
import { terminalSuggestConfigSection, type ITerminalSuggestConfiguration } from '../common/terminalSuggestConfiguration.js';
import { SimpleCompletionItem, ISimpleCompletion } from '../../../../services/suggest/browser/simpleCompletionItem.js';
import { SimpleCompletionItem } from '../../../../services/suggest/browser/simpleCompletionItem.js';
import { LineContext, SimpleCompletionModel } from '../../../../services/suggest/browser/simpleCompletionModel.js';
import { ISimpleSelectedSuggestion, SimpleSuggestWidget } from '../../../../services/suggest/browser/simpleSuggestWidget.js';
import type { ISimpleSuggestWidgetFontInfo } from '../../../../services/suggest/browser/simpleSuggestWidgetRenderer.js';
import { ITerminalCompletionService } from './terminalCompletionService.js';
import { ITerminalCompletion, ITerminalCompletionService, TerminalCompletionItemKind } from './terminalCompletionService.js';
import { TerminalShellType } from '../../../../../platform/terminal/common/terminal.js';
import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';
import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
export interface ISuggestController {
isPasting: boolean;
@ -57,7 +59,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
private _enableWidget: boolean = true;
private _pathSeparator: string = sep;
private _isFilteringDirectories: boolean = false;
private _mostRecentCompletion?: ISimpleCompletion;
private _mostRecentCompletion?: ITerminalCompletion;
// TODO: Remove these in favor of prompt input state
private _leadingLineContent?: string;
@ -79,6 +81,14 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
private readonly _onDidReceiveCompletions = this._register(new Emitter<void>());
readonly onDidReceiveCompletions = this._onDidReceiveCompletions.event;
private _kindToIconMap = new Map<number, ThemeIcon>([
[TerminalCompletionItemKind.File, Codicon.file],
[TerminalCompletionItemKind.Folder, Codicon.folder],
[TerminalCompletionItemKind.Flag, Codicon.symbolProperty],
[TerminalCompletionItemKind.Method, Codicon.symbolMethod],
[TerminalCompletionItemKind.Argument, Codicon.symbolVariable]
]);
constructor(
private readonly _shellType: TerminalShellType | undefined,
private readonly _capabilities: ITerminalCapabilityStore,
@ -87,6 +97,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService,
@IExtensionService private readonly _extensionService: IExtensionService
) {
super();
@ -116,7 +127,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
}));
}
private async _handleCompletionProviders(terminal: Terminal | undefined, token: CancellationToken): Promise<void> {
private async _handleCompletionProviders(terminal: Terminal | undefined, token: CancellationToken, triggerCharacter?: boolean): Promise<void> {
// Nothing to handle if the terminal is not attached
if (!terminal?.element || !this._enableWidget || !this._promptInputModel) {
return;
@ -130,17 +141,24 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
if (!this._shellType) {
return;
}
this._requestedCompletionsIndex = this._promptInputModel.cursorIndex;
const providedCompletions = await this._terminalCompletionService.provideCompletions(this._promptInputModel.value, this._promptInputModel.cursorIndex, this._shellType);
const enableExtensionCompletions = this._configurationService.getValue<ITerminalSuggestConfiguration>(terminalSuggestConfigSection).enableExtensionCompletions;
if (enableExtensionCompletions) {
await this._extensionService.activateByEvent('onTerminalCompletionsRequested');
}
const providedCompletions = await this._terminalCompletionService.provideCompletions(this._promptInputModel.value, this._promptInputModel.cursorIndex, this._shellType, token, triggerCharacter);
if (!providedCompletions?.length || token.isCancellationRequested) {
return;
}
this._onDidReceiveCompletions.fire();
const replacementIndices = [...new Set(providedCompletions.filter(p => p.replacementIndex !== undefined).map(c => c.replacementIndex))].sort();
// this is of length 1 because all extension providers should have the same replacement index, so we just take the last one
const replacementIndex = replacementIndices.length > 0 ? replacementIndices[replacementIndices.length - 1] : 0;
// ATM, the two providers calculate the same replacement index / prefix, so we can just take the first one
// TODO: figure out if we can add support for multiple replacement indices
const replacementIndices = [...new Set(providedCompletions.map(c => c.replacementIndex))];
const replacementIndex = replacementIndices.length === 1 ? replacementIndices[0] : 0;
this._providerReplacementIndex = replacementIndex;
this._requestedCompletionsIndex = this._promptInputModel.cursorIndex;
this._currentPromptInputState = {
value: this._promptInputModel.value,
@ -183,7 +201,11 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
this._pathSeparator = firstDir?.label.match(/(?<sep>[\\\/])/)?.groups?.sep ?? sep;
normalizedLeadingLineContent = normalizePathSeparator(normalizedLeadingLineContent, this._pathSeparator);
}
for (const completion of completions) {
if (!completion.icon && completion.kind) {
completion.icon = this._kindToIconMap.get(completion.kind);
}
}
const lineContext = new LineContext(normalizedLeadingLineContent, this._cursorIndexDelta);
const model = new SimpleCompletionModel(completions.filter(c => !!c.label).map(c => new SimpleCompletionItem(c)), lineContext);
if (token.isCancellationRequested) {
@ -192,6 +214,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
this._showCompletions(model);
}
setContainerWithOverflow(container: HTMLElement): void {
this._container = container;
}
@ -200,7 +224,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
this._screen = screen;
}
async requestCompletions(): Promise<void> {
async requestCompletions(triggerCharacter?: boolean): Promise<void> {
if (!this._promptInputModel) {
return;
}
@ -214,7 +238,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
}
this._cancellationTokenSource = new CancellationTokenSource();
const token = this._cancellationTokenSource.token;
await this._handleCompletionProviders(this._terminal, token);
await this._handleCompletionProviders(this._terminal, token, triggerCharacter);
}
private _sync(promptInputState: IPromptInputModelState): void {
@ -254,7 +278,20 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
this.requestCompletions();
sent = true;
}
// TODO: eventually add an appropriate trigger char check for other shells
if (!sent) {
for (const provider of this._terminalCompletionService.providers) {
if (!provider.triggerCharacters) {
continue;
}
for (const char of provider.triggerCharacters) {
if (prefix?.endsWith(char)) {
this.requestCompletions(true);
sent = true;
break;
}
}
}
}
}
}
@ -274,7 +311,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
// Hide the widget if the cursor moves to the left of the initial position as the
// completions are no longer valid
// to do: get replacement length to be correct, readd this?
if (this._currentPromptInputState && this._currentPromptInputState.cursorIndex < this._leadingLineContent.length) {
if (this._currentPromptInputState && this._currentPromptInputState.cursorIndex <= this._leadingLineContent.length) {
this.hideSuggestWidget();
return;
}

Просмотреть файл

@ -14,6 +14,7 @@ export const enum TerminalSuggestSettingId {
SuggestOnTriggerCharacters = 'terminal.integrated.suggest.suggestOnTriggerCharacters',
RunOnEnter = 'terminal.integrated.suggest.runOnEnter',
BuiltinCompletions = 'terminal.integrated.suggest.builtinCompletions',
EnableExtensionCompletions = 'terminal.integrated.suggest.enableExtensionCompletions',
}
export const terminalSuggestConfigSection = 'terminal.integrated.suggest';
@ -27,14 +28,16 @@ export interface ITerminalSuggestConfiguration {
'pwshCode': boolean;
'pwshGit': boolean;
};
enableExtensionCompletions: boolean;
}
export const terminalSuggestConfiguration: IStringDictionary<IConfigurationPropertySchema> = {
[TerminalSuggestSettingId.Enabled]: {
restricted: true,
markdownDescription: localize('suggest.enabled', "Enables experimental terminal Intellisense suggestions for supported shells ({0}) when {1} is set to {2}.\n\nIf shell integration is installed manually, {3} needs to be set to {4} before calling the shell integration script.", 'PowerShell v7+', `\`#${TerminalSettingId.ShellIntegrationEnabled}#\``, '`true`', '`VSCODE_SUGGEST`', '`1`'),
markdownDescription: localize('suggest.enabled', "Enables experimental terminal Intellisense suggestions for supported shells ({0}) when {1} is set to {2}.\n\nIf shell integration is installed manually, {3} needs to be set to {4} before calling the shell integration script. \n\nFor zsh and bash completions, {5} will also need to be set.", 'PowerShell v7+, zsh, bash', `\`#${TerminalSettingId.ShellIntegrationEnabled}#\``, '`true`', '`VSCODE_SUGGEST`', '`1`', `\`#${TerminalSuggestSettingId.EnableExtensionCompletions}#\``),
type: 'boolean',
default: false,
tags: ['experimental'],
},
[TerminalSuggestSettingId.QuickSuggestions]: {
restricted: true,
@ -80,4 +83,11 @@ export const terminalSuggestConfiguration: IStringDictionary<IConfigurationPrope
pwshGit: true,
}
},
[TerminalSuggestSettingId.EnableExtensionCompletions]: {
restricted: true,
markdownDescription: localize('suggest.enableExtensionCompletions', "Controls whether extension completions are enabled."),
type: 'boolean',
default: false,
tags: ['experimental'],
},
};

Просмотреть файл

@ -34,7 +34,6 @@ import { events as windows11_pwsh_filename_same_case } from './recordings/window
import { events as windows11_pwsh_filename_same_case_change_forward_slash } from './recordings/windows11_pwsh_filename_same_case_change_forward_slash.js';
import { events as windows11_pwsh_getcontent_delete_ghost } from './recordings/windows11_pwsh_getcontent_delete_ghost.js';
import { events as windows11_pwsh_input_ls_complete_ls } from './recordings/windows11_pwsh_input_ls_complete_ls.js';
import { events as windows11_pwsh_namespace_change_prefix } from './recordings/windows11_pwsh_namespace_change_prefix.js';
import { events as windows11_pwsh_namespace_same_prefix } from './recordings/windows11_pwsh_namespace_same_prefix.js';
import { events as windows11_pwsh_single_char } from './recordings/windows11_pwsh_single_char.js';
import { events as windows11_pwsh_type_before_prompt } from './recordings/windows11_pwsh_type_before_prompt.js';
@ -60,7 +59,6 @@ const recordedTestCases: { name: string; events: RecordedSessionEvent[] }[] = [
{ name: 'windows11_pwsh_filename_same_case', events: windows11_pwsh_filename_same_case as any as RecordedSessionEvent[] },
{ name: 'windows11_pwsh_getcontent_delete_ghost', events: windows11_pwsh_getcontent_delete_ghost as any as RecordedSessionEvent[] },
{ name: 'windows11_pwsh_input_ls_complete_ls', events: windows11_pwsh_input_ls_complete_ls as any as RecordedSessionEvent[] },
{ name: 'windows11_pwsh_namespace_change_prefix', events: windows11_pwsh_namespace_change_prefix as any as RecordedSessionEvent[] },
{ name: 'windows11_pwsh_namespace_same_prefix', events: windows11_pwsh_namespace_same_prefix as any as RecordedSessionEvent[] },
{ name: 'windows11_pwsh_single_char', events: windows11_pwsh_single_char as any as RecordedSessionEvent[] },
{ name: 'windows11_pwsh_type_before_prompt', events: windows11_pwsh_type_before_prompt as any as RecordedSessionEvent[] },
@ -111,7 +109,8 @@ suite('Terminal Contrib Suggest Recordings', () => {
builtinCompletions: {
pwshCode: true,
pwshGit: true
}
},
enableExtensionCompletions: false
} satisfies ITerminalSuggestConfiguration
}
};

Просмотреть файл

@ -398,6 +398,11 @@ export const schema: IJSONSchema = {
body: 'onLanguageModelTool:${1:toolId}',
description: nls.localize('vscode.extension.activationEvents.onLanguageModelTool', 'An activation event emitted when the specified language model tool is invoked.'),
},
{
label: 'onTerminalCompletionsRequested',
body: 'onTerminalCompletionsRequested',
description: nls.localize('vscode.extension.activationEvents.onTerminalCompletionsRequested', 'An activation event emitted when terminal completions are requested.'),
},
{
label: '*',
description: nls.localize('vscode.extension.activationEvents.star', 'An activation event emitted on VS Code startup. To ensure a great end user experience, please use this activation event in your extension only when no other activation events combination works in your use-case.'),

Просмотреть файл

@ -114,9 +114,8 @@ export class SimpleCompletionModel {
}
// remember the word against which this item was
// scored
// scored. If word is undefined, then match against the empty string.
item.word = word;
if (wordLen === 0) {
// when there is nothing to score against, don't
// event try to do. Use a const rank and rely on
@ -165,13 +164,13 @@ export class SimpleCompletionModel {
} else {
// by default match `word` against the `label`
const match = scoreFn(word, wordLow, wordPos, item.completion.label, item.labelLow, 0, this._fuzzyScoreOptions);
if (!match) {
if (!match && word !== '') {
continue; // NO match
}
item.score = match;
// Use default sorting when word is empty
item.score = match || FuzzyScore.Default;
}
}
item.idx = i;
target.push(item);

83
src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,83 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'vscode' {
// https://github.com/microsoft/vscode/issues/226562
export interface TerminalCompletionProvider<T extends TerminalCompletionItem> {
id: string;
/**
* Provide completions for the given position and document.
* @param terminal The terminal for which completions are being provided.
* @param context Information about the terminal's current state.
* @param token A cancellation token.
* @return A list of completions.
*/
provideTerminalCompletions(terminal: Terminal, context: TerminalCompletionContext, token: CancellationToken): ProviderResult<T[]>;
}
export interface TerminalCompletionItem {
/**
* The label of the completion.
*/
label: string;
/**
* The index of the start of the range to replace.
*/
replacementIndex: number;
/**
* The length of the range to replace.
*/
replacementLength: number;
/**
* The completion's detail which appears on the right of the list.
*/
detail?: string;
/**
* The completion's kind. Note that this will map to an icon.
*/
kind?: TerminalCompletionItemKind;
}
/**
* Terminal item kinds.
*/
export enum TerminalCompletionItemKind {
File = 0,
Folder = 1,
Flag = 2,
Method = 3,
Argument = 4
}
export interface TerminalCompletionContext {
/**
* The complete terminal command line.
*/
commandLine: string;
/**
* The index of the
* cursor in the command line.
*/
cursorPosition: number;
}
export namespace window {
/**
* Register a completion provider for a certain type of terminal.
*
* @param provider The completion provider.
* @returns A {@link Disposable} that unregisters this provider when being disposed.
*/
export function registerTerminalCompletionProvider<T extends TerminalCompletionItem>(provider: TerminalCompletionProvider<T>, ...triggerCharacters: string[]): Disposable;
}
}