Skip to content

Commit a8f6dd5

Browse files
authored
feat: Add copy path to Project and Environments (#170)
Closes #166
1 parent f24720c commit a8f6dd5

File tree

6 files changed

+141
-6
lines changed

6 files changed

+141
-6
lines changed

package.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,18 @@
218218
"title": "%python-envs.uninstallPackage.title%",
219219
"category": "Python Envs",
220220
"icon": "$(trash)"
221+
},
222+
{
223+
"command": "python-envs.copyEnvPath",
224+
"title": "%python-envs.copyEnvPath.title%",
225+
"category": "Python Envs",
226+
"icon": "$(copy)"
227+
},
228+
{
229+
"command": "python-envs.copyProjectPath",
230+
"title": "%python-envs.copyProjectPath.title%",
231+
"category": "Python Envs",
232+
"icon": "$(copy)"
221233
}
222234
],
223235
"menus": {
@@ -285,6 +297,14 @@
285297
{
286298
"command": "python-envs.uninstallPackage",
287299
"when": "false"
300+
},
301+
{
302+
"command": "python-envs.copyEnvPath",
303+
"when": "false"
304+
},
305+
{
306+
"command": "python-envs.copyProjectPath",
307+
"when": "false"
288308
}
289309
],
290310
"view/item/context": [
@@ -322,6 +342,11 @@
322342
"group": "inline",
323343
"when": "view == env-managers && viewItem =~ /.*pythonEnvironment.*/"
324344
},
345+
{
346+
"command": "python-envs.copyEnvPath",
347+
"group": "inline",
348+
"when": "view == env-managers && viewItem =~ /.*pythonEnvironment.*/"
349+
},
325350
{
326351
"command": "python-envs.uninstallPackage",
327352
"group": "inline",
@@ -332,6 +357,11 @@
332357
"group": "inline",
333358
"when": "view == python-projects && viewItem == python-env"
334359
},
360+
{
361+
"command": "python-envs.copyEnvPath",
362+
"group": "inline",
363+
"when": "view == python-projects && viewItem == python-env"
364+
},
335365
{
336366
"command": "python-envs.refreshPackages",
337367
"group": "inline",
@@ -355,6 +385,11 @@
355385
"group": "inline",
356386
"when": "view == python-projects && viewItem =~ /.*python-workspace.*/"
357387
},
388+
{
389+
"command": "python-envs.copyProjectPath",
390+
"group": "inline",
391+
"when": "view == python-projects && viewItem =~ /.*python-workspace.*/"
392+
},
358393
{
359394
"command": "python-envs.uninstallPackage",
360395
"group": "inline",

package.nls.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"python-envs.setPkgManager.title": "Set Package Manager",
1111
"python-envs.addPythonProject.title": "Add Python Project",
1212
"python-envs.removePythonProject.title": "Remove Python Project",
13+
"python-envs.copyEnvPath.title": "Copy Environment Path",
14+
"python-envs.copyProjectPath.title": "Copy Project Path",
1315
"python-envs.create.title": "Create Environment",
1416
"python-envs.createAny.title": "Create Environment",
1517
"python-envs.set.title": "Set Workspace Environment",

src/common/env.apis.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ import { env, Uri } from 'vscode';
33
export function launchBrowser(uri: string | Uri): Thenable<boolean> {
44
return env.openExternal(uri instanceof Uri ? uri : Uri.parse(uri));
55
}
6+
7+
export function clipboardWriteText(text: string): Thenable<void> {
8+
return env.clipboard.writeText(text);
9+
}

src/extension.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
createAnyEnvironmentCommand,
2323
runInDedicatedTerminalCommand,
2424
handlePackageUninstall,
25+
copyPathToClipboard,
2526
} from './features/envCommands';
2627
import { registerCondaFeatures } from './managers/conda/main';
2728
import { registerSystemPythonFeatures } from './managers/builtin/main';
@@ -179,6 +180,12 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
179180
commands.registerCommand('python-envs.createTerminal', (item) => {
180181
return createTerminalCommand(item, api, terminalManager);
181182
}),
183+
commands.registerCommand('python-envs.copyEnvPath', async (item) => {
184+
await copyPathToClipboard(item);
185+
}),
186+
commands.registerCommand('python-envs.copyProjectPath', async (item) => {
187+
await copyPathToClipboard(item);
188+
}),
182189
commands.registerCommand('python-envs.terminal.activate', async () => {
183190
const terminal = activeTerminal();
184191
if (terminal) {

src/features/envCommands.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { QuickInputButtons, TaskExecution, TaskRevealKind, Terminal, Uri, window } from 'vscode';
1+
import { QuickInputButtons, TaskExecution, TaskRevealKind, Terminal, Uri } from 'vscode';
22
import {
33
EnvironmentManagers,
44
InternalEnvironmentManager,
@@ -40,6 +40,10 @@ import { pickPackageOptions } from '../common/pickers/packages';
4040
import { pickProject, pickProjectMany } from '../common/pickers/projects';
4141
import { TerminalManager } from './terminal/terminalManager';
4242
import { runInTerminal } from './terminal/runInTerminal';
43+
import { quoteArgs } from './execution/execUtils';
44+
import { showErrorMessage } from '../common/errors/utils';
45+
import { activeTextEditor } from '../common/window.apis';
46+
import { clipboardWriteText } from '../common/env.apis';
4347

4448
export async function refreshManagerCommand(context: unknown): Promise<void> {
4549
if (context instanceof EnvManagerTreeItem) {
@@ -278,7 +282,7 @@ export async function setEnvironmentCommand(
278282
}
279283
} else {
280284
traceError(`Invalid context for setting environment command: ${context}`);
281-
window.showErrorMessage('Invalid context for setting environment');
285+
showErrorMessage('Invalid context for setting environment');
282286
}
283287
}
284288

@@ -296,7 +300,7 @@ export async function resetEnvironmentCommand(
296300
if (manager) {
297301
manager.set(uri, undefined);
298302
} else {
299-
window.showErrorMessage(`No environment manager found for: ${uri.fsPath}`);
303+
showErrorMessage(`No environment manager found for: ${uri.fsPath}`);
300304
traceError(`No environment manager found for ${uri.fsPath}`);
301305
}
302306
return;
@@ -308,7 +312,7 @@ export async function resetEnvironmentCommand(
308312
return;
309313
}
310314
traceError(`Invalid context for unset environment command: ${context}`);
311-
window.showErrorMessage('Invalid context for unset environment');
315+
showErrorMessage('Invalid context for unset environment');
312316
}
313317

314318
export async function setEnvManagerCommand(em: EnvironmentManagers, wm: PythonProjectManager): Promise<void> {
@@ -338,7 +342,7 @@ export async function addPythonProject(
338342
pc: ProjectCreators,
339343
): Promise<PythonProject | PythonProject[] | undefined> {
340344
if (wm.getProjects().length === 0) {
341-
window.showErrorMessage('Please open a folder/project before adding a workspace');
345+
showErrorMessage('Please open a folder/project before adding a workspace');
342346
return;
343347
}
344348

@@ -554,9 +558,24 @@ export async function runAsTaskCommand(item: unknown, api: PythonEnvironmentApi)
554558
);
555559
}
556560
} else if (item === undefined) {
557-
const uri = window.activeTextEditor?.document.uri;
561+
const uri = activeTextEditor()?.document.uri;
558562
if (uri) {
559563
return runAsTaskCommand(uri, api);
560564
}
561565
}
562566
}
567+
568+
export async function copyPathToClipboard(item: unknown): Promise<void> {
569+
if (item instanceof ProjectItem) {
570+
const projectPath = item.project.uri.fsPath;
571+
await clipboardWriteText(projectPath);
572+
traceInfo(`Copied project path to clipboard: ${projectPath}`);
573+
} else if (item instanceof ProjectEnvironment || item instanceof PythonEnvTreeItem) {
574+
const run = item.environment.execInfo.activatedRun ?? item.environment.execInfo.run;
575+
const envPath = quoteArgs([run.executable, ...(run.args ?? [])]).join(' ');
576+
await clipboardWriteText(envPath);
577+
traceInfo(`Copied environment path to clipboard: ${envPath}`);
578+
} else {
579+
traceVerbose(`Invalid context for copy path to clipboard: ${item}`);
580+
}
581+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import * as sinon from 'sinon';
2+
import * as envApis from '../../../common/env.apis';
3+
import { copyPathToClipboard } from '../../../features/envCommands';
4+
import {
5+
ProjectItem,
6+
ProjectEnvironment,
7+
PythonEnvTreeItem,
8+
EnvManagerTreeItem,
9+
} from '../../../features/views/treeViewItems';
10+
import { Uri } from 'vscode';
11+
import { PythonEnvironment } from '../../../api';
12+
import { InternalEnvironmentManager } from '../../../internal.api';
13+
14+
suite('Copy Path To Clipboard', () => {
15+
let clipboardWriteTextStub: sinon.SinonStub;
16+
17+
setup(() => {
18+
clipboardWriteTextStub = sinon.stub(envApis, 'clipboardWriteText');
19+
clipboardWriteTextStub.resolves();
20+
});
21+
22+
teardown(() => {
23+
sinon.restore();
24+
});
25+
26+
test('Copy project path to clipboard', async () => {
27+
const uri = Uri.file('/test');
28+
const item = new ProjectItem({ name: 'test', uri });
29+
await copyPathToClipboard(item);
30+
31+
sinon.assert.calledOnce(clipboardWriteTextStub);
32+
sinon.assert.calledWith(clipboardWriteTextStub, uri.fsPath);
33+
});
34+
35+
test('Copy env path to clipboard: project view', async () => {
36+
const uri = Uri.file('/test');
37+
const item = new ProjectEnvironment(new ProjectItem({ name: 'test', uri }), {
38+
envId: { managerId: 'test-manager', id: 'env1' },
39+
name: 'env1',
40+
displayName: 'Environment 1',
41+
displayPath: '/test-env',
42+
execInfo: { run: { executable: '/test-env/bin/test', args: ['-m', 'env'] } },
43+
} as PythonEnvironment);
44+
45+
await copyPathToClipboard(item);
46+
47+
sinon.assert.calledOnce(clipboardWriteTextStub);
48+
sinon.assert.calledWith(clipboardWriteTextStub, '/test-env/bin/test -m env');
49+
});
50+
51+
test('Copy env path to clipboard: env manager view', async () => {
52+
const item = new PythonEnvTreeItem(
53+
{
54+
envId: { managerId: 'test-manager', id: 'env1' },
55+
name: 'env1',
56+
displayName: 'Environment 1',
57+
displayPath: '/test-env',
58+
execInfo: { run: { executable: '/test-env/bin/test', args: ['-m', 'env'] } },
59+
} as PythonEnvironment,
60+
new EnvManagerTreeItem({ name: 'test-manager', id: 'test-manager' } as InternalEnvironmentManager),
61+
);
62+
63+
await copyPathToClipboard(item);
64+
65+
sinon.assert.calledOnce(clipboardWriteTextStub);
66+
sinon.assert.calledWith(clipboardWriteTextStub, '/test-env/bin/test -m env');
67+
});
68+
});

0 commit comments

Comments
 (0)