Skip to content

Commit a2bfe2b

Browse files
authored
Filter and remove folders from auto-find already in projects (#87)
Fixes #76 1. Fixes bug 2. Refactors classes 3. Add tests
1 parent 2532a2f commit a2bfe2b

File tree

8 files changed

+442
-130
lines changed

8 files changed

+442
-130
lines changed

src/common/localize.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,15 @@ export namespace CondaStrings {
126126
export const condaRemoveFailed = l10n.t('Failed to remove conda environment');
127127
export const condaExists = l10n.t('Environment already exists');
128128
}
129+
130+
export namespace ProjectCreatorString {
131+
export const addExistingProjects = l10n.t('Add Existing Projects');
132+
export const autoFindProjects = l10n.t('Auto Find Projects');
133+
export const selectProjects = l10n.t('Select Python projects');
134+
export const selectFilesOrFolders = l10n.t('Select Project folders or Python files');
135+
export const autoFindProjectsDescription = l10n.t(
136+
'Automatically find folders with `pyproject.toml` or `setup.py` files.',
137+
);
138+
139+
export const noProjectsFound = l10n.t('No projects found');
140+
}

src/common/utils/asyncUtils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export async function sleep(milliseconds: number) {
2+
return new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
3+
}

src/extension.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,7 @@ import { getPythonApi, setPythonApi } from './features/pythonApi';
3030
import { setPersistentState } from './common/persistentState';
3131
import { createNativePythonFinder, NativePythonFinder } from './managers/common/nativePythonFinder';
3232
import { PythonEnvironmentApi } from './api';
33-
import {
34-
ProjectCreatorsImpl,
35-
registerAutoProjectProvider,
36-
registerExistingProjectProvider,
37-
} from './features/projectCreators';
33+
import { ProjectCreatorsImpl } from './features/creators/projectCreators';
3834
import { ProjectView } from './features/views/projectView';
3935
import { registerCompletionProvider } from './features/settings/settingCompletions';
4036
import { TerminalManager, TerminalManagerImpl } from './features/terminal/terminalManager';
@@ -56,6 +52,8 @@ import { StopWatch } from './common/stopWatch';
5652
import { sendTelemetryEvent } from './common/telemetry/sender';
5753
import { EventNames } from './common/telemetry/constants';
5854
import { ensureCorrectVersion } from './common/extVersion';
55+
import { ExistingProjects } from './features/creators/existingProjects';
56+
import { AutoFindProjects } from './features/creators/autoFindProjects';
5957

6058
export async function activate(context: ExtensionContext): Promise<PythonEnvironmentApi> {
6159
const start = new StopWatch();
@@ -87,8 +85,8 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
8785
const projectCreators: ProjectCreators = new ProjectCreatorsImpl();
8886
context.subscriptions.push(
8987
projectCreators,
90-
registerExistingProjectProvider(projectCreators),
91-
registerAutoProjectProvider(projectCreators),
88+
projectCreators.registerPythonProjectCreator(new ExistingProjects()),
89+
projectCreators.registerPythonProjectCreator(new AutoFindProjects(projectManager)),
9290
);
9391

9492
setPythonApi(envManagers, projectManager, projectCreators, terminalManager, envVarManager);
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import * as path from 'path';
2+
import { Uri } from 'vscode';
3+
import { showQuickPickWithButtons } from '../../common/window.apis';
4+
import { ProjectCreatorString } from '../../common/localize';
5+
import { PythonProject, PythonProjectCreator, PythonProjectCreatorOptions } from '../../api';
6+
import { PythonProjectManager } from '../../internal.api';
7+
import { showErrorMessage } from '../../common/errors/utils';
8+
import { findFiles } from '../../common/workspace.apis';
9+
10+
function getUniqueUri(uris: Uri[]): {
11+
label: string;
12+
description: string;
13+
uri: Uri;
14+
}[] {
15+
const files = uris.map((uri) => uri.fsPath).sort();
16+
const dirs: Map<string, string> = new Map();
17+
files.forEach((file) => {
18+
const dir = path.dirname(file);
19+
if (dirs.has(dir)) {
20+
return;
21+
}
22+
dirs.set(dir, file);
23+
});
24+
return Array.from(dirs.entries())
25+
.map(([dir, file]) => ({
26+
label: path.basename(dir),
27+
description: file,
28+
uri: Uri.file(dir),
29+
}))
30+
.sort((a, b) => a.label.localeCompare(b.label));
31+
}
32+
33+
async function pickProjects(uris: Uri[]): Promise<Uri[] | undefined> {
34+
const items = getUniqueUri(uris);
35+
36+
const selected = await showQuickPickWithButtons(items, {
37+
canPickMany: true,
38+
ignoreFocusOut: true,
39+
placeHolder: ProjectCreatorString.selectProjects,
40+
showBackButton: true,
41+
});
42+
43+
if (Array.isArray(selected)) {
44+
return selected.map((s) => s.uri);
45+
} else if (selected) {
46+
return [selected.uri];
47+
}
48+
49+
return undefined;
50+
}
51+
52+
export class AutoFindProjects implements PythonProjectCreator {
53+
public readonly name = 'autoProjects';
54+
public readonly displayName = ProjectCreatorString.autoFindProjects;
55+
public readonly description = ProjectCreatorString.autoFindProjectsDescription;
56+
57+
constructor(private readonly pm: PythonProjectManager) {}
58+
59+
async create(_options?: PythonProjectCreatorOptions): Promise<PythonProject | PythonProject[] | undefined> {
60+
const files = await findFiles('**/{pyproject.toml,setup.py}');
61+
if (!files || files.length === 0) {
62+
setImmediate(() => {
63+
showErrorMessage('No projects found');
64+
});
65+
return;
66+
}
67+
68+
const filtered = files.filter((uri) => {
69+
const p = this.pm.get(uri);
70+
if (p) {
71+
// If there ia already a project with the same path, skip it.
72+
// If there is a project with the same parent path, skip it.
73+
const np = path.normalize(p.uri.fsPath);
74+
const nf = path.normalize(uri.fsPath);
75+
const nfp = path.dirname(nf);
76+
return np !== nf && np !== nfp;
77+
}
78+
return true;
79+
});
80+
81+
if (filtered.length === 0) {
82+
return;
83+
}
84+
85+
const projects = await pickProjects(filtered);
86+
if (!projects || projects.length === 0) {
87+
return;
88+
}
89+
90+
return projects.map((uri) => ({
91+
name: path.basename(uri.fsPath),
92+
uri,
93+
}));
94+
}
95+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as path from 'path';
2+
import { PythonProject, PythonProjectCreator, PythonProjectCreatorOptions } from '../../api';
3+
import { ProjectCreatorString } from '../../common/localize';
4+
import { showOpenDialog } from '../../common/window.apis';
5+
6+
export class ExistingProjects implements PythonProjectCreator {
7+
public readonly name = 'existingProjects';
8+
public readonly displayName = ProjectCreatorString.addExistingProjects;
9+
10+
async create(_options?: PythonProjectCreatorOptions): Promise<PythonProject | PythonProject[] | undefined> {
11+
const results = await showOpenDialog({
12+
canSelectFiles: true,
13+
canSelectFolders: true,
14+
canSelectMany: true,
15+
filters: {
16+
python: ['py'],
17+
},
18+
title: ProjectCreatorString.selectFilesOrFolders,
19+
});
20+
21+
if (!results || results.length === 0) {
22+
return;
23+
}
24+
25+
return results.map((r) => ({
26+
name: path.basename(r.fsPath),
27+
uri: r,
28+
}));
29+
}
30+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Disposable } from 'vscode';
2+
import { PythonProjectCreator } from '../../api';
3+
import { ProjectCreators } from '../../internal.api';
4+
5+
export class ProjectCreatorsImpl implements ProjectCreators {
6+
private _creators: PythonProjectCreator[] = [];
7+
8+
registerPythonProjectCreator(creator: PythonProjectCreator): Disposable {
9+
this._creators.push(creator);
10+
return new Disposable(() => {
11+
this._creators = this._creators.filter((item) => item !== creator);
12+
});
13+
}
14+
getProjectCreators(): PythonProjectCreator[] {
15+
return this._creators;
16+
}
17+
18+
dispose() {
19+
this._creators = [];
20+
}
21+
}

src/features/projectCreators.ts

Lines changed: 0 additions & 123 deletions
This file was deleted.

0 commit comments

Comments
 (0)