Skip to content

Commit

Permalink
Add Project views to python extension
Browse files Browse the repository at this point in the history
  • Loading branch information
karthiknadig committed Aug 13, 2024
1 parent 1c8b855 commit e38c056
Show file tree
Hide file tree
Showing 11 changed files with 471 additions and 3 deletions.
17 changes: 15 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,19 @@
"scope": "resource",
"type": "string"
},
"python.environments.activityBar": {
"default": "hide",
"description": "%python.environments.activityBar.description%",
"scope": "resource",
"type": "string",
"enum": [
"show",
"hide"
],
"tags": [
"experimental"
]
},
"python.experiments.enabled": {
"default": true,
"description": "%python.experiments.enabled.description%",
Expand Down Expand Up @@ -1496,8 +1509,8 @@
"views": {
"python-environments": [
{
"id": "python-projects-scripts",
"name": "%python.views.treeView.projectsAndScripts.title%",
"id": "python-projects",
"name": "%python.views.treeView.foldersAndScripts.title%",
"icon": "resources/logo.svg"
},
{
Expand Down
3 changes: 2 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"python.defaultInterpreterPath.description": "Path to default Python to use when extension loads up for the first time, no longer used once an interpreter is selected for the workspace. See [here](https://aka.ms/AAfekmf) to understand when this is used",
"python.diagnostics.sourceMapsEnabled.description": "Enable source map support for meaningful stack traces in error logs.",
"python.envFile.description": "Absolute path to a file containing environment variable definitions.",
"python.environments.activityBar.description": "Show Python environments in the activity bar.",
"python.experiments.enabled.description": "Enables A/B tests experiments in the Python extension. If enabled, you may get included in proposed enhancements and/or features.",
"python.experiments.optInto.description": "List of experiment to opt into. If empty, user is assigned the default experiment groups. See [here](https://github.com/microsoft/vscode-python/wiki/AB-Experiments) for more details.",
"python.experiments.optOutFrom.description": "List of experiment to opt out of. If empty, user is assigned the default experiment groups. See [here](https://github.com/microsoft/vscode-python/wiki/AB-Experiments) for more details.",
Expand Down Expand Up @@ -85,7 +86,7 @@
"python.venvFolders.description": "Folders in your home directory to look into for virtual environments (supports pyenv, direnv and virtualenvwrapper by default).",
"python.venvPath.description": "Path to folder with a list of Virtual Environments (e.g. ~/.pyenv, ~/Envs, ~/.virtualenvs).",
"python.views.activityBar.title": "Python Environments",
"python.views.treeView.projectsAndScripts.title": "Workspaces, Projects, and Scripts",
"python.views.treeView.foldersAndScripts.title": "Workspaces, Projects, and Scripts",
"python.views.treeView.environmentManagers.title": "Environment Managers",
"walkthrough.pythonWelcome.title": "Get Started with Python Development",
"walkthrough.pythonWelcome.description": "Your first steps to set up a Python project with all the powerful tools and features that the Python extension has to offer!",
Expand Down
3 changes: 3 additions & 0 deletions src/client/extensionActivation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { initializePersistentStateForTriggers } from './common/persistentState';
import { logAndNotifyOnLegacySettings } from './logging/settingLogs';
import { DebuggerTypeName } from './debugger/constants';
import { StopWatch } from './common/utils/stopWatch';
import { registerProjectFeatures } from './pythonProjects/api';
import { registerReplCommands, registerReplExecuteOnEnter, registerStartNativeReplCommand } from './repl/replCommands';

export async function activateComponents(
Expand Down Expand Up @@ -111,6 +112,8 @@ export function activateFeatures(ext: ExtensionState, _components: Components):
registerStartNativeReplCommand(ext.disposables, interpreterService);
registerReplCommands(ext.disposables, interpreterService, executionHelper, commandManager);
registerReplExecuteOnEnter(ext.disposables, interpreterService, commandManager);

registerProjectFeatures(ext.disposables);
}

/// //////////////////////////
Expand Down
2 changes: 2 additions & 0 deletions src/client/pythonEnvironments/views/managerView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
2 changes: 2 additions & 0 deletions src/client/pythonEnvironments/views/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
10 changes: 10 additions & 0 deletions src/client/pythonProjects/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { Disposable } from 'vscode';
import { getPythonProjectView } from './projectsView';

export function registerProjectFeatures(disposables: Disposable[]): void {
const projectView = getPythonProjectView();
disposables.push(projectView);
}
92 changes: 92 additions & 0 deletions src/client/pythonProjects/projectsManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import * as path from 'path';
import { Event, EventEmitter, Uri, WorkspaceFolder } from 'vscode';
import { PythonProject, PythonProjectsApi, PythonProjectsChangedEvent } from './types';
import { getWorkspaceFolders } from '../common/vscodeApis/workspaceApis';

class PythonProjectsManagerImpl implements PythonProjectsApi {
private _projects: Map<string, PythonProject> = new Map<string, PythonProject>();

private readonly _onDidChangePythonProjects = new EventEmitter<PythonProjectsChangedEvent>();

constructor(projects: readonly PythonProject[]) {
projects.forEach((project) => this.addProject(project));
}

addPythonProjects(...pythonProjects: PythonProject[]): void {
const addedProjects: PythonProject[] = [];
pythonProjects.forEach((project) => {
if (this.addProject(project)) {
addedProjects.push(project);
}
});
if (addedProjects.length > 0) {
this._onDidChangePythonProjects.fire({ added: addedProjects, removed: [] });
}
}

removePythonProjects(...pythonProjects: PythonProject[]): boolean {
const removedProjects: PythonProject[] = [];
pythonProjects.forEach((project) => {
if (this._projects.delete(project.uri.toString())) {
removedProjects.push(project);
}
});
if (removedProjects.length > 0) {
this._onDidChangePythonProjects.fire({ added: [], removed: removedProjects });
}
return removedProjects.length > 0;
}

onDidChangePythonProjects: Event<PythonProjectsChangedEvent> = this._onDidChangePythonProjects.event;

get pythonProjects(): readonly PythonProject[] {
return Array.from(this._projects.values());
}

getPythonProject(uri: Uri): PythonProject | undefined {
const project: PythonProject | undefined = this._projects.get(uri.toString());
if (project) {
return project;
}
// Check if given uri is a child of a project
const projects = Array.from(this._projects.values())
.sort((a, b) => a.uri.fsPath.length - b.uri.fsPath.length)
.reverse();

const normalizedUriPath = path.normalize(uri.fsPath);
for (const p of projects) {
const normalizedWorkspacePath = path.normalize(p.uri.fsPath);
if (normalizedWorkspacePath === normalizedUriPath) {
return p;
}
let parentPath = path.dirname(normalizedUriPath);
while (parentPath !== path.dirname(parentPath)) {
if (normalizedWorkspacePath === parentPath) {
return p;
}
parentPath = path.dirname(parentPath);
}
}
return undefined;
}

private addProject(project: PythonProject): boolean {
if (this._projects.has(project.uri.toString())) {
return false;
}
this._projects.set(project.uri.toString(), project);
return true;
}
}

let _projectManager: PythonProjectsApi | undefined;
export function getPythonProjectsApi(): PythonProjectsApi {
if (!_projectManager) {
const workspaces: readonly WorkspaceFolder[] = getWorkspaceFolders() ?? [];
_projectManager = new PythonProjectsManagerImpl(workspaces.map((w) => ({ uri: w.uri, name: w.name })));
}
return _projectManager;
}
142 changes: 142 additions & 0 deletions src/client/pythonProjects/projectsView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import {
Disposable,
Event,
EventEmitter,
ProviderResult,
TreeDataProvider,
TreeItem,
TreeView,
Uri,
window,
} from 'vscode';
import { getPythonProjectsApi } from './projectsManager';
import { PythonProject, PythonProjectsApi, PythonProjectsChangedEvent } from './types';
import {
ProjectEnvironmentInfoViewItem,
ProjectEnvironmentViewItem,
ProjectNoEnvironmentViewItem,
ProjectPackageViewItem,
ProjectPackagesViewItem,
ProjectViewItem,
PythonProjectTreeItem,
} from './viewItems/types';

const PROJECT_TREE_VIEW_ID = 'python-projects';

export interface PythonProjectsViewApi extends Disposable {
revealEnvironment(uri: Uri | PythonProject): void;
}

class PythonProjectViewImpl implements PythonProjectsViewApi, TreeDataProvider<PythonProjectTreeItem> {
private disposables: Disposable[] = [];

private _treeView: TreeView<PythonProjectTreeItem> | undefined;

private _treeDataChanged: EventEmitter<void | PythonProjectTreeItem | PythonProjectTreeItem[] | null | undefined>;

private _projectViews: Map<string, ProjectViewItem> = new Map<string, ProjectViewItem>();

private _projectEnvironments: Map<string, ProjectEnvironmentViewItem> = new Map<
string,
ProjectEnvironmentViewItem
>();

constructor(private readonly api: PythonProjectsApi) {
this._treeView = window.createTreeView(PROJECT_TREE_VIEW_ID, { treeDataProvider: this });
this._treeDataChanged = new EventEmitter<
void | PythonProjectTreeItem | PythonProjectTreeItem[] | null | undefined
>();
this.onDidChangeTreeData = this._treeDataChanged.event;
this.disposables.push(
this._treeView,
this._treeDataChanged,
this.api.onDidChangePythonProjects((e) => {
this.onDidChangePythonProjects(e);
}),
);

this.api.pythonProjects.forEach((project) => {
this.addProject(project);
});
}

onDidChangeTreeData?: Event<void | PythonProjectTreeItem | PythonProjectTreeItem[] | null | undefined>;

// eslint-disable-next-line class-methods-use-this
getTreeItem(element: PythonProjectTreeItem): TreeItem {
return element.getTreeItem();
}

getChildren(element?: PythonProjectTreeItem | undefined): ProviderResult<PythonProjectTreeItem[]> {
if (element === undefined) {
return Array.from(this._projectViews.values());
}

if (element.kind === 'project') {
const { project } = element as ProjectViewItem;
const envItem = this._projectEnvironments.get(project.uri.toString());
return envItem ? [envItem] : [new ProjectNoEnvironmentViewItem(project)];
}

if (element.kind === 'environment') {
return [
new ProjectEnvironmentInfoViewItem('Environment Manager: '),
new ProjectEnvironmentInfoViewItem('Package Manager: '),
new ProjectEnvironmentInfoViewItem('Python: '),
new ProjectPackagesViewItem(),
];
}

if (element.kind === 'packages') {
return [new ProjectPackageViewItem('Not Supported')];
}

return [];
}

// eslint-disable-next-line class-methods-use-this
getParent?(element: PythonProjectTreeItem): ProviderResult<PythonProjectTreeItem> {
return element.parent;
}

dispose() {
this.disposables.forEach((d) => d.dispose());
}

revealEnvironment(uri: Uri | PythonProject): void {
const project = uri instanceof Uri ? this.api.getPythonProject(uri) : uri;
if (!project) {
return;
}
const envItem = this._projectEnvironments.get(project.uri.toString());
if (envItem) {
this._treeView?.reveal(envItem);
}
}

private onDidChangePythonProjects(e: PythonProjectsChangedEvent) {
e.added.forEach((project) => this.addProject(project));
e.removed.forEach((project) => this.removeProject(project));
this._treeDataChanged.fire(undefined);
}

private addProject(project: PythonProject) {
const projectView = new ProjectViewItem(project);
this._projectViews.set(project.uri.toString(), projectView);
}

private removeProject(project: PythonProject) {
this._projectViews.delete(project.uri.toString());
}
}

let _projectViews: PythonProjectsViewApi | undefined;
export function getPythonProjectView(): PythonProjectsViewApi {
if (!_projectViews) {
_projectViews = new PythonProjectViewImpl(getPythonProjectsApi());
}
return _projectViews;
}
2 changes: 2 additions & 0 deletions src/client/pythonProjects/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
Loading

0 comments on commit e38c056

Please sign in to comment.