From 8eb7fac5658846e35a0399dc65e9a0580d4e4ed7 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Sun, 24 Nov 2024 10:38:19 -0500 Subject: [PATCH] add tests for terminal suggest widget, fix some bugs (#234445) --- .vscode-test.js | 5 ++ build/gulpfile.extensions.js | 1 + extensions/terminal-suggest/package.json | 4 +- .../src/terminalSuggestMain.test.ts | 69 +++++++++++++++++++ .../src/terminalSuggestMain.ts | 41 +++++++---- extensions/terminal-suggest/tsconfig.json | 1 + scripts/test-integration.sh | 6 ++ 7 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 extensions/terminal-suggest/src/terminalSuggestMain.test.ts diff --git a/.vscode-test.js b/.vscode-test.js index ce539a6572157..917413ede2acc 100644 --- a/.vscode-test.js +++ b/.vscode-test.js @@ -42,6 +42,11 @@ const extensions = [ workspaceFolder: `extensions/vscode-colorize-tests/test`, mocha: { timeout: 60_000 } }, + { + label: 'terminal-suggest', + workspaceFolder: path.join(os.tmpdir(), `terminal-suggest-${Math.floor(Math.random() * 100000)}`), + mocha: { timeout: 60_000 } + }, { label: 'vscode-colorize-perf-tests', workspaceFolder: `extensions/vscode-colorize-perf-tests/test`, diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index 13f27d6db4774..f05738faa620a 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -52,6 +52,7 @@ const compilations = [ 'extensions/markdown-math/tsconfig.json', 'extensions/media-preview/tsconfig.json', 'extensions/merge-conflict/tsconfig.json', + 'extensions/terminal-suggest/tsconfig.json', 'extensions/microsoft-authentication/tsconfig.json', 'extensions/notebook-renderers/tsconfig.json', 'extensions/npm/tsconfig.json', diff --git a/extensions/terminal-suggest/package.json b/extensions/terminal-suggest/package.json index aa4ca1909f51d..611ef00ed4ceb 100644 --- a/extensions/terminal-suggest/package.json +++ b/extensions/terminal-suggest/package.json @@ -17,8 +17,8 @@ "terminalCompletionProvider" ], "scripts": { - "compile": "npx gulp compile-extension:npm", - "watch": "npx gulp watch-extension:npm" + "compile": "npx gulp compile-extension:terminal-suggest", + "watch": "npx gulp watch-extension:terminal-suggest" }, "main": "./out/terminalSuggestMain", diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/terminalSuggestMain.test.ts new file mode 100644 index 0000000000000..dfc3cd58e2217 --- /dev/null +++ b/extensions/terminal-suggest/src/terminalSuggestMain.test.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepStrictEqual, strictEqual } from 'assert'; +import 'mocha'; +import { availableSpecs, getCompletionItemsFromSpecs } from './terminalSuggestMain'; + +suite('Terminal Suggest', () => { + + const availableCommands = ['cd', 'code', 'code-insiders']; + const codeOptions = ['-', '--add', '--category', '--diff', '--disable-extension', '--disable-extensions', '--disable-gpu', '--enable-proposed-api', '--extensions-dir', '--goto', '--help', '--inspect-brk-extensions', '--inspect-extensions', '--install-extension', '--list-extensions', '--locale', '--log', '--max-memory', '--merge', '--new-window', '--pre-release', '--prof-startup', '--profile', '--reuse-window', '--show-versions', '--status', '--sync', '--telemetry', '--uninstall-extension', '--user-data-dir', '--verbose', '--version', '--wait', '-a', '-d', '-g', '-h', '-m', '-n', '-r', '-s', '-v', '-w']; + + suite('Cursor at the end of the command line', () => { + createTestCase('|', availableCommands, 'neither', availableSpecs); + createTestCase('c|', availableCommands, 'neither', availableSpecs); + createTestCase('ls && c|', availableCommands, 'neither', availableSpecs); + createTestCase('cd |', ['~', '-'], 'folders', availableSpecs); + createTestCase('code|', ['code-insiders'], 'neither', availableSpecs); + createTestCase('code-insiders|', [], 'neither', availableSpecs); + createTestCase('code |', codeOptions, 'neither', availableSpecs); + createTestCase('code --locale |', ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW'], 'neither', availableSpecs); + createTestCase('code --diff |', [], 'files', availableSpecs); + createTestCase('code -di|', codeOptions.filter(o => o.startsWith('di')), 'neither', availableSpecs); + createTestCase('code --diff ./file1 |', [], 'files', availableSpecs); + createTestCase('code --merge |', [], 'files', availableSpecs); + createTestCase('code --merge ./file1 ./file2 |', [], 'files', availableSpecs); + createTestCase('code --merge ./file1 ./file2 ./base |', [], 'files', availableSpecs); + createTestCase('code --goto |', [], 'files', availableSpecs); + createTestCase('code --user-data-dir |', [], 'folders', availableSpecs); + createTestCase('code --profile |', [], 'neither', availableSpecs); + createTestCase('code --install-extension |', [], 'neither', availableSpecs); + createTestCase('code --uninstall-extension |', [], 'neither', availableSpecs); + createTestCase('code --log |', ['critical', 'error', 'warn', 'info', 'debug', 'trace', 'off'], 'neither', availableSpecs); + createTestCase('code --sync |', ['on', 'off'], 'neither', availableSpecs); + createTestCase('code --extensions-dir |', [], 'folders', availableSpecs); + createTestCase('code --list-extensions |', codeOptions, 'neither', availableSpecs); + createTestCase('code --show-versions |', codeOptions, 'neither', availableSpecs); + createTestCase('code --category |', ['azure', 'data science', 'debuggers', 'extension packs', 'education', 'formatters', 'keymaps', 'language packs', 'linters', 'machine learning', 'notebooks', 'programming languages', 'scm providers', 'snippets', 'testing', 'themes', 'visualization', 'other'], 'neither', availableSpecs); + createTestCase('code --category a|', ['azure'], 'neither', availableSpecs); + createTestCase('code-insiders --list-extensions |', codeOptions, 'neither', availableSpecs); + createTestCase('code-insiders --show-versions |', codeOptions, 'neither', availableSpecs); + createTestCase('code-insiders --category |', ['azure', 'data science', 'debuggers', 'extension packs', 'education', 'formatters', 'keymaps', 'language packs', 'linters', 'machine learning', 'notebooks', 'programming languages', 'scm providers', 'snippets', 'testing', 'themes', 'visualization', 'other'], 'neither', availableSpecs); + createTestCase('code-insiders --category a|', ['azure'], 'neither', availableSpecs); + createTestCase('code-insiders --category azure |', [], 'neither', availableSpecs); + }); + suite('Cursor not at the end of the line', () => { + createTestCase('code | --locale', codeOptions, 'neither', availableSpecs); + createTestCase('code --locale | && ls', ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW'], 'neither', availableSpecs); + createTestCase('code-insiders | --locale', codeOptions, 'neither', availableSpecs); + createTestCase('code-insiders --locale | && ls', ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW'], 'neither', availableSpecs); + }); + + function createTestCase(commandLineWithCursor: string, expectedCompletionLabels: string[], resourcesRequested: 'files' | 'folders' | 'both' | 'neither', availableSpecs: Fig.Spec[]): void { + const commandLine = commandLineWithCursor.split('|')[0]; + const cursorPosition = commandLineWithCursor.indexOf('|'); + const prefix = commandLine.slice(0, cursorPosition).split(' ').pop() || ''; + const filesRequested = resourcesRequested === 'files' || resourcesRequested === 'both'; + const foldersRequested = resourcesRequested === 'folders' || resourcesRequested === 'both'; + test(commandLineWithCursor, function () { + const result = getCompletionItemsFromSpecs(availableSpecs, { commandLine, cursorPosition }, availableCommands, prefix); + deepStrictEqual(result.items.map(i => i.label).sort(), expectedCompletionLabels.sort()); + strictEqual(result.filesRequested, filesRequested); + strictEqual(result.foldersRequested, foldersRequested); + }); + } +}); + diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 1ff88ed9eb6bf..a6e03d65dcd65 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -14,6 +14,8 @@ import cdSpec from './completions/cd'; let cachedAvailableCommands: Set | undefined; let cachedBuiltinCommands: Map | undefined; +export const availableSpecs = [codeCompletionSpec, codeInsidersCompletionSpec, cdSpec]; + function getBuiltinCommands(shell: string): string[] | undefined { try { const shellType = path.basename(shell); @@ -89,8 +91,7 @@ export async function activate(context: vscode.ExtensionContext) { const items: vscode.TerminalCompletionItem[] = []; const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition); - const specs = [codeCompletionSpec, codeInsidersCompletionSpec, cdSpec]; - const specCompletions = await getCompletionItemsFromSpecs(specs, terminalContext, new Set(commands), prefix, token); + const specCompletions = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, token); items.push(...specCompletions.items); let filesRequested = specCompletions.filesRequested; @@ -98,7 +99,7 @@ export async function activate(context: vscode.ExtensionContext) { if (!specCompletions.specificSuggestionsProvided) { for (const command of commands) { - if (command.startsWith(prefix)) { + if (command.startsWith(prefix) && !items.find(item => item.label === command)) { items.push(createCompletionItem(terminalContext.cursorPosition, prefix, command)); } } @@ -214,7 +215,7 @@ export function asArray(x: T | T[]): T[] { return Array.isArray(x) ? x : [x]; } -function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalContext: { commandLine: string; cursorPosition: number }, availableCommands: Set, prefix: string, token: vscode.CancellationToken): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; specificSuggestionsProvided: boolean } { +export function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalContext: { commandLine: string; cursorPosition: number }, availableCommands: string[], prefix: string, token?: vscode.CancellationToken): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; specificSuggestionsProvided: boolean } { const items: vscode.TerminalCompletionItem[] = []; let filesRequested = false; let foldersRequested = false; @@ -224,7 +225,21 @@ function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalContext: { comma continue; } for (const specLabel of specLabels) { - if (!availableCommands.has(specLabel) || token.isCancellationRequested || !terminalContext.commandLine.startsWith(specLabel)) { + if (!availableCommands.includes(specLabel) || (token && token?.isCancellationRequested)) { + continue; + } + // + if ( + // If the prompt is empty + !terminalContext.commandLine + // or the prefix matches the command and the prefix is not equal to the command + || !!prefix && specLabel.startsWith(prefix) && specLabel !== prefix + ) { + // push it to the completion items + items.push(createCompletionItem(terminalContext.cursorPosition, prefix, specLabel)); + } + if (!terminalContext.commandLine.startsWith(specLabel)) { + // the spec label is not the first word in the command line, so do not provide options or args continue; } const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1); @@ -235,7 +250,7 @@ function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalContext: { comma continue; } for (const optionLabel of optionLabels) { - if (optionLabel.startsWith(prefix) || (prefix.length > specLabel.length && prefix.trim() === specLabel)) { + if (!items.find(i => i.label === optionLabel) && optionLabel.startsWith(prefix) || (prefix.length > specLabel.length && prefix.trim() === specLabel)) { items.push(createCompletionItem(terminalContext.cursorPosition, prefix, optionLabel, option.description, false, vscode.TerminalCompletionItemKind.Flag)); } const expectedText = `${specLabel} ${optionLabel} `; @@ -248,13 +263,8 @@ function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalContext: { comma if (!argsCompletions) { continue; } - if (argsCompletions.specificSuggestionsProvided) { - // prevents the list from containing a bunch of other stuff - return argsCompletions; - } - items.push(...argsCompletions.items); - filesRequested = filesRequested || argsCompletions.filesRequested; - foldersRequested = foldersRequested || argsCompletions.foldersRequested; + // return early so that we don't show the other completions + return argsCompletions; } } } @@ -307,7 +317,10 @@ function getCompletionItemsFromArgs(args: Fig.SingleOrArray | undefined } for (const suggestionLabel of suggestionLabels) { - if (suggestionLabel && suggestionLabel.startsWith(currentPrefix.trim())) { + if (items.find(i => i.label === suggestionLabel)) { + continue; + } + if (suggestionLabel && suggestionLabel.startsWith(currentPrefix.trim()) && suggestionLabel !== currentPrefix.trim()) { const hasSpaceBeforeCursor = terminalContext.commandLine[terminalContext.cursorPosition - 1] === ' '; // prefix will be '' if there is a space before the cursor const description = typeof suggestion !== 'string' ? suggestion.description : ''; diff --git a/extensions/terminal-suggest/tsconfig.json b/extensions/terminal-suggest/tsconfig.json index 0fc7ab5f53c92..151a29616bb23 100644 --- a/extensions/terminal-suggest/tsconfig.json +++ b/extensions/terminal-suggest/tsconfig.json @@ -14,6 +14,7 @@ }, "include": [ "src/**/*", + "src/completions/index.d.ts", "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts" ] diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 35b358ae168f3..8900648030899 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -70,6 +70,12 @@ echo npm run test-extension -- -l vscode-colorize-tests kill_app +echo +echo "### Terminal Suggest tests" +echo +npm run test-extension -- -l terminal-suggest --enable-proposed-api=vscode.vscode-api-tests +kill_app + echo echo "### TypeScript tests" echo