mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add multi-root workspace support
- Updated getWorkspacePath to properly handle multi-root workspaces - Added getAllWorkspacePaths and getWorkspaceFolderForPath utility functions - Fixed isPathOutsideWorkspace to correctly check all workspace folders - Updated Task class to support multiple workspace roots - Modified environment details to show files from all workspace folders - Added comprehensive tests for multi-root workspace scenarios Fixes #8041
This commit is contained in:
parent
2263d86a20
commit
49acdc9a97
6 changed files with 560 additions and 47 deletions
|
|
@ -238,32 +238,69 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
|
|||
}
|
||||
|
||||
if (includeFileDetails) {
|
||||
details += `\n\n# Current Workspace Directory (${cline.cwd.toPosix()}) Files\n`
|
||||
const isDesktop = arePathsEqual(cline.cwd, path.join(os.homedir(), "Desktop"))
|
||||
// Check if we have multiple workspace folders
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders
|
||||
const isMultiRoot = workspaceFolders && workspaceFolders.length > 1
|
||||
|
||||
if (isDesktop) {
|
||||
// Don't want to immediately access desktop since it would show
|
||||
// permission popup.
|
||||
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
|
||||
if (isMultiRoot) {
|
||||
// For multi-root workspaces, show files from all workspace folders
|
||||
details += `\n\n# Multi-Root Workspace Files\n`
|
||||
const maxFilesPerFolder = Math.floor((maxWorkspaceFiles ?? 200) / workspaceFolders.length)
|
||||
|
||||
for (const folder of workspaceFolders) {
|
||||
const folderPath = folder.uri.fsPath
|
||||
const folderName = folder.name
|
||||
details += `\n## ${folderName} (${folderPath.toPosix()})\n`
|
||||
|
||||
const isDesktop = arePathsEqual(folderPath, path.join(os.homedir(), "Desktop"))
|
||||
if (isDesktop) {
|
||||
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)\n"
|
||||
} else if (maxFilesPerFolder === 0) {
|
||||
details += "(Workspace files context disabled. Use list_files to explore if needed.)\n"
|
||||
} else {
|
||||
const [files, didHitLimit] = await listFiles(folderPath, true, maxFilesPerFolder)
|
||||
const { showRooIgnoredFiles = false } = state ?? {}
|
||||
|
||||
const result = formatResponse.formatFilesList(
|
||||
folderPath,
|
||||
files,
|
||||
didHitLimit,
|
||||
cline.rooIgnoreController,
|
||||
showRooIgnoredFiles,
|
||||
)
|
||||
|
||||
details += result + "\n"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const maxFiles = maxWorkspaceFiles ?? 200
|
||||
// Single workspace folder - use existing logic
|
||||
details += `\n\n# Current Workspace Directory (${cline.cwd.toPosix()}) Files\n`
|
||||
const isDesktop = arePathsEqual(cline.cwd, path.join(os.homedir(), "Desktop"))
|
||||
|
||||
// Early return for limit of 0
|
||||
if (maxFiles === 0) {
|
||||
details += "(Workspace files context disabled. Use list_files to explore if needed.)"
|
||||
if (isDesktop) {
|
||||
// Don't want to immediately access desktop since it would show
|
||||
// permission popup.
|
||||
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
|
||||
} else {
|
||||
const [files, didHitLimit] = await listFiles(cline.cwd, true, maxFiles)
|
||||
const { showRooIgnoredFiles = false } = state ?? {}
|
||||
const maxFiles = maxWorkspaceFiles ?? 200
|
||||
|
||||
const result = formatResponse.formatFilesList(
|
||||
cline.cwd,
|
||||
files,
|
||||
didHitLimit,
|
||||
cline.rooIgnoreController,
|
||||
showRooIgnoredFiles,
|
||||
)
|
||||
// Early return for limit of 0
|
||||
if (maxFiles === 0) {
|
||||
details += "(Workspace files context disabled. Use list_files to explore if needed.)"
|
||||
} else {
|
||||
const [files, didHitLimit] = await listFiles(cline.cwd, true, maxFiles)
|
||||
const { showRooIgnoredFiles = false } = state ?? {}
|
||||
|
||||
details += result
|
||||
const result = formatResponse.formatFilesList(
|
||||
cline.cwd,
|
||||
files,
|
||||
didHitLimit,
|
||||
cline.rooIgnoreController,
|
||||
showRooIgnoredFiles,
|
||||
)
|
||||
|
||||
details += result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2909,6 +2909,33 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
return this.workspacePath
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all workspace folders for multi-root workspace support
|
||||
* @returns Array of all workspace folder paths
|
||||
*/
|
||||
public get workspaceFolders(): string[] {
|
||||
const folders = vscode.workspace.workspaceFolders
|
||||
if (!folders || folders.length === 0) {
|
||||
return [this.workspacePath]
|
||||
}
|
||||
return folders.map((folder) => folder.uri.fsPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path is within any of the workspace folders
|
||||
* @param filePath The file path to check
|
||||
* @returns true if the path is in a workspace folder, false otherwise
|
||||
*/
|
||||
public isPathInWorkspace(filePath: string): boolean {
|
||||
const absolutePath = path.resolve(filePath)
|
||||
const normalizedPath = path.normalize(absolutePath)
|
||||
|
||||
return this.workspaceFolders.some((folderPath) => {
|
||||
const normalizedFolderPath = path.normalize(folderPath)
|
||||
return normalizedPath === normalizedFolderPath || normalizedPath.startsWith(normalizedFolderPath + path.sep)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Process any queued messages by dequeuing and submitting them.
|
||||
* This ensures that queued user messages are sent when appropriate,
|
||||
|
|
|
|||
|
|
@ -3,37 +3,62 @@
|
|||
import os from "os"
|
||||
import * as path from "path"
|
||||
|
||||
import { arePathsEqual, getReadablePath, getWorkspacePath } from "../path"
|
||||
import {
|
||||
arePathsEqual,
|
||||
getReadablePath,
|
||||
getWorkspacePath,
|
||||
getAllWorkspacePaths,
|
||||
getWorkspaceFolderForPath,
|
||||
} from "../path"
|
||||
|
||||
// Mock modules
|
||||
const mockWorkspaceFolders = vi.fn()
|
||||
const mockGetWorkspaceFolder = vi.fn()
|
||||
const mockActiveTextEditor = vi.fn()
|
||||
|
||||
vi.mock("vscode", () => ({
|
||||
window: {
|
||||
activeTextEditor: {
|
||||
document: {
|
||||
uri: { fsPath: "/test/workspaceFolder/file.ts" },
|
||||
},
|
||||
get activeTextEditor() {
|
||||
return mockActiveTextEditor()
|
||||
},
|
||||
},
|
||||
workspace: {
|
||||
workspaceFolders: [
|
||||
{
|
||||
uri: { fsPath: "/test/workspace" },
|
||||
name: "test",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
getWorkspaceFolder: vi.fn().mockReturnValue({
|
||||
uri: {
|
||||
fsPath: "/test/workspaceFolder",
|
||||
},
|
||||
}),
|
||||
get workspaceFolders() {
|
||||
return mockWorkspaceFolders()
|
||||
},
|
||||
getWorkspaceFolder: mockGetWorkspaceFolder,
|
||||
},
|
||||
}))
|
||||
describe("Path Utilities", () => {
|
||||
const originalPlatform = process.platform
|
||||
// Helper to mock VS Code configuration
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks before each test
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Set default mock values
|
||||
mockWorkspaceFolders.mockReturnValue([
|
||||
{
|
||||
uri: { fsPath: "/test/workspace" },
|
||||
name: "test",
|
||||
index: 0,
|
||||
},
|
||||
])
|
||||
|
||||
mockActiveTextEditor.mockReturnValue({
|
||||
document: {
|
||||
uri: { fsPath: "/test/workspaceFolder/file.ts" },
|
||||
},
|
||||
})
|
||||
|
||||
mockGetWorkspaceFolder.mockReturnValue({
|
||||
uri: {
|
||||
fsPath: "/test/workspaceFolder",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: originalPlatform,
|
||||
|
|
@ -56,14 +81,129 @@ describe("Path Utilities", () => {
|
|||
expect(extendedPath.toPosix()).toBe("\\\\?\\C:\\Very\\Long\\Path")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getWorkspacePath", () => {
|
||||
it("should return the current workspace path", () => {
|
||||
const workspacePath = "/Users/test/project"
|
||||
expect(getWorkspacePath(workspacePath)).toBe("/Users/test/project")
|
||||
it("should return the workspace folder of the active editor", () => {
|
||||
mockActiveTextEditor.mockReturnValue({
|
||||
document: {
|
||||
uri: { fsPath: "/test/workspaceFolder/file.ts" },
|
||||
},
|
||||
})
|
||||
mockGetWorkspaceFolder.mockReturnValue({
|
||||
uri: { fsPath: "/test/workspaceFolder" },
|
||||
})
|
||||
mockWorkspaceFolders.mockReturnValue([{ uri: { fsPath: "/test/workspace" }, name: "workspace", index: 0 }])
|
||||
|
||||
expect(getWorkspacePath()).toBe("/test/workspaceFolder")
|
||||
})
|
||||
|
||||
it("should return undefined when outside a workspace", () => {})
|
||||
it("should return the first workspace folder when no active editor", () => {
|
||||
mockActiveTextEditor.mockReturnValue(undefined)
|
||||
mockGetWorkspaceFolder.mockReturnValue(undefined)
|
||||
mockWorkspaceFolders.mockReturnValue([
|
||||
{ uri: { fsPath: "/test/workspace1" }, name: "workspace1", index: 0 },
|
||||
{ uri: { fsPath: "/test/workspace2" }, name: "workspace2", index: 1 },
|
||||
])
|
||||
|
||||
expect(getWorkspacePath()).toBe("/test/workspace1")
|
||||
})
|
||||
|
||||
it("should return default path when no workspace folders", () => {
|
||||
mockActiveTextEditor.mockReturnValue(undefined)
|
||||
mockGetWorkspaceFolder.mockReturnValue(undefined)
|
||||
mockWorkspaceFolders.mockReturnValue(undefined)
|
||||
|
||||
expect(getWorkspacePath("/default/path")).toBe("/default/path")
|
||||
})
|
||||
|
||||
it("should handle multi-root workspaces correctly", () => {
|
||||
mockWorkspaceFolders.mockReturnValue([
|
||||
{ uri: { fsPath: "/test/frontend" }, name: "frontend", index: 0 },
|
||||
{ uri: { fsPath: "/test/backend" }, name: "backend", index: 1 },
|
||||
])
|
||||
|
||||
// When active editor is in backend folder
|
||||
mockActiveTextEditor.mockReturnValue({
|
||||
document: { uri: { fsPath: "/test/backend/src/app.ts" } },
|
||||
})
|
||||
mockGetWorkspaceFolder.mockReturnValue({
|
||||
uri: { fsPath: "/test/backend" },
|
||||
})
|
||||
|
||||
expect(getWorkspacePath()).toBe("/test/backend")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getAllWorkspacePaths", () => {
|
||||
it("should return all workspace folder paths", () => {
|
||||
mockWorkspaceFolders.mockReturnValue([
|
||||
{ uri: { fsPath: "/test/frontend" }, name: "frontend", index: 0 },
|
||||
{ uri: { fsPath: "/test/backend" }, name: "backend", index: 1 },
|
||||
])
|
||||
|
||||
const paths = getAllWorkspacePaths()
|
||||
expect(paths).toEqual(["/test/frontend", "/test/backend"])
|
||||
})
|
||||
|
||||
it("should return empty array when no workspace folders", () => {
|
||||
mockWorkspaceFolders.mockReturnValue(undefined)
|
||||
|
||||
const paths = getAllWorkspacePaths()
|
||||
expect(paths).toEqual([])
|
||||
})
|
||||
|
||||
it("should handle single workspace folder", () => {
|
||||
mockWorkspaceFolders.mockReturnValue([{ uri: { fsPath: "/test/workspace" }, name: "workspace", index: 0 }])
|
||||
|
||||
const paths = getAllWorkspacePaths()
|
||||
expect(paths).toEqual(["/test/workspace"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("getWorkspaceFolderForPath", () => {
|
||||
beforeEach(() => {
|
||||
mockWorkspaceFolders.mockReturnValue([
|
||||
{ uri: { fsPath: "/test/frontend" }, name: "frontend", index: 0 },
|
||||
{ uri: { fsPath: "/test/backend" }, name: "backend", index: 1 },
|
||||
])
|
||||
})
|
||||
|
||||
it("should return the workspace folder containing the path", () => {
|
||||
expect(getWorkspaceFolderForPath("/test/frontend/src/app.ts")).toBe("/test/frontend")
|
||||
expect(getWorkspaceFolderForPath("/test/backend/src/server.ts")).toBe("/test/backend")
|
||||
})
|
||||
|
||||
it("should return the workspace folder for exact match", () => {
|
||||
expect(getWorkspaceFolderForPath("/test/frontend")).toBe("/test/frontend")
|
||||
expect(getWorkspaceFolderForPath("/test/backend")).toBe("/test/backend")
|
||||
})
|
||||
|
||||
it("should return null for paths outside any workspace", () => {
|
||||
expect(getWorkspaceFolderForPath("/other/path/file.ts")).toBeNull()
|
||||
expect(getWorkspaceFolderForPath("/test/other/file.ts")).toBeNull()
|
||||
})
|
||||
|
||||
it("should return null when no workspace folders", () => {
|
||||
mockWorkspaceFolders.mockReturnValue(undefined)
|
||||
expect(getWorkspaceFolderForPath("/test/frontend/src/app.ts")).toBeNull()
|
||||
})
|
||||
|
||||
it("should handle relative paths by resolving them", () => {
|
||||
// This depends on the current working directory, so we'll use path.resolve
|
||||
const resolvedPath = path.resolve("./src/app.ts")
|
||||
const result = getWorkspaceFolderForPath("./src/app.ts")
|
||||
|
||||
// The result should be based on the resolved path
|
||||
if (resolvedPath.startsWith("/test/frontend")) {
|
||||
expect(result).toBe("/test/frontend")
|
||||
} else if (resolvedPath.startsWith("/test/backend")) {
|
||||
expect(result).toBe("/test/backend")
|
||||
} else {
|
||||
expect(result).toBeNull()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("arePathsEqual", () => {
|
||||
describe("on Windows", () => {
|
||||
beforeEach(() => {
|
||||
|
|
|
|||
233
src/utils/__tests__/pathUtils.spec.ts
Normal file
233
src/utils/__tests__/pathUtils.spec.ts
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
// npx vitest utils/__tests__/pathUtils.spec.ts
|
||||
|
||||
import * as path from "path"
|
||||
import { isPathOutsideWorkspace, getContainingWorkspaceFolder } from "../pathUtils"
|
||||
|
||||
// Mock vscode module
|
||||
const mockWorkspaceFolders = vi.fn()
|
||||
|
||||
vi.mock("vscode", () => ({
|
||||
workspace: {
|
||||
get workspaceFolders() {
|
||||
return mockWorkspaceFolders()
|
||||
},
|
||||
},
|
||||
WorkspaceFolder: class {
|
||||
constructor(
|
||||
public uri: { fsPath: string },
|
||||
public name: string,
|
||||
public index: number,
|
||||
) {}
|
||||
},
|
||||
}))
|
||||
|
||||
describe("pathUtils", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("isPathOutsideWorkspace", () => {
|
||||
describe("single workspace folder", () => {
|
||||
beforeEach(() => {
|
||||
mockWorkspaceFolders.mockReturnValue([
|
||||
{
|
||||
uri: { fsPath: "/test/workspace" },
|
||||
name: "workspace",
|
||||
index: 0,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should return false for paths inside the workspace", () => {
|
||||
expect(isPathOutsideWorkspace("/test/workspace")).toBe(false)
|
||||
expect(isPathOutsideWorkspace("/test/workspace/src/file.ts")).toBe(false)
|
||||
expect(isPathOutsideWorkspace("/test/workspace/nested/deep/file.ts")).toBe(false)
|
||||
})
|
||||
|
||||
it("should return true for paths outside the workspace", () => {
|
||||
expect(isPathOutsideWorkspace("/test/other")).toBe(true)
|
||||
expect(isPathOutsideWorkspace("/other/path/file.ts")).toBe(true)
|
||||
expect(isPathOutsideWorkspace("/test")).toBe(true) // Parent directory
|
||||
})
|
||||
|
||||
it("should handle relative paths by resolving them", () => {
|
||||
const originalCwd = process.cwd()
|
||||
|
||||
// Mock process.cwd to be inside workspace
|
||||
const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue("/test/workspace/src")
|
||||
|
||||
expect(isPathOutsideWorkspace("./file.ts")).toBe(false) // Resolves to /test/workspace/src/file.ts
|
||||
expect(isPathOutsideWorkspace("../file.ts")).toBe(false) // Resolves to /test/workspace/file.ts
|
||||
expect(isPathOutsideWorkspace("../../other/file.ts")).toBe(true) // Resolves to /test/other/file.ts
|
||||
|
||||
cwdSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe("multi-root workspace", () => {
|
||||
beforeEach(() => {
|
||||
mockWorkspaceFolders.mockReturnValue([
|
||||
{
|
||||
uri: { fsPath: "/test/frontend" },
|
||||
name: "frontend",
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
uri: { fsPath: "/test/backend" },
|
||||
name: "backend",
|
||||
index: 1,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should return false for paths in any workspace folder", () => {
|
||||
// Frontend paths
|
||||
expect(isPathOutsideWorkspace("/test/frontend")).toBe(false)
|
||||
expect(isPathOutsideWorkspace("/test/frontend/src/app.ts")).toBe(false)
|
||||
|
||||
// Backend paths
|
||||
expect(isPathOutsideWorkspace("/test/backend")).toBe(false)
|
||||
expect(isPathOutsideWorkspace("/test/backend/src/server.ts")).toBe(false)
|
||||
})
|
||||
|
||||
it("should return true for paths outside all workspace folders", () => {
|
||||
expect(isPathOutsideWorkspace("/test/other")).toBe(true)
|
||||
expect(isPathOutsideWorkspace("/other/path")).toBe(true)
|
||||
expect(isPathOutsideWorkspace("/test")).toBe(true) // Parent of both workspaces
|
||||
})
|
||||
|
||||
it("should handle paths between workspace folders correctly", () => {
|
||||
// This is the key test for the bug fix
|
||||
// Paths in sibling workspace folders should NOT be considered outside
|
||||
expect(isPathOutsideWorkspace("/test/frontend/src/app.ts")).toBe(false)
|
||||
expect(isPathOutsideWorkspace("/test/backend/src/server.ts")).toBe(false)
|
||||
|
||||
// But paths that are truly outside should still be blocked
|
||||
expect(isPathOutsideWorkspace("/test/shared/lib.ts")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("no workspace folders", () => {
|
||||
beforeEach(() => {
|
||||
mockWorkspaceFolders.mockReturnValue(undefined)
|
||||
})
|
||||
|
||||
it("should return true for all paths when no workspace", () => {
|
||||
expect(isPathOutsideWorkspace("/any/path")).toBe(true)
|
||||
expect(isPathOutsideWorkspace("./relative/path")).toBe(true)
|
||||
expect(isPathOutsideWorkspace("/test/workspace/file.ts")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("empty workspace folders array", () => {
|
||||
beforeEach(() => {
|
||||
mockWorkspaceFolders.mockReturnValue([])
|
||||
})
|
||||
|
||||
it("should return true for all paths when workspace array is empty", () => {
|
||||
expect(isPathOutsideWorkspace("/any/path")).toBe(true)
|
||||
expect(isPathOutsideWorkspace("./relative/path")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("path normalization", () => {
|
||||
beforeEach(() => {
|
||||
mockWorkspaceFolders.mockReturnValue([
|
||||
{
|
||||
uri: { fsPath: "/test/workspace" },
|
||||
name: "workspace",
|
||||
index: 0,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should normalize paths with .. and .", () => {
|
||||
expect(isPathOutsideWorkspace("/test/workspace/../workspace/src/file.ts")).toBe(false)
|
||||
expect(isPathOutsideWorkspace("/test/workspace/./src/file.ts")).toBe(false)
|
||||
expect(isPathOutsideWorkspace("/test/workspace/src/../src/file.ts")).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle trailing slashes", () => {
|
||||
expect(isPathOutsideWorkspace("/test/workspace/")).toBe(false)
|
||||
expect(isPathOutsideWorkspace("/test/workspace/src/")).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getContainingWorkspaceFolder", () => {
|
||||
describe("single workspace folder", () => {
|
||||
const workspaceFolder = {
|
||||
uri: { fsPath: "/test/workspace" },
|
||||
name: "workspace",
|
||||
index: 0,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockWorkspaceFolders.mockReturnValue([workspaceFolder])
|
||||
})
|
||||
|
||||
it("should return the workspace folder for paths inside it", () => {
|
||||
expect(getContainingWorkspaceFolder("/test/workspace")).toEqual(workspaceFolder)
|
||||
expect(getContainingWorkspaceFolder("/test/workspace/src/file.ts")).toEqual(workspaceFolder)
|
||||
})
|
||||
|
||||
it("should return undefined for paths outside the workspace", () => {
|
||||
expect(getContainingWorkspaceFolder("/test/other")).toBeUndefined()
|
||||
expect(getContainingWorkspaceFolder("/other/path")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("multi-root workspace", () => {
|
||||
const frontendFolder = {
|
||||
uri: { fsPath: "/test/frontend" },
|
||||
name: "frontend",
|
||||
index: 0,
|
||||
}
|
||||
|
||||
const backendFolder = {
|
||||
uri: { fsPath: "/test/backend" },
|
||||
name: "backend",
|
||||
index: 1,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockWorkspaceFolders.mockReturnValue([frontendFolder, backendFolder])
|
||||
})
|
||||
|
||||
it("should return the correct workspace folder for each path", () => {
|
||||
expect(getContainingWorkspaceFolder("/test/frontend/src/app.ts")).toEqual(frontendFolder)
|
||||
expect(getContainingWorkspaceFolder("/test/backend/src/server.ts")).toEqual(backendFolder)
|
||||
})
|
||||
|
||||
it("should return the exact workspace folder for root paths", () => {
|
||||
expect(getContainingWorkspaceFolder("/test/frontend")).toEqual(frontendFolder)
|
||||
expect(getContainingWorkspaceFolder("/test/backend")).toEqual(backendFolder)
|
||||
})
|
||||
|
||||
it("should return undefined for paths outside all workspaces", () => {
|
||||
expect(getContainingWorkspaceFolder("/test/other")).toBeUndefined()
|
||||
expect(getContainingWorkspaceFolder("/test")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("no workspace folders", () => {
|
||||
beforeEach(() => {
|
||||
mockWorkspaceFolders.mockReturnValue(undefined)
|
||||
})
|
||||
|
||||
it("should return undefined when no workspace folders", () => {
|
||||
expect(getContainingWorkspaceFolder("/any/path")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("empty workspace folders array", () => {
|
||||
beforeEach(() => {
|
||||
mockWorkspaceFolders.mockReturnValue([])
|
||||
})
|
||||
|
||||
it("should return undefined when workspace array is empty", () => {
|
||||
expect(getContainingWorkspaceFolder("/any/path")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -107,13 +107,61 @@ export const toRelativePath = (filePath: string, cwd: string) => {
|
|||
}
|
||||
|
||||
export const getWorkspacePath = (defaultCwdPath = "") => {
|
||||
const cwdPath = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || defaultCwdPath
|
||||
// For multi-root workspaces, we should consider all workspace folders
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders
|
||||
|
||||
// If no workspace folders, return the default
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
return defaultCwdPath
|
||||
}
|
||||
|
||||
// If there's an active editor, use its workspace folder
|
||||
const currentFileUri = vscode.window.activeTextEditor?.document.uri
|
||||
if (currentFileUri) {
|
||||
const workspaceFolder = vscode.workspace.getWorkspaceFolder(currentFileUri)
|
||||
return workspaceFolder?.uri.fsPath || cwdPath
|
||||
if (workspaceFolder) {
|
||||
return workspaceFolder.uri.fsPath
|
||||
}
|
||||
}
|
||||
return cwdPath
|
||||
|
||||
// For multi-root workspaces, we'll return the first folder as the default
|
||||
// but the system should be aware of all folders
|
||||
return workspaceFolders[0].uri.fsPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all workspace paths in a multi-root workspace
|
||||
* @returns Array of all workspace folder paths
|
||||
*/
|
||||
export const getAllWorkspacePaths = (): string[] => {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
return []
|
||||
}
|
||||
return workspaceFolders.map((folder) => folder.uri.fsPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path belongs to any workspace folder
|
||||
* @param filePath The file path to check
|
||||
* @returns The workspace folder path if the file belongs to a workspace, null otherwise
|
||||
*/
|
||||
export const getWorkspaceFolderForPath = (filePath: string): string | null => {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const normalizedFilePath = path.normalize(filePath)
|
||||
|
||||
for (const folder of workspaceFolders) {
|
||||
const folderPath = path.normalize(folder.uri.fsPath)
|
||||
if (normalizedFilePath === folderPath || normalizedFilePath.startsWith(folderPath + path.sep)) {
|
||||
return folder.uri.fsPath
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const getWorkspacePathForContext = (contextPath?: string): string => {
|
||||
|
|
|
|||
|
|
@ -14,11 +14,39 @@ export function isPathOutsideWorkspace(filePath: string): boolean {
|
|||
|
||||
// Normalize and resolve the path to handle .. and . components correctly
|
||||
const absolutePath = path.resolve(filePath)
|
||||
const normalizedPath = path.normalize(absolutePath)
|
||||
|
||||
// Check if the path is within any workspace folder
|
||||
return !vscode.workspace.workspaceFolders.some((folder) => {
|
||||
const folderPath = folder.uri.fsPath
|
||||
// This properly supports multi-root workspaces by checking against ALL folders
|
||||
for (const folder of vscode.workspace.workspaceFolders) {
|
||||
const folderPath = path.normalize(folder.uri.fsPath)
|
||||
|
||||
// Path is inside a workspace if it equals the workspace path or is a subfolder
|
||||
return absolutePath === folderPath || absolutePath.startsWith(folderPath + path.sep)
|
||||
if (normalizedPath === folderPath || normalizedPath.startsWith(folderPath + path.sep)) {
|
||||
return false // Path is inside this workspace folder
|
||||
}
|
||||
}
|
||||
|
||||
// Path is not in any workspace folder
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the workspace folder that contains the given path
|
||||
* @param filePath The file path to check
|
||||
* @returns The workspace folder URI if found, undefined otherwise
|
||||
*/
|
||||
export function getContainingWorkspaceFolder(filePath: string): vscode.WorkspaceFolder | undefined {
|
||||
if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const absolutePath = path.resolve(filePath)
|
||||
const normalizedPath = path.normalize(absolutePath)
|
||||
|
||||
// Find the workspace folder that contains this path
|
||||
return vscode.workspace.workspaceFolders.find((folder) => {
|
||||
const folderPath = path.normalize(folder.uri.fsPath)
|
||||
return normalizedPath === folderPath || normalizedPath.startsWith(folderPath + path.sep)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue