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
40 changes: 38 additions & 2 deletions core/util/ideUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,36 @@ import {
pathToUriPathSegment,
} from "./uri";

/*
Helper function to normalize workspace directory paths to proper file:// URIs.
This handles cases where workspace directories might be returned as plain file paths
instead of URIs, which can occur on some IDE extensions like IntelliJ on Linux.

Examples:
- Input: "/home/user/project" -> Output: "file:///home/user/project"
- Input: "file:///home/user/project" -> Output: "file:///home/user/project"
*/
export function normalizeDirUri(dirPath: string): string {
// If it's already a URI with a scheme, return as-is
if (dirPath.includes("://")) {
return dirPath;
}

// Convert likely absolute filesystem paths to file:// URIs.
// Workspace dirs should already be URIs, but this guards against malformed IDE inputs.
if (dirPath.startsWith("/")) {
return `file:///${pathToUriPathSegment(dirPath)}`;
}

// For Windows paths (C:\ or drive letters)
if (/^[a-zA-Z]:/.test(dirPath)) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: UNC Windows workspace paths are not normalized to file:// URIs, so network-share directories can still bypass the new fix and break relative-path URI resolution.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At core/util/ideUtils.ts, line 31:

<comment>UNC Windows workspace paths are not normalized to `file://` URIs, so network-share directories can still bypass the new fix and break relative-path URI resolution.</comment>

<file context>
@@ -6,6 +6,36 @@ import {
+  }
+
+  // For Windows paths (C:\ or drive letters)
+  if (/^[a-zA-Z]:/.test(dirPath)) {
+    const normalized = dirPath.replaceAll("\\", "/");
+    return `file:///${pathToUriPathSegment(normalized)}`;
</file context>
Fix with Cubic

const normalized = dirPath.replaceAll("\\", "/");
return `file:///${pathToUriPathSegment(normalized)}`;
}

return dirPath;
}

/*
This function takes a relative (to workspace) filepath
And checks each workspace for if it exists or not
Expand All @@ -18,7 +48,10 @@ export async function resolveRelativePathInDir(
): Promise<string | undefined> {
const dirs = dirUriCandidates ?? (await ide.getWorkspaceDirs());
for (const dirUri of dirs) {
const fullUri = joinPathsToUri(dirUri, path);
// Normalize the directory URI to ensure it's a proper file:// URI
// This handles cases where workspace directories might be plain file paths
const normalizedDirUri = normalizeDirUri(dirUri);
const fullUri = joinPathsToUri(normalizedDirUri, path);
if (await ide.fileExists(fullUri)) {
return fullUri;
}
Expand All @@ -39,7 +72,10 @@ export async function inferResolvedUriFromRelativePath(
dirCandidates?: string[],
): Promise<string> {
const relativePath = _relativePath.trim().replaceAll("\\", "/");
const dirs = dirCandidates ?? (await ide.getWorkspaceDirs());
const rawDirs = dirCandidates ?? (await ide.getWorkspaceDirs());

// Normalize all directories to proper file:// URIs
const dirs = rawDirs.map(normalizeDirUri);

if (dirs.length === 0) {
throw new Error("inferResolvedUriFromRelativePath: no dirs provided");
Expand Down
93 changes: 93 additions & 0 deletions core/util/ideUtils.vitest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, expect, it, vi } from "vitest";
import { IDE } from "..";
import {
inferResolvedUriFromRelativePath,
normalizeDirUri,
resolveRelativePathInDir,
} from "./ideUtils";

describe("normalizeDirUri", () => {
it("should pass through file:// URIs unchanged", () => {
expect(normalizeDirUri("file:///home/user/project")).toBe(
"file:///home/user/project",
);
});

it("should convert Linux absolute path to file:// URI", () => {
expect(normalizeDirUri("/home/user/project")).toBe("file:///home/user/project");
});

it("should encode special characters in Linux paths", () => {
expect(normalizeDirUri("/home/user/my project")).toBe("file:///home/user/my%20project");
});

it("should convert Windows drive path to file:// URI", () => {
expect(normalizeDirUri("C:\\Users\\user\\project")).toBe("file:///C%3A/Users/user/project");
});
});

function createMockIde(opts: {
workspaceDirs: string[];
existingFiles?: Set<string>;
}): IDE {
const existingFiles = opts.existingFiles ?? new Set<string>();
return {
getWorkspaceDirs: vi.fn(async () => opts.workspaceDirs),
fileExists: vi.fn(async (uri: string) => existingFiles.has(uri)),
} as unknown as IDE;
}

describe("resolveRelativePathInDir", () => {
it("should resolve a relative path against a proper file:// workspace dir", async () => {
const ide = createMockIde({
workspaceDirs: ["file:///home/user/project"],
existingFiles: new Set(["file:///home/user/project/src/file.ts"]),
});
const result = await resolveRelativePathInDir("src/file.ts", ide);
expect(result).toBe("file:///home/user/project/src/file.ts");
});

it("should resolve when workspace dir is a plain Linux path (bug #11559)", async () => {
const ide = createMockIde({
workspaceDirs: ["/home/user/project"],
existingFiles: new Set(["file:///home/user/project/src/file.ts"]),
});
const result = await resolveRelativePathInDir("src/file.ts", ide);
expect(result).toBe("file:///home/user/project/src/file.ts");
});

it("should NOT produce a double-prefixed path (regression guard)", async () => {
const ide = createMockIde({
workspaceDirs: ["/home/user/project"],
existingFiles: new Set(["file:///home/user/project/src/file.ts"]),
});
const result = await resolveRelativePathInDir("src/file.ts", ide);
expect(result).not.toContain("/home/user/home/");
});
});

describe("inferResolvedUriFromRelativePath", () => {
it("should join relative path to normalized workspace dir", async () => {
const ide = createMockIde({
workspaceDirs: ["file:///home/user/project"],
});
const result = await inferResolvedUriFromRelativePath("src/file.ts", ide);
expect(result).toBe("file:///home/user/project/src/file.ts");
});

it("should produce correct URI when workspace dir is plain path (bug #11559)", async () => {
const ide = createMockIde({
workspaceDirs: ["/home/user/project"],
});
const result = await inferResolvedUriFromRelativePath("src/file.ts", ide);
expect(result).toBe("file:///home/user/project/src/file.ts");
});

it("should NOT produce double-prefixed path (regression guard)", async () => {
const ide = createMockIde({
workspaceDirs: ["/home/user/project"],
});
const result = await inferResolvedUriFromRelativePath("src/file.ts", ide);
expect(result).not.toContain("/home/user/home/");
});
});
Loading