Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/features/creators/autoFindProjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { traceInfo } from '../../common/logging';
import { showErrorMessage, showQuickPickWithButtons, showWarningMessage } from '../../common/window.apis';
import { findFiles } from '../../common/workspace.apis';
import { PythonProjectManager, PythonProjectsImpl } from '../../internal.api';
import { normalizePath } from '../../common/utils/pathUtils';

function getUniqueUri(uris: Uri[]): {
label: string;
Expand Down Expand Up @@ -73,8 +74,8 @@ export class AutoFindProjects implements PythonProjectCreator {
// Skip this project if:
// 1. There's already a project registered with exactly the same path
// 2. There's already a project registered with this project's parent directory path
const np = path.normalize(p.uri.fsPath);
const nf = path.normalize(uri.fsPath);
const np = normalizePath(p.uri.fsPath);
const nf = normalizePath(uri.fsPath);
const nfp = path.dirname(nf);
return np !== nf && np !== nfp;
}
Expand Down
5 changes: 3 additions & 2 deletions src/features/creators/existingProjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ProjectCreatorString } from '../../common/localize';
import { traceInfo, traceLog } from '../../common/logging';
import { showOpenDialog, showWarningMessage } from '../../common/window.apis';
import { PythonProjectManager, PythonProjectsImpl } from '../../internal.api';
import { normalizePath } from '../../common/utils/pathUtils';

export class ExistingProjects implements PythonProjectCreator {
public readonly name = 'existingProjects';
Expand Down Expand Up @@ -47,8 +48,8 @@ export class ExistingProjects implements PythonProjectCreator {
const p = this.pm.get(uri);
if (p) {
// Skip this project if there's already a project registered with exactly the same path
const np = path.normalize(p.uri.fsPath);
const nf = path.normalize(uri.fsPath);
const np = normalizePath(p.uri.fsPath);
const nf = normalizePath(uri.fsPath);
return np !== nf;
}
return true;
Expand Down
5 changes: 3 additions & 2 deletions src/features/interpreterSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { StopWatch } from '../common/stopWatch';
import { EventNames } from '../common/telemetry/constants';
import { sendTelemetryEvent } from '../common/telemetry/sender';
import { resolveVariables } from '../common/utils/internalVariables';
import { normalizePath } from '../common/utils/pathUtils';
import { showWarningMessage } from '../common/window.apis';
import {
getConfiguration,
Expand Down Expand Up @@ -534,8 +535,8 @@ function getProjectSpecificEnvManager(projectManager: PythonProjectManager, scop
const pw = projectManager.get(scope);
const w = getWorkspaceFolder(scope);
if (pw && w) {
const pwPath = path.resolve(pw.uri.fsPath);
const matching = overrides.find((s) => path.resolve(w.uri.fsPath, s.path) === pwPath);
const pwPath = normalizePath(path.resolve(pw.uri.fsPath));
const matching = overrides.find((s) => normalizePath(path.resolve(w.uri.fsPath, s.path)) === pwPath);
if (matching && matching.envManager && matching.envManager.length > 0) {
return matching.envManager;
}
Expand Down
5 changes: 3 additions & 2 deletions src/features/projectManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
onDidRenameFiles,
} from '../common/workspace.apis';
import { PythonProjectManager, PythonProjectSettings, PythonProjectsImpl } from '../internal.api';
import { normalizePath } from '../common/utils/pathUtils';
import {
addPythonProjectSetting,
EditProjectSettings,
Expand Down Expand Up @@ -276,9 +277,9 @@ export class PythonProjectManagerImpl implements PythonProjectManager {
private findProjectByUri(uri: Uri): PythonProject | undefined {
const _projects = Array.from(this._projects.values()).sort((a, b) => b.uri.fsPath.length - a.uri.fsPath.length);

const normalizedUriPath = path.normalize(uri.fsPath);
const normalizedUriPath = normalizePath(uri.fsPath);
for (const p of _projects) {
const normalizedProjectPath = path.normalize(p.uri.fsPath);
const normalizedProjectPath = normalizePath(p.uri.fsPath);
if (this.isUriMatching(normalizedUriPath, normalizedProjectPath)) {
return p;
}
Expand Down
45 changes: 25 additions & 20 deletions src/features/settings/settingHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PythonProject } from '../../api';
import { DEFAULT_ENV_MANAGER_ID, DEFAULT_PACKAGE_MANAGER_ID, SYSTEM_MANAGER_ID } from '../../common/constants';
import { traceError, traceInfo, traceVerbose, traceWarn } from '../../common/logging';
import { getGlobalPersistentState } from '../../common/persistentState';
import { normalizePath } from '../../common/utils/pathUtils';
import { EventNames } from '../../common/telemetry/constants';
import { sendTelemetryEvent } from '../../common/telemetry/sender';
import * as workspaceApis from '../../common/workspace.apis';
Expand All @@ -20,8 +21,8 @@ function getSettings(
const pw = wm.get(scope);
const w = workspaceApis.getWorkspaceFolder(scope);
if (pw && w) {
const pwPath = path.normalize(pw.uri.fsPath);
return overrides.find((s) => path.resolve(w.uri.fsPath, s.path) === pwPath);
const pwPath = normalizePath(pw.uri.fsPath);
return overrides.find((s) => normalizePath(path.resolve(w.uri.fsPath, s.path)) === pwPath);
}
}
return undefined;
Expand Down Expand Up @@ -140,9 +141,9 @@ export async function setAllManagerSettings(edits: EditAllManagerSettings[]): Pr
const originalOverridesLength = overrides.length;

es.forEach((e) => {
const pwPath = path.normalize(e.project.uri.fsPath);
const isRoot = path.normalize(w.uri.fsPath) === pwPath;
const index = overrides.findIndex((s) => path.resolve(w.uri.fsPath, s.path) === pwPath);
const pwPath = normalizePath(e.project.uri.fsPath);
const isRoot = normalizePath(w.uri.fsPath) === pwPath;
const index = overrides.findIndex((s) => normalizePath(path.resolve(w.uri.fsPath, s.path)) === pwPath);

// For workspace root in single-folder workspaces (no workspaceFile),
// use default settings instead of pythonProjects entries
Expand Down Expand Up @@ -250,8 +251,8 @@ export async function setEnvironmentManager(edits: EditEnvManagerSettings[]): Pr
let projectsModified = false;

es.forEach((e) => {
const pwPath = path.normalize(e.project.uri.fsPath);
const index = overrides.findIndex((s) => path.resolve(w.uri.fsPath, s.path) === pwPath);
const pwPath = normalizePath(e.project.uri.fsPath);
const index = overrides.findIndex((s) => normalizePath(path.resolve(w.uri.fsPath, s.path)) === pwPath);
if (index >= 0) {
overrides[index].envManager = e.envManager;
projectsModified = true;
Expand Down Expand Up @@ -311,8 +312,8 @@ export async function setPackageManager(edits: EditPackageManagerSettings[]): Pr
let projectsModified = false;

es.forEach((e) => {
const pwPath = path.normalize(e.project.uri.fsPath);
const index = overrides.findIndex((s) => path.resolve(w.uri.fsPath, s.path) === pwPath);
const pwPath = normalizePath(e.project.uri.fsPath);
const index = overrides.findIndex((s) => normalizePath(path.resolve(w.uri.fsPath, s.path)) === pwPath);
if (index >= 0) {
overrides[index].packageManager = e.packageManager;
projectsModified = true;
Expand Down Expand Up @@ -367,14 +368,16 @@ export async function addPythonProjectSetting(edits: EditProjectSettings[]): Pro
const overrides = config.get<PythonProjectSettings[]>('pythonProjects', []);
let overridesModified = false;
es.forEach((e) => {
const pwPath = path.normalize(e.project.uri.fsPath);
const isRoot = path.normalize(w.uri.fsPath) === pwPath;
const pwPath = normalizePath(e.project.uri.fsPath);
const isRoot = normalizePath(w.uri.fsPath) === pwPath;

// For workspace root projects in single-folder workspaces, use default settings
// instead of adding to pythonProjects with empty path
if (isRoot && !isMultiroot) {
// Remove existing entry if present (migration from buggy empty path)
const existingIndex = overrides.findIndex((s) => path.resolve(w.uri.fsPath, s.path) === pwPath);
const existingIndex = overrides.findIndex(
(s) => normalizePath(path.resolve(w.uri.fsPath, s.path)) === pwPath,
);
if (existingIndex >= 0) {
overrides.splice(existingIndex, 1);
overridesModified = true;
Expand All @@ -398,9 +401,9 @@ export async function addPythonProjectSetting(edits: EditProjectSettings[]): Pro
const index = overrides.findIndex((s) => {
if (s.workspace) {
// If the workspace is set, check workspace and path in existing overrides
return s.workspace === w.name && path.resolve(w.uri.fsPath, s.path) === pwPath;
return s.workspace === w.name && normalizePath(path.resolve(w.uri.fsPath, s.path)) === pwPath;
}
return path.resolve(w.uri.fsPath, s.path) === pwPath;
return normalizePath(path.resolve(w.uri.fsPath, s.path)) === pwPath;
});
if (index >= 0) {
// Preserve existing manager settings if not explicitly provided
Expand Down Expand Up @@ -454,8 +457,8 @@ export async function removePythonProjectSetting(edits: EditProjectSettings[]):
const config = workspaceApis.getConfiguration('python-envs', w.uri);
const overrides = config.get<PythonProjectSettings[]>('pythonProjects', []);
es.forEach((e) => {
const pwPath = path.normalize(e.project.uri.fsPath);
const index = overrides.findIndex((s) => path.resolve(w.uri.fsPath, s.path) === pwPath);
const pwPath = normalizePath(e.project.uri.fsPath);
const index = overrides.findIndex((s) => normalizePath(path.resolve(w.uri.fsPath, s.path)) === pwPath);
if (index >= 0) {
overrides.splice(index, 1);
}
Expand All @@ -480,8 +483,8 @@ export async function updatePythonProjectSettingPath(oldUri: Uri, newUri: Uri):
// Find the workspace folder that contains the old path
let targetWorkspace: WorkspaceFolder | undefined;
for (const w of workspaceFolders) {
const oldPath = path.normalize(oldUri.fsPath);
if (oldPath.startsWith(path.normalize(w.uri.fsPath))) {
const oldPath = normalizePath(oldUri.fsPath);
if (oldPath.startsWith(normalizePath(w.uri.fsPath))) {
targetWorkspace = w;
break;
}
Expand All @@ -494,9 +497,11 @@ export async function updatePythonProjectSettingPath(oldUri: Uri, newUri: Uri):

const config = workspaceApis.getConfiguration('python-envs', targetWorkspace.uri);
const overrides = config.get<PythonProjectSettings[]>('pythonProjects', []);
const oldNormalizedPath = path.normalize(oldUri.fsPath);
const oldNormalizedPath = normalizePath(oldUri.fsPath);

const index = overrides.findIndex((s) => path.resolve(targetWorkspace!.uri.fsPath, s.path) === oldNormalizedPath);
const index = overrides.findIndex(
(s) => normalizePath(path.resolve(targetWorkspace!.uri.fsPath, s.path)) === oldNormalizedPath,
);
if (index >= 0) {
// Update the path to the new location
const newRelativePath = path.relative(targetWorkspace.uri.fsPath, newUri.fsPath).replace(/\\/g, '/');
Expand Down
5 changes: 3 additions & 2 deletions src/features/terminal/terminalManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
withProgress,
} from '../../common/window.apis';
import { getConfiguration, onDidChangeConfiguration } from '../../common/workspace.apis';
import { normalizePath } from '../../common/utils/pathUtils';
import { isActivatableEnvironment } from '../common/activation';
import { identifyTerminalShell } from '../common/shellDetector';
import { getPythonApi } from '../pythonApi';
Expand Down Expand Up @@ -323,7 +324,7 @@ export class TerminalManagerImpl implements TerminalManager {
environment: PythonEnvironment,
createNew: boolean = false,
): Promise<Terminal> {
const part = terminalKey instanceof Uri ? path.normalize(terminalKey.fsPath) : terminalKey;
const part = terminalKey instanceof Uri ? normalizePath(terminalKey.fsPath) : terminalKey;
const key = `${environment.envId.id}:${part}`;
if (!createNew) {
const terminal = this.dedicatedTerminals.get(key);
Expand Down Expand Up @@ -373,7 +374,7 @@ export class TerminalManagerImpl implements TerminalManager {
createNew: boolean = false,
): Promise<Terminal> {
const uri = project instanceof Uri ? project : project.uri;
const key = `${environment.envId.id}:${path.normalize(uri.fsPath)}`;
const key = `${environment.envId.id}:${normalizePath(uri.fsPath)}`;
if (!createNew) {
const terminal = this.projectTerminals.get(key);
if (terminal) {
Expand Down
4 changes: 2 additions & 2 deletions src/features/views/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import * as path from 'path';
import { PythonProject } from '../../api';
import { getWorkspaceFolder } from '../../common/workspace.apis';
import { normalizePath } from '../../common/utils/pathUtils';

export function removable(project: PythonProject): boolean {
const workspace = getWorkspaceFolder(project.uri);
if (workspace) {
// If the project path is same as the workspace path, then we cannot remove the project.
return path.normalize(workspace?.uri.fsPath).toLowerCase() !== path.normalize(project.uri.fsPath).toLowerCase();
return normalizePath(workspace?.uri.fsPath) !== normalizePath(project.uri.fsPath);
}
return true;
}
72 changes: 72 additions & 0 deletions src/test/features/views/utils.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as assert from 'assert';
import * as sinon from 'sinon';
import { PythonProject } from '../../../api';
import * as platformUtils from '../../../common/utils/platformUtils';
import * as workspaceApis from '../../../common/workspace.apis';
import { removable } from '../../../features/views/utils';

/**
* Builds a minimal PythonProject-like object for the given filesystem path.
*/
function makeProject(fsPath: string): PythonProject {
return { name: fsPath, uri: { fsPath } } as any;
}

/**
* Stubs getWorkspaceFolder to return a workspace folder with the given path.
*/
function stubWorkspaceFolder(getWsStub: sinon.SinonStub, fsPath: string | undefined): void {
getWsStub.returns(fsPath === undefined ? undefined : ({ uri: { fsPath } } as any));
}

suite('Views utils - removable', () => {
let getWorkspaceFolderStub: sinon.SinonStub;
let isWindowsStub: sinon.SinonStub;

setup(() => {
getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder');
isWindowsStub = sinon.stub(platformUtils, 'isWindows');
});

teardown(() => {
sinon.restore();
});

test('returns true when the project has no workspace folder', () => {
stubWorkspaceFolder(getWorkspaceFolderStub, undefined);
assert.strictEqual(removable(makeProject('/home/user/project')), true);
});

test('returns false when project path equals workspace path', () => {
isWindowsStub.returns(false);
stubWorkspaceFolder(getWorkspaceFolderStub, '/home/user/project');
assert.strictEqual(removable(makeProject('/home/user/project')), false);
});

test('returns true when project is nested inside the workspace folder', () => {
isWindowsStub.returns(false);
stubWorkspaceFolder(getWorkspaceFolderStub, '/home/user/workspace');
assert.strictEqual(removable(makeProject('/home/user/workspace/project')), true);
});

test('Windows: matches paths that differ only by drive-letter case', () => {
// Regression: path.normalize() does not lowercase, so 'C:\\ws' and 'c:\\ws'
// would not match and the workspace root would be wrongly reported removable.
isWindowsStub.returns(true);
stubWorkspaceFolder(getWorkspaceFolderStub, 'C:\\Users\\test\\project');
assert.strictEqual(removable(makeProject('c:\\users\\test\\project')), false);
});

test('Windows: matches paths that differ only by slash direction', () => {
isWindowsStub.returns(true);
stubWorkspaceFolder(getWorkspaceFolderStub, 'C:\\Users\\test\\project');
assert.strictEqual(removable(makeProject('C:/Users/test/project')), false);
});

test('non-Windows comparison stays case-sensitive', () => {
isWindowsStub.returns(false);
stubWorkspaceFolder(getWorkspaceFolderStub, '/home/user/Project');
assert.strictEqual(removable(makeProject('/home/user/project')), true);
});
});
Loading