From 927cbbba3891dfc60240b109088c716ebdf7c869 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 17 Sep 2024 14:31:21 -0700 Subject: [PATCH] pass in shellType, commandLine --- .vscode/launch.json | 20 +++-- extensions/terminal-suggest/src/npmMain.ts | 6 +- .../api/browser/mainThreadTerminalService.ts | 42 ++++++++- .../workbench/api/common/extHost.api.impl.ts | 4 +- .../workbench/api/common/extHost.protocol.ts | 5 ++ .../api/common/extHostTerminalService.ts | 61 ++++++++++++- src/vs/workbench/api/common/extHostTypes.ts | 37 ++------ .../browser/terminal.suggest.contribution.ts | 4 + .../browser/terminalSuggestionService.ts | 87 +++++++++++++++++++ ...e.proposed.terminalCompletionProvider.d.ts | 2 +- 10 files changed, 219 insertions(+), 49 deletions(-) create mode 100644 src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestionService.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index b2e25927a8a..453754f8a48 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,13 @@ { "version": "0.1.0", "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch in Web Port 3000", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}" + }, { "type": "node", "request": "launch", @@ -72,7 +79,7 @@ "${workspaceFolder}/out/**/*.js" ], "presentation": { - "hidden": true, + "hidden": true } }, { @@ -152,7 +159,6 @@ "name": "VS Code API Tests (single folder)", "runtimeExecutable": "${execPath}", "args": [ - // "${workspaceFolder}", // Uncomment for running out of sources. "${workspaceFolder}/extensions/vscode-api-tests/testWorkspace", "--extensionDevelopmentPath=${workspaceFolder}/extensions/vscode-api-tests", "--extensionTestsPath=${workspaceFolder}/extensions/vscode-api-tests/out/singlefolder-tests", @@ -240,7 +246,6 @@ "--inspect-brk=5875", "--no-cached-data", "--crash-reporter-directory=${workspaceFolder}/.profile-oss/crashes", - // for general runtime freezes: https://github.com/microsoft/vscode/issues/127861#issuecomment-904144910 "--disable-features=CalculateNativeWinOcclusion", "--disable-extension=vscode.vscode-api-tests" ], @@ -255,9 +260,8 @@ ], "browserLaunchLocation": "workspace", "presentation": { - "hidden": true, + "hidden": true }, - // This is read by the vscode-diagnostic-tools extension "vscode-diagnostic-tools.debuggerScripts": [ "${workspaceFolder}/scripts/hot-reload-injected-script.js" ] @@ -268,7 +272,7 @@ "name": "VS Code Server (Web)", "runtimeExecutable": "${workspaceFolder}/scripts/code-server.sh", "windows": { - "runtimeExecutable": "${workspaceFolder}/scripts/code-server.bat", + "runtimeExecutable": "${workspaceFolder}/scripts/code-server.bat" }, "outFiles": [ "${workspaceFolder}/out/**/*.js" @@ -286,11 +290,11 @@ "enableContentValidation": false, "runtimeExecutable": "${workspaceFolder}/scripts/code.sh", "windows": { - "runtimeExecutable": "${workspaceFolder}/scripts/code.bat", + "runtimeExecutable": "${workspaceFolder}/scripts/code.bat" }, "runtimeArgs": [ "--inspect-brk=5875", - "--no-cached-data", + "--no-cached-data" ], "outFiles": [ "${workspaceFolder}/out/**/*.js" diff --git a/extensions/terminal-suggest/src/npmMain.ts b/extensions/terminal-suggest/src/npmMain.ts index a8e7a6ed98e..55f21b0d030 100644 --- a/extensions/terminal-suggest/src/npmMain.ts +++ b/extensions/terminal-suggest/src/npmMain.ts @@ -9,14 +9,14 @@ import * as vscode from 'vscode'; export async function activate(context: vscode.ExtensionContext): Promise { (vscode as any).registerTerminalCompletionProvider({ - async provideTerminalCompletions(terminal: vscode.Terminal, context: any, token: vscode.CancellationToken) { + async provideTerminalCompletions(terminal: vscode.Terminal, terminalContext: { shellType: string; commandLine: string }, token: vscode.CancellationToken) { if (token.isCancellationRequested) { return; } - if (context.shellType === 'pwsh') { + if (terminalContext.shellType === 'pwsh') { return; } - const commandLine = context.commandLine; + const commandLine = terminalContext.commandLine; if (commandLine.startsWith('cd')) { return [ { diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index b184d49085b..7d2a5f6518f 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -24,6 +24,8 @@ 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 { ITerminalCompletion, ITerminalSuggestionService } from '../../contrib/terminalContrib/suggest/browser/terminalSuggestionService.js'; +import { MarkdownString } from '../../../base/common/htmlContent.js'; @extHostNamedCustomer(MainContext.MainThreadTerminalService) export class MainThreadTerminalService implements MainThreadTerminalServiceShape { @@ -39,6 +41,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape private readonly _extHostTerminals = new Map>(); private readonly _terminalProcessProxies = new Map(); private readonly _profileProviders = new Map(); + private readonly _completionProviders = new Map(); private readonly _quickFixProviders = new Map(); private readonly _dataEventTracker = new MutableDisposable(); private readonly _sendCommandEventListener = new MutableDisposable(); @@ -65,7 +68,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, + @ITerminalSuggestionService private readonly _terminalSuggestionService: ITerminalSuggestionService, ) { this._proxy = _extHostContext.getProxy(ExtHostContext.ExtHostTerminalService); @@ -264,6 +268,41 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._terminalService.registerProcessSupport(isSupported); } + public $registerCompletionProvider(id: string, extensionIdentifier: string): void { + // Proxy completion provider requests through the extension host + this._completionProviders.set(id, this._terminalSuggestionService.registerTerminalSuggestionProvider(extensionIdentifier, id, { + provideSuggestions: async (terminal) => { + const shellType = terminal.shellType; + if (!shellType) { + return; + } + const commandLine = terminal.capabilities.get(TerminalCapability.CommandDetection)?.currentCommand?.command; + if (commandLine === undefined) { + return; + } + const suggestions = await this._proxy.$provideTerminalSuggestions(id, { shellType, commandLine }); + const converted: ITerminalCompletion[] = []; + if (!suggestions?.length) { + return; + } + for (const s of suggestions) { + converted.push({ + label: s.label, + kind: s.kind, + detail: s.detail, + documentation: s.documentation ? new MarkdownString(s.documentation.toString()) : undefined, + }); + } + return converted; + } + })); + } + + 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 +521,4 @@ function parseQuickFix(id: string, source: string, fix: TerminalQuickFix): ITerm } return { id, type, source, ...fix }; } + diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 16c42d3dfaf..40f64cbb705 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1660,10 +1660,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TerminalProfile: extHostTypes.TerminalProfile, TerminalExitReason: extHostTypes.TerminalExitReason, TerminalShellExecutionCommandLineConfidence: extHostTypes.TerminalShellExecutionCommandLineConfidence, - TerminalCompletionItem: extHostTypes.TerminalCompletionItem, - TerminalCompletionContext: extHostTypes.TerminalCompletionContext, - TerminalCompletionItemKind: extHostTypes.TerminalCompletionItemKind, TerminalCompletion: extHostTypes.TerminalCompletion, + TerminalCompletionItemKind: extHostTypes.TerminalCompletionItemKind, TextDocumentSaveReason: extHostTypes.TextDocumentSaveReason, TextEdit: extHostTypes.TextEdit, SnippetTextEdit: extHostTypes.SnippetTextEdit, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 0928dc67f2b..05f06f77747 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -83,6 +83,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 { ITerminalCompletion } from './extHostTerminalService.js'; import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js'; import * as tasks from './shared/tasks.js'; @@ -536,6 +537,7 @@ export interface TerminalLaunchConfig { isTransient?: boolean; } + export interface MainThreadTerminalServiceShape extends IDisposable { $createTerminal(extHostTerminalId: string, config: TerminalLaunchConfig): Promise; $dispose(id: ExtHostTerminalIdentifier): void; @@ -545,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): 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; @@ -2383,6 +2387,7 @@ export interface ExtHostTerminalServiceShape { $acceptDefaultProfile(profile: ITerminalProfile, automationProfile: ITerminalProfile): void; $createContributedProfileTerminal(id: string, options: ICreateContributedTerminalProfileOptions): Promise; $provideTerminalQuickFixes(id: string, matchResult: TerminalCommandMatchResultDto, token: CancellationToken): Promise | undefined>; + $provideTerminalSuggestions(id: string, options: { shellType: string; commandLine: string }): Promise; } export interface ExtHostTerminalShellIntegrationShape { diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 76ce2cdda00..1499cb397e9 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -10,7 +10,7 @@ import { createDecorator } from '../../../platform/instantiation/common/instanti 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, CompletionItemLabel, TerminalCompletionItemKind } from './extHostTypes.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { localize } from '../../../nls.js'; import { NotSupportedError } from '../../../base/common/errors.js'; @@ -739,10 +739,10 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I throw new Error(`Terminal completion provider "${provider.id}" already registered`); } this._completionProviders.set(provider.id, provider); - this._proxy.$registerProfileProvider(provider.id, extension.identifier.value); + this._proxy.$registerCompletionProvider(provider.id, extension.identifier.value); return new VSCodeDisposable(() => { this._completionProviders.delete(provider.id); - this._proxy.$unregisterProfileProvider(provider.id); + this._proxy.$unregisterCompletionProvider(provider.id); }); } @@ -771,6 +771,33 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I }); } + public async $provideTerminalSuggestions(id: string, options: { shellType: string; commandLine: string }): Promise { + const token = new CancellationTokenSource().token; + if (token.isCancellationRequested || !this.activeTerminal) { + return []; + } + const provider = this._completionProviders.get(id); + if (!provider) { + return []; + } + // todo + const completions = await provider.provideTerminalCompletions(this.activeTerminal, options, token); + if (completions === null || completions === undefined || (Array.isArray(completions) && completions.length === 0)) { + return []; + } + + const result: ITerminalCompletion[] = []; + for (const completion of completions) { + result.push({ + label: completion.label, + detail: completion.detail, + documentation: completion.documentation, + kind: completion.kind, + }); + } + return result; + } + public async $provideTerminalQuickFixes(id: string, matchResult: TerminalCommandMatchResultDto): Promise<(ITerminalQuickFixTerminalCommandDto | ITerminalQuickFixOpenerDto | ICommandDto)[] | ITerminalQuickFixTerminalCommandDto | ITerminalQuickFixOpenerDto | ICommandDto | undefined> { const token = new CancellationTokenSource().token; if (token.isCancellationRequested) { @@ -1238,3 +1265,31 @@ function convertMutator(mutator: IEnvironmentVariableMutator): vscode.Environmen delete (newMutator as any).variable; return newMutator as vscode.EnvironmentVariableMutator; } + + +export interface ITerminalCompletion { + + /** + * The label of this completion item. By default + * this is also the text that is inserted when selecting + * this completion. + */ + label: string | CompletionItemLabel; + + /** + * The kind of this completion item. Based on the kind, + * an icon is chosen. + */ + kind?: TerminalCompletionItemKind; + + /** + * A human-readable string with additional information + * about this item. + */ + detail?: string; + + /** + * A human-readable string that represents a doc-comment. + */ + documentation?: string | vscode.MarkdownString; +} diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 14624ceee6d..40aa5afc7d9 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2108,31 +2108,13 @@ export class TerminalProfile implements vscode.TerminalProfile { } } -export class TerminalCompletion { - - /** - * The label of this completion item. By default - * this is also the text that is inserted when selecting - * this completion. - */ - label: string | CompletionItemLabel; - - /** - * The kind of this completion item. Based on the kind, - * an icon is chosen. - */ - kind?: TerminalCompletionItemKind; - - /** - * A human-readable string with additional information - * about this item. - */ - detail?: string; - - /** - * A human-readable string that represents a doc-comment. - */ - documentation?: string | MarkdownString; +export class TerminalCompletion implements vscode.TerminalCompletion { + label: string | vscode.CompletionItemLabel = ''; + kind?: vscode.TerminalCompletionItemKind | undefined; + detail?: string | undefined; + documentation?: string | vscode.MarkdownString | undefined; + constructor() { + } } export enum TerminalCompletionItemKind { @@ -2141,11 +2123,6 @@ export enum TerminalCompletionItemKind { Flag = 2, } -export interface TerminalCompletionContext { - shellType: string; - commandLine: string; -} - export enum TaskRevealKind { Always = 1, diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index ec989b191a0..62be3c9bfe4 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -28,11 +28,15 @@ import { parseCompletionsFromShell, SuggestAddon, VSCodeSuggestOscPt, type Compr import { TerminalSuggestCommandId } from '../common/terminal.suggest.js'; import { terminalSuggestConfigSection, TerminalSuggestSettingId, type ITerminalSuggestConfiguration } from '../common/terminalSuggestConfiguration.js'; import { SimpleCompletionItem } from '../../../../services/suggest/browser/simpleCompletionItem.js'; +import { ITerminalSuggestionService, TerminalSuggestionService } from './terminalSuggestionService.js'; +import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js'; const enum Constants { CachedPwshCommandsStorageKey = 'terminal.suggest.pwshCommands' } +registerSingleton(ITerminalSuggestionService, TerminalSuggestionService, InstantiationType.Delayed); + // #region Terminal Contributions class TerminalSuggestContribution extends DisposableStore implements ITerminalContribution { diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestionService.ts new file mode 100644 index 00000000000..04cf8183e03 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestionService.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { CompletionItemLabel } from '../../../../../editor/common/languages.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ITerminalInstance } from '../../../terminal/browser/terminal.js'; + +export const ITerminalSuggestionService = createDecorator('terminalSuggestionService'); + + +export interface ITerminalCompletion { + + /** + * The label of this completion item. By default + * this is also the text that is inserted when selecting + * this completion. + */ + label: string | CompletionItemLabel; + + /** + * The kind of this completion item. Based on the kind, + * an icon is chosen. + */ + kind?: TerminalCompletionItemKind; + + /** + * A human-readable string with additional information + * about this item. + */ + detail?: string; + + /** + * A human-readable string that represents a doc-comment. + */ + documentation?: string | MarkdownString; +} + + +export enum TerminalCompletionItemKind { + File = 0, + Folder = 1, + Flag = 2, +} + +export interface ITerminalSuggestionProvider { + provideSuggestions(terminal: ITerminalInstance): Promise; +} + +export interface ITerminalSuggestionService { + _serviceBrand: undefined; + registerTerminalSuggestionProvider(extensionIdentifier: string, id: string, provider: ITerminalSuggestionProvider): IDisposable; +} + +export class TerminalSuggestionService extends Disposable implements ITerminalSuggestionService { + declare _serviceBrand: undefined; + private readonly _providers: Map> = new Map(); + + constructor( + ) { + super(); + } + registerTerminalSuggestionProvider(extensionIdentifier: string, id: string, provider: ITerminalSuggestionProvider): IDisposable { + let extMap = this._providers.get(extensionIdentifier); + if (!extMap) { + extMap = new Map(); + this._providers.set(extensionIdentifier, extMap); + } + extMap.set(id, provider); + return toDisposable(() => this._providers.delete(id)); + } + + async provideSuggestions(terminal: ITerminalInstance): Promise { + const result: ITerminalCompletion[] = []; + for (const providers of this._providers.values()) { + for (const provider of providers.values()) { + const suggestions = await provider.provideSuggestions(terminal); + if (suggestions) { + result.push(...suggestions); + } + } + } + return result.length > 0 ? result : undefined; + } +} diff --git a/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts b/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts index 61847dd6973..3407adb872a 100644 --- a/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts @@ -16,7 +16,7 @@ declare module 'vscode' { * @param token A cancellation token. * @return A list of completions. */ - provideTerminalCompletions(terminal: Terminal, context: TerminalCompletionContext, token: CancellationToken): ProviderResult>; + provideTerminalCompletions(terminal: Terminal, context: TerminalCompletionContext, token: CancellationToken): ProviderResult>; } export class TerminalCompletion {