Repository: microsoft/vscode (TypeScript)

Issue:
Fig completions aren't working on Windows
1. Type `code --locale ` + `ctrl+space`. You should see all locales, but do not 🐛 
2. Type `cd ` + `ctrl+space`. You should see `~`, `-`, and all folders, but do not 🐛 

cc @Tyriar 


Discussion:
The `code-insiders --locale` case still doesn't work for me.
![code-insiders --locale only shows folder names after](https://github.com/user-attachments/assets/abd23473-c0ad-48c9-8ce2-6a32c0769da0)
I see `locales` but also file/folder completions, which is unexpected.

<img width="645" alt="Image" src="https://github.com/user-attachments/assets/af432832-9834-40d5-909e-22f5867f1fe1" />
<img width="577" alt="Image" src="https://github.com/user-attachments/assets/f4fc22ab-ef8b-4262-972b-0bb04edeb16f" />
Reopening as it seems this is broken on windows
2 is fixed, 1 still happens on Windows. I think it's related to the label validation and `code.cmd` being in the path



Patch:
diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts
index 685ed84100564..71bcfba77ca8e 100644
--- a/extensions/terminal-suggest/src/terminalSuggestMain.ts
+++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts
@@ -239,9 +239,15 @@ export async function getCompletionItemsFromSpecs(
 	let filesRequested = false;
 	let foldersRequested = false;
 
-	const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1);
-	// TODO: Normalize precedingText to ignore file extensions on Windows
-	// precedingText = precedingText.replace('.cmd', '');
+	let precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1);
+	if (isWindows) {
+		const spaceIndex = precedingText.indexOf(' ');
+		const commandEndIndex = spaceIndex === -1 ? precedingText.length : spaceIndex;
+		const lastDotIndex = precedingText.lastIndexOf('.', commandEndIndex);
+		if (lastDotIndex > 0) { // Don't treat dotfiles as extensions
+			precedingText = precedingText.substring(0, lastDotIndex) + precedingText.substring(spaceIndex);
+		}
+	}
 
 	let specificItemsProvided = false;
 	for (const spec of specs) {
@@ -252,9 +258,9 @@ export async function getCompletionItemsFromSpecs(
 		}
 
 		for (const specLabel of specLabels) {
-			const availableCommand = availableCommands.find(command => specLabel === command.label);
-			// TODO: Normalize commands to ignore file extensions on Windows https://github.com/microsoft/vscode/issues/237598
-			// const availableCommand = availableCommands.find(command => command.label.startsWith(specLabel));
+			const availableCommand = (isWindows
+				? availableCommands.find(command => command.label.match(new RegExp(`${specLabel}(\\.[^ ]+)?$`)))
+				: availableCommands.find(command => command.label.startsWith(specLabel)));
 			if (!availableCommand || (token && token.isCancellationRequested)) {
 				continue;
 			}
@@ -267,11 +273,14 @@ export async function getCompletionItemsFromSpecs(
 				continue;
 			}
 
-			// TODO: Normalize commands to ignore file extensions on Windows https://github.com/microsoft/vscode/issues/237598
-			// const commandAndAliases = availableCommands.filter(command => specLabel === (command.definitionCommand ?? command.label).replace('.cmd', ''));
-			// if (!commandAndAliases.some(e => terminalContext.commandLine.startsWith(`${e.label} `) || terminalContext.commandLine.startsWith(`${e.label}.cmd `))) {
-			const commandAndAliases = availableCommands.filter(command => specLabel === (command.definitionCommand ?? command.label));
-			if (!commandAndAliases.some(e => terminalContext.commandLine.startsWith(`${e.label} `))) {
+			const commandAndAliases = (isWindows
+				? availableCommands.filter(command => specLabel === removeAnyFileExtension(command.definitionCommand ?? command.label))
+				: availableCommands.filter(command => specLabel === (command.definitionCommand ?? command.label)));
+			if (
+				!(isWindows
+					? commandAndAliases.some(e => precedingText.startsWith(`${removeAnyFileExtension(e.label)} `))
+					: commandAndAliases.some(e => precedingText.startsWith(`${e.label} `)))
+			) {
 				// the spec label is not the first word in the command line, so do not provide options or args
 				continue;
 			}
@@ -469,3 +478,6 @@ function getShell(shellType: TerminalShellType): string | undefined {
 	}
 }
 
+function removeAnyFileExtension(label: string): string {
+	return label.replace(/\.[a-zA-Z0-9!#\$%&'\(\)\-@\^_`{}~\+,;=\[\]]+$/, '');
+}


Test patch:
diff --git a/extensions/terminal-suggest/src/test/completions/code.test.ts b/extensions/terminal-suggest/src/test/completions/code.test.ts
index 50ad0458a2242..ecbaef0c130bd 100644
--- a/extensions/terminal-suggest/src/test/completions/code.test.ts
+++ b/extensions/terminal-suggest/src/test/completions/code.test.ts
@@ -7,8 +7,9 @@ import 'mocha';
 import codeCompletionSpec from '../../completions/code';
 import { testPaths, type ISuiteSpec, type ITestSpec } from '../helpers';
 
+export const codeSpecOptions = ['-', '--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'];
+
 export function createCodeTestSpecs(executable: string): ITestSpec[] {
-	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'];
 	const localeOptions = ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW'];
 	const categoryOptions = ['azure', 'data science', 'debuggers', 'extension packs', 'education', 'formatters', 'keymaps', 'language packs', 'linters', 'machine learning', 'notebooks', 'programming languages', 'scm providers', 'snippets', 'testing', 'themes', 'visualization', 'other'];
 	const logOptions = ['critical', 'error', 'warn', 'info', 'debug', 'trace', 'off'];
@@ -25,7 +26,7 @@ export function createCodeTestSpecs(executable: string): ITestSpec[] {
 		...typingTests,
 
 		// Basic arguments
-		{ input: `${executable} |`, expectedCompletions: codeOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
+		{ input: `${executable} |`, expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
 		{ input: `${executable} --locale |`, expectedCompletions: localeOptions },
 		{ input: `${executable} --diff |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } },
 		{ input: `${executable} --diff ./file1 |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } },
@@ -40,13 +41,13 @@ export function createCodeTestSpecs(executable: string): ITestSpec[] {
 		{ input: `${executable} --log |`, expectedCompletions: logOptions },
 		{ input: `${executable} --sync |`, expectedCompletions: syncOptions },
 		{ input: `${executable} --extensions-dir |`, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
-		{ input: `${executable} --list-extensions |`, expectedCompletions: codeOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
-		{ input: `${executable} --show-versions |`, expectedCompletions: codeOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
+		{ input: `${executable} --list-extensions |`, expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
+		{ input: `${executable} --show-versions |`, expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
 		{ input: `${executable} --category |`, expectedCompletions: categoryOptions },
 		{ input: `${executable} --category a|`, expectedCompletions: categoryOptions.filter(c => c.startsWith('a')) },
 
 		// Middle of command
-		{ input: `${executable} | --locale`, expectedCompletions: codeOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
+		{ input: `${executable} | --locale`, expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
 	];
 }
 
diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts
index eca864c9727cd..189a022101b73 100644
--- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts
+++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts
@@ -9,7 +9,7 @@ import { basename } from 'path';
 import { asArray, getCompletionItemsFromSpecs } from '../terminalSuggestMain';
 import { getTokenType } from '../tokens';
 import { cdTestSuiteSpec as cdTestSuite } from './completions/cd.test';
-import { codeTestSuite } from './completions/code.test';
+import { codeSpecOptions, codeTestSuite } from './completions/code.test';
 import { testPaths, type ISuiteSpec } from './helpers';
 import { codeInsidersTestSuite } from './completions/code-insiders.test';
 import { lsTestSuiteSpec } from './completions/upstream/ls.test';
@@ -18,6 +18,8 @@ import { mkdirTestSuiteSpec } from './completions/upstream/mkdir.test';
 import { rmTestSuiteSpec } from './completions/upstream/rm.test';
 import { rmdirTestSuiteSpec } from './completions/upstream/rmdir.test';
 import { touchTestSuiteSpec } from './completions/upstream/touch.test';
+import { osIsWindows } from '../helpers/os';
+import codeCompletionSpec from '../completions/code';
 
 const testSpecs2: ISuiteSpec[] = [
 	{
@@ -46,6 +48,26 @@ const testSpecs2: ISuiteSpec[] = [
 	touchTestSuiteSpec,
 ];
 
+if (osIsWindows()) {
+	testSpecs2.push({
+		name: 'Handle options extensions on Windows',
+		completionSpecs: [codeCompletionSpec],
+		availableCommands: [
+			'code.bat',
+			'code.cmd',
+			'code.exe',
+			'code.anything',
+		],
+		testSpecs: [
+			{ input: 'code |', expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
+			{ input: 'code.bat |', expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
+			{ input: 'code.cmd |', expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
+			{ input: 'code.exe |', expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
+			{ input: 'code.anything |', expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
+		]
+	});
+}
+
 suite('Terminal Suggest', () => {
 	for (const suiteSpec of testSpecs2) {
 		suite(suiteSpec.name, () => {