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:
Коммит
e57cc506b2
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -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;
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче