Skip to content

Commit 8237317

Browse files
authored
Merge pull request #239356 from microsoft/tyriar/aliases
Terminal suggest: Pull aliases from each shell, add alias icon type and polish pwsh detail
2 parents 2aaf628 + 9d7b589 commit 8237317

File tree

11 files changed

+347
-96
lines changed

11 files changed

+347
-96
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import type { ICompletionResource } from '../types';
8+
import { type ExecOptionsWithStringEncoding } from 'node:child_process';
9+
import { execHelper, spawnHelper } from './common';
10+
11+
export async function getBashGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set<string>): Promise<(string | ICompletionResource)[]> {
12+
return [
13+
...await getAliases(options),
14+
...await getBuiltins(options, existingCommands)
15+
];
16+
}
17+
18+
async function getBuiltins(options: ExecOptionsWithStringEncoding, existingCommands?: Set<string>): Promise<string[]> {
19+
const compgenOutput = await execHelper('compgen -b', options);
20+
const filter = (cmd: string) => cmd && !existingCommands?.has(cmd);
21+
return compgenOutput.split('\n').filter(filter);
22+
}
23+
24+
async function getAliases(options: ExecOptionsWithStringEncoding): Promise<ICompletionResource[]> {
25+
// This must be run with interactive, otherwise there's a good chance aliases won't
26+
// be set up. Note that this could differ from the actual aliases as it's a new bash
27+
// session, for the same reason this would not include aliases that are created
28+
// by simply running `alias ...` in the terminal.
29+
const aliasOutput = await spawnHelper('bash', ['-ic', 'alias'], options);
30+
const result: ICompletionResource[] = [];
31+
for (const line of aliasOutput.split('\n')) {
32+
const match = line.match(/^alias (?<alias>[a-zA-Z0-9\.:-]+)='(?<resolved>.+)'$/);
33+
if (!match?.groups) {
34+
continue;
35+
}
36+
result.push({
37+
label: match.groups.alias,
38+
detail: match.groups.resolved,
39+
kind: vscode.TerminalCompletionItemKind.Alias,
40+
});
41+
}
42+
return result;
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { exec, spawn, type ExecOptionsWithStringEncoding } from 'node:child_process';
7+
8+
export async function spawnHelper(command: string, args: string[], options: ExecOptionsWithStringEncoding): Promise<string> {
9+
// This must be run with interactive, otherwise there's a good chance aliases won't
10+
// be set up. Note that this could differ from the actual aliases as it's a new bash
11+
// session, for the same reason this would not include aliases that are created
12+
// by simply running `alias ...` in the terminal.
13+
return new Promise<string>((resolve, reject) => {
14+
const child = spawn(command, args, options);
15+
let stdout = '';
16+
child.stdout.on('data', (data) => {
17+
stdout += data;
18+
});
19+
child.on('close', (code) => {
20+
if (code !== 0) {
21+
reject(new Error(`process exited with code ${code}`));
22+
} else {
23+
resolve(stdout);
24+
}
25+
});
26+
});
27+
}
28+
29+
export async function execHelper(commandLine: string, options: ExecOptionsWithStringEncoding): Promise<string> {
30+
return new Promise<string>((resolve, reject) => {
31+
exec(commandLine, options, (error, stdout) => {
32+
if (error) {
33+
reject(error);
34+
} else {
35+
resolve(stdout);
36+
}
37+
});
38+
});
39+
}
40+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import type { ICompletionResource } from '../types';
8+
import { execHelper, spawnHelper } from './common';
9+
import { type ExecOptionsWithStringEncoding } from 'node:child_process';
10+
11+
export async function getFishGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set<string>): Promise<(string | ICompletionResource)[]> {
12+
return [
13+
...await getAliases(options),
14+
...await getBuiltins(options, existingCommands),
15+
];
16+
}
17+
18+
async function getBuiltins(options: ExecOptionsWithStringEncoding, existingCommands?: Set<string>): Promise<string[]> {
19+
const compgenOutput = await execHelper('functions -n', options);
20+
const filter = (cmd: string) => cmd && !existingCommands?.has(cmd);
21+
return compgenOutput.split(', ').filter(filter);
22+
}
23+
24+
async function getAliases(options: ExecOptionsWithStringEncoding): Promise<ICompletionResource[]> {
25+
// This must be run with interactive, otherwise there's a good chance aliases won't
26+
// be set up. Note that this could differ from the actual aliases as it's a new bash
27+
// session, for the same reason this would not include aliases that are created
28+
// by simply running `alias ...` in the terminal.
29+
const aliasOutput = await spawnHelper('fish', ['-ic', 'alias'], options);
30+
31+
const result: ICompletionResource[] = [];
32+
for (const line of aliasOutput.split('\n')) {
33+
const match = line.match(/^alias (?<alias>[a-zA-Z0-9\.:-]+) (?<resolved>.+)$/);
34+
if (!match?.groups) {
35+
continue;
36+
}
37+
result.push({
38+
label: match.groups.alias,
39+
detail: match.groups.resolved,
40+
kind: vscode.TerminalCompletionItemKind.Alias,
41+
});
42+
}
43+
return result;
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import type { ICompletionResource } from '../types';
8+
import { type ExecOptionsWithStringEncoding } from 'node:child_process';
9+
import { execHelper } from './common';
10+
11+
export async function getPwshGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set<string>): Promise<(string | ICompletionResource)[]> {
12+
return [
13+
...await getAliases(options, existingCommands),
14+
...await getCommands(options, existingCommands),
15+
];
16+
}
17+
18+
/**
19+
* The numeric values associated with CommandType from Get-Command. It appears that this is a
20+
* bitfield based on the values but I think it's actually used as an enum where a CommandType can
21+
* only be a single one of these.
22+
*
23+
* Source:
24+
*
25+
* ```
26+
* [enum]::GetValues([System.Management.Automation.CommandTypes]) | ForEach-Object {
27+
* [pscustomobject]@{
28+
* Name = $_
29+
* Value = [int]$_
30+
* }
31+
* }
32+
* ```
33+
*/
34+
const enum PwshCommandType {
35+
Alias = 1,
36+
Function = 2,
37+
Filter = 4,
38+
Cmdlet = 8,
39+
ExternalScript = 16,
40+
Application = 32,
41+
Script = 64,
42+
Configuration = 256,
43+
// All = 383,
44+
}
45+
46+
const pwshCommandTypeToCompletionKind: Map<PwshCommandType, vscode.TerminalCompletionItemKind> = new Map([
47+
[PwshCommandType.Alias, vscode.TerminalCompletionItemKind.Alias],
48+
[PwshCommandType.Function, vscode.TerminalCompletionItemKind.Method],
49+
[PwshCommandType.Filter, vscode.TerminalCompletionItemKind.Method],
50+
[PwshCommandType.Cmdlet, vscode.TerminalCompletionItemKind.Method],
51+
[PwshCommandType.ExternalScript, vscode.TerminalCompletionItemKind.Method],
52+
[PwshCommandType.Application, vscode.TerminalCompletionItemKind.Method],
53+
[PwshCommandType.Script, vscode.TerminalCompletionItemKind.Method],
54+
[PwshCommandType.Configuration, vscode.TerminalCompletionItemKind.Argument],
55+
]);
56+
57+
async function getAliases(options: ExecOptionsWithStringEncoding, existingCommands?: Set<string>): Promise<ICompletionResource[]> {
58+
const output = await execHelper('Get-Command -CommandType Alias | Select-Object Name, CommandType, Definition, DisplayName, ModuleName, @{Name="Version";Expression={$_.Version.ToString()}} | ConvertTo-Json', {
59+
...options,
60+
maxBuffer: 1024 * 1024 * 100 // This is a lot of content, increase buffer size
61+
});
62+
let json: any;
63+
try {
64+
json = JSON.parse(output);
65+
} catch (e) {
66+
console.error('Error parsing output:', e);
67+
return [];
68+
}
69+
return (json as any[]).map(e => {
70+
// Aliases sometimes use the same Name and DisplayName, show them as methods in this case.
71+
const isAlias = e.Name !== e.DisplayName;
72+
const detailParts: string[] = [];
73+
if (e.Definition) {
74+
detailParts.push(e.Definition);
75+
}
76+
if (e.ModuleName && e.Version) {
77+
detailParts.push(`${e.ModuleName} v${e.Version}`);
78+
}
79+
return {
80+
label: e.Name,
81+
detail: detailParts.join('\n\n'),
82+
kind: (isAlias
83+
? vscode.TerminalCompletionItemKind.Alias
84+
: vscode.TerminalCompletionItemKind.Method),
85+
};
86+
});
87+
}
88+
89+
async function getCommands(options: ExecOptionsWithStringEncoding, existingCommands?: Set<string>): Promise<ICompletionResource[]> {
90+
const output = await execHelper('Get-Command -All | Select-Object Name, CommandType, Definition, ModuleName, @{Name="Version";Expression={$_.Version.ToString()}} | ConvertTo-Json', {
91+
...options,
92+
maxBuffer: 1024 * 1024 * 100 // This is a lot of content, increase buffer size
93+
});
94+
let json: any;
95+
try {
96+
json = JSON.parse(output);
97+
} catch (e) {
98+
console.error('Error parsing pwsh output:', e);
99+
return [];
100+
}
101+
return (
102+
(json as any[])
103+
.filter(e => e.CommandType !== PwshCommandType.Alias)
104+
.map(e => {
105+
const detailParts: string[] = [];
106+
if (e.Definition) {
107+
detailParts.push(e.Definition.trim());
108+
}
109+
if (e.ModuleName && e.Version) {
110+
detailParts.push(`${e.ModuleName} v${e.Version}`);
111+
}
112+
return {
113+
label: e.Name,
114+
detail: detailParts.join('\n\n'),
115+
kind: pwshCommandTypeToCompletionKind.get(e.CommandType)
116+
};
117+
})
118+
);
119+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import type { ICompletionResource } from '../types';
8+
import { execHelper, spawnHelper } from './common';
9+
import { type ExecOptionsWithStringEncoding } from 'node:child_process';
10+
11+
export async function getZshGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set<string>): Promise<(string | ICompletionResource)[]> {
12+
return [
13+
...await getAliases(options),
14+
...await getBuiltins(options, existingCommands),
15+
];
16+
}
17+
18+
async function getBuiltins(options: ExecOptionsWithStringEncoding, existingCommands?: Set<string>): Promise<string[]> {
19+
const compgenOutput = await execHelper('printf "%s\\n" ${(k)builtins}', options);
20+
const filter = (cmd: string) => cmd && !existingCommands?.has(cmd);
21+
return compgenOutput.split('\n').filter(filter);
22+
}
23+
24+
async function getAliases(options: ExecOptionsWithStringEncoding): Promise<ICompletionResource[]> {
25+
// This must be run with interactive, otherwise there's a good chance aliases won't
26+
// be set up. Note that this could differ from the actual aliases as it's a new bash
27+
// session, for the same reason this would not include aliases that are created
28+
// by simply running `alias ...` in the terminal.
29+
const aliasOutput = await spawnHelper('zsh', ['-ic', 'alias'], options);
30+
const result: ICompletionResource[] = [];
31+
for (const line of aliasOutput.split('\n')) {
32+
const match = line.match(/^(?<alias>[a-zA-Z0-9\.:-]+)=(?:'(?<resolved>.+)'|(?<resolved>.+))$/);
33+
if (!match?.groups) {
34+
continue;
35+
}
36+
result.push({
37+
label: match.groups.alias,
38+
detail: match.groups.resolved,
39+
kind: vscode.TerminalCompletionItemKind.Alias,
40+
});
41+
}
42+
return result;
43+
}

0 commit comments

Comments
 (0)