Skip to content

Commit d90d69f

Browse files
committed
feat: add nu shell support
1 parent 3bae4a1 commit d90d69f

1 file changed

Lines changed: 206 additions & 0 deletions

File tree

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import * as fs from 'fs-extra';
2+
import * as path from 'path';
3+
import * as os from 'os';
4+
import { ShellScriptEditState, ShellSetupState, ShellStartupProvider } from './startupProvider';
5+
import { EnvironmentVariableCollection } from 'vscode';
6+
import { PythonCommandRunConfiguration, PythonEnvironment, TerminalShellType } from '../../../api';
7+
import { getActivationCommandForShell } from '../../common/activation';
8+
import { quoteArgs } from '../../execution/execUtils';
9+
import { traceError, traceInfo, traceVerbose } from '../../../common/logging';
10+
import which from 'which';
11+
12+
async function isNuShellInstalled(): Promise<boolean> {
13+
try {
14+
await which('nu');
15+
return true;
16+
} catch {
17+
return false;
18+
}
19+
}
20+
21+
async function getNuShellProfile(): Promise<string> {
22+
const homeDir = os.homedir();
23+
// Nu shell configuration is typically at ~/.config/nushell/config.nu
24+
return path.join(homeDir, '.config', 'nushell', 'config.nu');
25+
}
26+
27+
const regionStart = '# >>> vscode python';
28+
const regionEnd = '# <<< vscode python';
29+
30+
function getActivationContent(key: string): string {
31+
const lineSep = '\n';
32+
// In Nu shell, environment variables are checked with `if` statement and executed with `do`
33+
return [
34+
'',
35+
'',
36+
regionStart,
37+
`if (env | where name == "${key}" | is-empty | not)`,
38+
` do $env.${key}`,
39+
'end',
40+
regionEnd,
41+
'',
42+
].join(lineSep);
43+
}
44+
45+
async function isStartupSetup(profilePath: string, key: string): Promise<boolean> {
46+
if (!(await fs.pathExists(profilePath))) {
47+
return false;
48+
}
49+
50+
// Check if profile has our activation content
51+
const content = await fs.readFile(profilePath, 'utf8');
52+
return content.includes(key);
53+
}
54+
55+
async function setupStartup(profilePath: string, key: string): Promise<boolean> {
56+
try {
57+
const activationContent = getActivationContent(key);
58+
59+
// Create profile directory if it doesn't exist
60+
await fs.mkdirp(path.dirname(profilePath));
61+
62+
// Create or update profile
63+
if (!(await fs.pathExists(profilePath))) {
64+
// Create new profile with our content
65+
await fs.writeFile(profilePath, activationContent);
66+
traceInfo(`Created new Nu shell profile at: ${profilePath}\n${activationContent}`);
67+
} else {
68+
// Update existing profile
69+
const content = await fs.readFile(profilePath, 'utf8');
70+
if (!content.includes(key)) {
71+
await fs.writeFile(profilePath, `${content}${activationContent}`);
72+
traceInfo(`Updated existing Nu shell profile at: ${profilePath}\n${activationContent}`);
73+
} else {
74+
// Already contains our activation code
75+
traceInfo(`Nu shell profile at ${profilePath} already contains activation code`);
76+
}
77+
}
78+
return true;
79+
} catch (err) {
80+
traceVerbose(`Failed to setup Nu shell startup`, err);
81+
return false;
82+
}
83+
}
84+
85+
async function removeNuShellStartup(profilePath: string, key: string): Promise<boolean> {
86+
if (!(await fs.pathExists(profilePath))) {
87+
return true; // Count as success if file doesn't exist since there's nothing to remove
88+
}
89+
90+
try {
91+
const content = await fs.readFile(profilePath, 'utf8');
92+
if (content.includes(key)) {
93+
// Use regex to remove the entire region including newlines
94+
const pattern = new RegExp(`${regionStart}[\\s\\S]*?${regionEnd}\\n?`, 'g');
95+
const newContent = content.replace(pattern, '');
96+
await fs.writeFile(profilePath, newContent);
97+
traceInfo(`Removed activation from Nu shell profile at: ${profilePath}`);
98+
}
99+
return true;
100+
} catch (err) {
101+
traceVerbose(`Failed to remove Nu shell startup`, err);
102+
return false;
103+
}
104+
}
105+
106+
function getCommandAsString(command: PythonCommandRunConfiguration[]): string {
107+
const parts = [];
108+
for (const cmd of command) {
109+
const args = cmd.args ?? [];
110+
// For Nu shell, we need to ensure proper quoting
111+
parts.push(quoteArgs([cmd.executable, ...args]).join(' '));
112+
}
113+
// In Nu shell, commands are chained with `;` followed by a space
114+
return parts.join('; ');
115+
}
116+
117+
export class NuShellStartupProvider implements ShellStartupProvider {
118+
public readonly name: string = 'nushell';
119+
public readonly nuShellActivationEnvVarKey = 'VSCODE_NU_ACTIVATE';
120+
121+
async isSetup(): Promise<ShellSetupState> {
122+
const isInstalled = await isNuShellInstalled();
123+
if (!isInstalled) {
124+
traceVerbose('Nu shell is not installed');
125+
return ShellSetupState.NotInstalled;
126+
}
127+
128+
try {
129+
const nuShellProfile = await getNuShellProfile();
130+
const isSetup = await isStartupSetup(nuShellProfile, this.nuShellActivationEnvVarKey);
131+
return isSetup ? ShellSetupState.Setup : ShellSetupState.NotSetup;
132+
} catch (err) {
133+
traceError('Failed to check if Nu shell startup is setup', err);
134+
return ShellSetupState.NotSetup;
135+
}
136+
}
137+
138+
async setupScripts(): Promise<ShellScriptEditState> {
139+
const isInstalled = await isNuShellInstalled();
140+
if (!isInstalled) {
141+
traceVerbose('Nu shell is not installed');
142+
return ShellScriptEditState.NotInstalled;
143+
}
144+
145+
try {
146+
const nuShellProfile = await getNuShellProfile();
147+
const success = await setupStartup(nuShellProfile, this.nuShellActivationEnvVarKey);
148+
return success ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited;
149+
} catch (err) {
150+
traceError('Failed to setup Nu shell startup', err);
151+
return ShellScriptEditState.NotEdited;
152+
}
153+
}
154+
155+
async teardownScripts(): Promise<ShellScriptEditState> {
156+
const isInstalled = await isNuShellInstalled();
157+
if (!isInstalled) {
158+
traceVerbose('Nu shell is not installed');
159+
return ShellScriptEditState.NotInstalled;
160+
}
161+
162+
try {
163+
const nuShellProfile = await getNuShellProfile();
164+
const success = await removeNuShellStartup(nuShellProfile, this.nuShellActivationEnvVarKey);
165+
return success ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited;
166+
} catch (err) {
167+
traceError('Failed to remove Nu shell startup', err);
168+
return ShellScriptEditState.NotEdited;
169+
}
170+
}
171+
172+
async updateEnvVariables(collection: EnvironmentVariableCollection, env: PythonEnvironment): Promise<void> {
173+
try {
174+
const nuShellActivation = getActivationCommandForShell(env, TerminalShellType.nushell);
175+
if (nuShellActivation) {
176+
const command = getCommandAsString(nuShellActivation);
177+
collection.replace(this.nuShellActivationEnvVarKey, command);
178+
} else {
179+
collection.delete(this.nuShellActivationEnvVarKey);
180+
}
181+
} catch (err) {
182+
traceError('Failed to update Nu shell environment variables', err);
183+
collection.delete(this.nuShellActivationEnvVarKey);
184+
}
185+
}
186+
187+
async removeEnvVariables(envCollection: EnvironmentVariableCollection): Promise<void> {
188+
envCollection.delete(this.nuShellActivationEnvVarKey);
189+
}
190+
191+
async getEnvVariables(env?: PythonEnvironment): Promise<Map<string, string | undefined> | undefined> {
192+
if (!env) {
193+
return new Map([[this.nuShellActivationEnvVarKey, undefined]]);
194+
}
195+
196+
try {
197+
const nuShellActivation = getActivationCommandForShell(env, TerminalShellType.nushell);
198+
return nuShellActivation
199+
? new Map([[this.nuShellActivationEnvVarKey, getCommandAsString(nuShellActivation)]])
200+
: undefined;
201+
} catch (err) {
202+
traceError('Failed to get Nu shell environment variables', err);
203+
return undefined;
204+
}
205+
}
206+
}

0 commit comments

Comments
 (0)