Skip to content

Commit 16c9419

Browse files
committed
implement copy import path command
1 parent 0fe3beb commit 16c9419

6 files changed

Lines changed: 95 additions & 0 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import * as path from 'path';
2+
import * as vscode from 'vscode';
3+
import { inject, injectable } from 'inversify';
4+
5+
import { ICommandManager, IWorkspaceService } from '../../common/application/types';
6+
import { IExtensionSingleActivationService } from '../../activation/types';
7+
import { Commands } from '../../common/constants';
8+
import { getSysPath } from '../../common/utils/pythonUtils';
9+
10+
@injectable()
11+
export class CopyImportPathCommand implements IExtensionSingleActivationService {
12+
public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true };
13+
14+
constructor(
15+
@inject(ICommandManager) private readonly commands: ICommandManager,
16+
@inject(IWorkspaceService) private readonly workspace: IWorkspaceService,
17+
) {}
18+
19+
async activate(): Promise<void> {
20+
this.commands.registerCommand(Commands.CopyImportPath, this.execute, this);
21+
}
22+
23+
private async execute(fileUri?: vscode.Uri): Promise<void> {
24+
const uri = fileUri ?? vscode.window.activeTextEditor?.document.uri;
25+
if (!uri || uri.scheme !== 'file' || !uri.fsPath.endsWith('.py')) {
26+
void vscode.window.showWarningMessage('No Python file selected for import-path copy.');
27+
return;
28+
}
29+
30+
const importPath = this.resolveImportPath(uri.fsPath);
31+
await vscode.env.clipboard.writeText(importPath);
32+
void vscode.window.showInformationMessage(`Copied: ${importPath}`);
33+
}
34+
35+
/**
36+
* Resolves a Python import-style dotted path from an absolute file path.
37+
*
38+
* The resolution follows a 3-level fallback strategy:
39+
*
40+
* 1. If the file is located under any entry in `sys.path`, the path relative to that entry is used.
41+
* 2. If the file is located under the current workspace folder, the path relative to the workspace root is used.
42+
* 3. Otherwise, the import path falls back to the file name (without extension).
43+
*
44+
* @param absPath - The absolute path to a `.py` file.
45+
* @returns The resolved import path in dotted notation (e.g., 'pkg.module').
46+
*/
47+
private resolveImportPath(absPath: string): string {
48+
// ---------- ① sys.path ----------
49+
for (const sysRoot of getSysPath()) {
50+
if (sysRoot && absPath.startsWith(sysRoot)) {
51+
return CopyImportPathCommand.toDotted(path.relative(sysRoot, absPath));
52+
}
53+
}
54+
55+
// ---------- ② workspaceFolder ----------
56+
const ws = this.workspace.getWorkspaceFolder(vscode.Uri.file(absPath));
57+
if (ws && absPath.startsWith(ws.uri.fsPath)) {
58+
return CopyImportPathCommand.toDotted(path.relative(ws.uri.fsPath, absPath));
59+
}
60+
61+
// ---------- ③ fallback ----------
62+
return path.basename(absPath, '.py');
63+
}
64+
65+
private static toDotted(relPath: string): string {
66+
return relPath.replace(/\.py$/i, '').split(path.sep).filter(Boolean).join('.');
67+
}
68+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { IServiceManager } from '../../ioc/types';
2+
import { IExtensionSingleActivationService } from '../../activation/types';
3+
import { CopyImportPathCommand } from './copyImportPathCommand';
4+
5+
export function registerTypes(serviceManager: IServiceManager): void {
6+
serviceManager.addSingleton<IExtensionSingleActivationService>(
7+
IExtensionSingleActivationService,
8+
CopyImportPathCommand,
9+
);
10+
}

src/client/application/serviceRegistry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
import { IServiceManager } from '../ioc/types';
77
import { registerTypes as diagnosticsRegisterTypes } from './diagnostics/serviceRegistry';
8+
import { registerTypes as importPathRegisterTypes } from './importPath/serviceRegistry';
89

910
export function registerTypes(serviceManager: IServiceManager) {
1011
diagnosticsRegisterTypes(serviceManager);
12+
importPathRegisterTypes(serviceManager);
1113
}

src/client/common/application/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ interface ICommandNameWithoutArgumentTypeMapping {
4040
[Commands.ClearStorage]: [];
4141
[Commands.CreateNewFile]: [];
4242
[Commands.ReportIssue]: [];
43+
[Commands.CopyImportPath]: [];
4344
[LSCommands.RestartLS]: [];
4445
}
4546

src/client/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export enum CommandSource {
3535

3636
export namespace Commands {
3737
export const ClearStorage = 'python.clearCacheAndReload';
38+
export const CopyImportPath = 'python.copyImportPath';
3839
export const CreateNewFile = 'python.createNewFile';
3940
export const ClearWorkspaceInterpreter = 'python.clearWorkspaceInterpreter';
4041
export const Create_Environment = 'python.createEnvironment';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { execFileSync } from 'child_process';
2+
3+
export function getSysPath(pythonCmd = 'python3'): string[] {
4+
try {
5+
const out = execFileSync(pythonCmd, ['-c', 'import sys, json; print(json.dumps(sys.path))'], {
6+
encoding: 'utf-8',
7+
});
8+
return JSON.parse(out);
9+
} catch (err) {
10+
console.warn('[CopyImportPath] getSysPath failed:', err);
11+
return [];
12+
}
13+
}

0 commit comments

Comments
 (0)