mirror of
https://github.com/cline/cline.git
synced 2026-09-17 17:45:33 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12d5eff84b | ||
|
|
ebbc6f4097 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adding Workspace path adapter and workspace hints logic for tools
|
||||
@@ -18,7 +18,6 @@ import { ToolUse } from "../assistant-message"
|
||||
import { ContextManager } from "../context/context-management/ContextManager"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { StateManager } from "../storage/StateManager"
|
||||
import { WorkspaceRootManager } from "../workspace"
|
||||
import { ToolResponse } from "."
|
||||
import { MessageStateHandler } from "./message-state"
|
||||
import { TaskState } from "./TaskState"
|
||||
@@ -90,10 +89,6 @@ export class ToolExecutor {
|
||||
private strictPlanModeEnabled: boolean,
|
||||
private yoloModeToggled: boolean,
|
||||
|
||||
// Workspace Management
|
||||
private workspaceManager: WorkspaceRootManager | undefined,
|
||||
private isMultiRootEnabled: boolean,
|
||||
|
||||
// Callbacks to the Task (Entity)
|
||||
private say: (
|
||||
type: ClineSay,
|
||||
@@ -138,8 +133,6 @@ export class ToolExecutor {
|
||||
strictPlanModeEnabled: this.strictPlanModeEnabled,
|
||||
yoloModeToggled: this.yoloModeToggled,
|
||||
cwd: this.cwd,
|
||||
workspaceManager: this.workspaceManager,
|
||||
isMultiRootEnabled: this.isMultiRootEnabled,
|
||||
taskState: this.taskState,
|
||||
messageState: this.messageStateHandler,
|
||||
api: this.api,
|
||||
|
||||
+1
-65
@@ -417,8 +417,6 @@ export class Task {
|
||||
this.mode,
|
||||
strictPlanModeEnabled,
|
||||
yoloModeToggled,
|
||||
this.workspaceManager,
|
||||
featureFlagsService.getMultiRootEnabled(),
|
||||
this.say.bind(this),
|
||||
this.ask.bind(this),
|
||||
this.saveCheckpointCallback.bind(this),
|
||||
@@ -2377,72 +2375,10 @@ export class Task {
|
||||
return [processedUserContent, environmentDetails, clinerulesError]
|
||||
}
|
||||
|
||||
/**
|
||||
* Format workspace roots section for multi-root workspaces
|
||||
*/
|
||||
private formatWorkspaceRootsSection(): string {
|
||||
const isMultiRootEnabled = featureFlagsService.getMultiRootEnabled()
|
||||
const hasWorkspaceManager = !!this.workspaceManager
|
||||
const roots = hasWorkspaceManager ? this.workspaceManager!.getRoots() : []
|
||||
|
||||
// Only show workspace roots if multi-root is enabled and there are multiple roots
|
||||
if (!isMultiRootEnabled || roots.length <= 1) {
|
||||
return ""
|
||||
}
|
||||
|
||||
let section = "\n\n# Workspace Roots"
|
||||
|
||||
// Format each root with its name, path, and VCS info
|
||||
for (const root of roots) {
|
||||
const name = root.name || path.basename(root.path)
|
||||
const vcs = root.vcs ? ` (${String(root.vcs)})` : ""
|
||||
section += `\n- ${name}: ${root.path}${vcs}`
|
||||
}
|
||||
|
||||
// Add primary workspace information
|
||||
const primary = this.workspaceManager!.getPrimaryRoot()
|
||||
const primaryName = this.getPrimaryWorkspaceName(primary)
|
||||
section += `\n\nPrimary workspace: ${primaryName}`
|
||||
|
||||
return section
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display name for the primary workspace
|
||||
*/
|
||||
private getPrimaryWorkspaceName(primary?: ReturnType<WorkspaceRootManager["getRoots"]>[0]): string {
|
||||
if (primary?.name) {
|
||||
return primary.name
|
||||
}
|
||||
if (primary?.path) {
|
||||
return path.basename(primary.path)
|
||||
}
|
||||
return path.basename(this.cwd)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the file details header based on workspace configuration
|
||||
*/
|
||||
private formatFileDetailsHeader(): string {
|
||||
const isMultiRootEnabled = featureFlagsService.getMultiRootEnabled()
|
||||
const roots = this.workspaceManager?.getRoots() || []
|
||||
|
||||
if (isMultiRootEnabled && roots.length > 1) {
|
||||
const primary = this.workspaceManager?.getPrimaryRoot()
|
||||
const primaryName = this.getPrimaryWorkspaceName(primary)
|
||||
return `\n\n# Current Working Directory (Primary: ${primaryName}) Files\n`
|
||||
} else {
|
||||
return `\n\n# Current Working Directory (${this.cwd.toPosix()}) Files\n`
|
||||
}
|
||||
}
|
||||
|
||||
async getEnvironmentDetails(includeFileDetails: boolean = false) {
|
||||
const host = await HostProvider.env.getHostVersion({})
|
||||
let details = ""
|
||||
|
||||
// Workspace roots (multi-root)
|
||||
details += this.formatWorkspaceRootsSection()
|
||||
|
||||
// It could be useful for cline to know if the user went from one or no file to another between messages, so we always include this context
|
||||
details += `\n\n# ${host.platform} Visible Files`
|
||||
const visibleFilePaths = (await HostProvider.window.getVisibleTabs({})).paths.map((absolutePath) =>
|
||||
@@ -2566,7 +2502,7 @@ export class Task {
|
||||
details += `\n\n# Current Time\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})`
|
||||
|
||||
if (includeFileDetails) {
|
||||
details += this.formatFileDetailsHeader()
|
||||
details += `\n\n# Current Working Directory (${this.cwd.toPosix()}) Files\n`
|
||||
const isDesktop = arePathsEqual(this.cwd, getDesktopDir())
|
||||
if (isDesktop) {
|
||||
// don't want to immediately access desktop since it would show permission popup
|
||||
|
||||
@@ -13,7 +13,6 @@ import type { Mode } from "@shared/storage/types"
|
||||
import type { ClineDefaultTool } from "@shared/tools"
|
||||
import type { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
import * as vscode from "vscode"
|
||||
import { WorkspaceRootManager } from "@/core/workspace"
|
||||
import type { ContextManager } from "../../../context/context-management/ContextManager"
|
||||
import type { StateManager } from "../../../storage/StateManager"
|
||||
import type { MessageStateHandler } from "../../message-state"
|
||||
@@ -35,10 +34,6 @@ export interface TaskConfig {
|
||||
yoloModeToggled: boolean
|
||||
context: vscode.ExtensionContext
|
||||
|
||||
// Multi-workspace support (optional for backward compatibility)
|
||||
workspaceManager?: WorkspaceRootManager
|
||||
isMultiRootEnabled?: boolean
|
||||
|
||||
// State management
|
||||
taskState: TaskState
|
||||
messageState: MessageStateHandler
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
/**
|
||||
* WorkspacePathAdapter - Utility for resolving paths in single or multi-workspace environments
|
||||
*
|
||||
* This adapter provides a unified interface for path resolution that works with both
|
||||
* single-root (legacy) and multi-root workspace configurations. It encapsulates the
|
||||
* logic for determining which workspace a path belongs to and resolving relative paths
|
||||
* to their absolute equivalents.
|
||||
*/
|
||||
|
||||
import * as path from "path"
|
||||
import { resolveWorkspacePath } from "./WorkspaceResolver"
|
||||
import type { WorkspaceRootManager } from "./WorkspaceRootManager"
|
||||
|
||||
export interface WorkspaceAdapterConfig {
|
||||
cwd: string
|
||||
isMultiRootEnabled?: boolean
|
||||
workspaceManager?: WorkspaceRootManager
|
||||
}
|
||||
|
||||
export class WorkspacePathAdapter {
|
||||
constructor(private config: WorkspaceAdapterConfig) {}
|
||||
|
||||
/**
|
||||
* Resolves a path using either single-root or multi-root logic
|
||||
*
|
||||
* @param relativePath - The path to resolve (can be relative or absolute)
|
||||
* @param workspaceHint - Optional hint for which workspace to use (name or path)
|
||||
* @returns The resolved absolute path
|
||||
*/
|
||||
resolvePath(relativePath: string, workspaceHint?: string): string {
|
||||
// Single-root mode (backward compatible)
|
||||
if (!this.config.isMultiRootEnabled || !this.config.workspaceManager) {
|
||||
return resolveWorkspacePath(this.config.cwd, relativePath, "WorkspacePathAdapter")
|
||||
}
|
||||
|
||||
// Multi-root mode
|
||||
const manager = this.config.workspaceManager as WorkspaceRootManager
|
||||
|
||||
// If absolute path, find which workspace it belongs to
|
||||
if (path.isAbsolute(relativePath)) {
|
||||
// Already absolute, just validate it belongs to a workspace
|
||||
const root = manager.resolvePathToRoot(relativePath)
|
||||
if (!root) {
|
||||
// Path doesn't belong to any workspace, but return it anyway
|
||||
console.warn(`[WorkspacePathAdapter] Absolute path ${relativePath} doesn't belong to any workspace`)
|
||||
}
|
||||
return relativePath
|
||||
}
|
||||
|
||||
// If hint provided, try to use that workspace
|
||||
if (workspaceHint) {
|
||||
// Try by name first
|
||||
let root = manager.getRootByName(workspaceHint)
|
||||
|
||||
// If not found by name, try to find a root that contains the hint path
|
||||
if (!root) {
|
||||
const roots = manager.getRoots()
|
||||
root = roots.find((r) => r.path === workspaceHint || r.path.includes(workspaceHint))
|
||||
}
|
||||
|
||||
if (root) {
|
||||
return path.join(root.path, relativePath)
|
||||
}
|
||||
|
||||
console.warn(`[WorkspacePathAdapter] Workspace hint '${workspaceHint}' not found, using primary workspace`)
|
||||
}
|
||||
|
||||
// Default to primary workspace
|
||||
const primaryRoot = manager.getPrimaryRoot()
|
||||
if (primaryRoot) {
|
||||
return path.join(primaryRoot.path, relativePath)
|
||||
}
|
||||
|
||||
// Fallback to cwd if no roots (shouldn't happen, but defensive)
|
||||
console.warn(`[WorkspacePathAdapter] No workspace roots found, falling back to cwd`)
|
||||
return resolveWorkspacePath(this.config.cwd, relativePath, "WorkspacePathAdapter-fallback")
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all possible paths for a relative path across all workspaces
|
||||
* Useful for search operations or when checking if a file exists in any workspace
|
||||
*
|
||||
* @param relativePath - The relative path to resolve
|
||||
* @returns Array of absolute paths, one for each workspace
|
||||
*/
|
||||
getAllPossiblePaths(relativePath: string): string[] {
|
||||
// Single-root mode
|
||||
if (!this.config.isMultiRootEnabled || !this.config.workspaceManager) {
|
||||
return [resolveWorkspacePath(this.config.cwd, relativePath, "WorkspacePathAdapter-getAllPaths")]
|
||||
}
|
||||
|
||||
// Multi-root mode
|
||||
const manager = this.config.workspaceManager as WorkspaceRootManager
|
||||
return manager.getRoots().map((root) => path.join(root.path, relativePath))
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which workspace a given absolute path belongs to
|
||||
*
|
||||
* @param absolutePath - The absolute path to check
|
||||
* @returns The workspace root that contains this path, or undefined if not in any workspace
|
||||
*/
|
||||
getWorkspaceForPath(absolutePath: string): { name: string; path: string } | undefined {
|
||||
// Single-root mode
|
||||
if (!this.config.isMultiRootEnabled || !this.config.workspaceManager) {
|
||||
// In single-root, check if path is within cwd
|
||||
if (absolutePath.startsWith(this.config.cwd)) {
|
||||
return {
|
||||
name: path.basename(this.config.cwd),
|
||||
path: this.config.cwd,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Multi-root mode
|
||||
const manager = this.config.workspaceManager as WorkspaceRootManager
|
||||
const root = manager.resolvePathToRoot(absolutePath)
|
||||
if (root) {
|
||||
return {
|
||||
name: root.name || path.basename(root.path),
|
||||
path: root.path,
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the relative path from the appropriate workspace root
|
||||
*
|
||||
* @param absolutePath - The absolute path to make relative
|
||||
* @returns The relative path from its workspace root, or the original path if not in a workspace
|
||||
*/
|
||||
getRelativePath(absolutePath: string): string {
|
||||
// Single-root mode
|
||||
if (!this.config.isMultiRootEnabled || !this.config.workspaceManager) {
|
||||
if (absolutePath.startsWith(this.config.cwd)) {
|
||||
return path.relative(this.config.cwd, absolutePath)
|
||||
}
|
||||
return absolutePath
|
||||
}
|
||||
|
||||
// Multi-root mode
|
||||
const manager = this.config.workspaceManager as WorkspaceRootManager
|
||||
const relativePath = manager.getRelativePathFromRoot(absolutePath)
|
||||
return relativePath || absolutePath
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if multi-root mode is enabled
|
||||
*
|
||||
* @returns True if multi-root mode is enabled and configured
|
||||
*/
|
||||
isMultiRootEnabled(): boolean {
|
||||
return !!(this.config.isMultiRootEnabled && this.config.workspaceManager)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all workspace roots
|
||||
*
|
||||
* @returns Array of workspace root information
|
||||
*/
|
||||
getWorkspaceRoots(): Array<{ name: string; path: string }> {
|
||||
// Single-root mode
|
||||
if (!this.config.isMultiRootEnabled || !this.config.workspaceManager) {
|
||||
return [
|
||||
{
|
||||
name: path.basename(this.config.cwd),
|
||||
path: this.config.cwd,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// Multi-root mode
|
||||
const manager = this.config.workspaceManager as WorkspaceRootManager
|
||||
return manager.getRoots().map((root) => ({
|
||||
name: root.name || path.basename(root.path),
|
||||
path: root.path,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the primary workspace root
|
||||
*
|
||||
* @returns The primary workspace root information
|
||||
*/
|
||||
getPrimaryWorkspace(): { name: string; path: string } {
|
||||
// Single-root mode
|
||||
if (!this.config.isMultiRootEnabled || !this.config.workspaceManager) {
|
||||
return {
|
||||
name: path.basename(this.config.cwd),
|
||||
path: this.config.cwd,
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-root mode
|
||||
const manager = this.config.workspaceManager as WorkspaceRootManager
|
||||
const primaryRoot = manager.getPrimaryRoot()
|
||||
if (primaryRoot) {
|
||||
return {
|
||||
name: primaryRoot.name || path.basename(primaryRoot.path),
|
||||
path: primaryRoot.path,
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback (shouldn't happen)
|
||||
return {
|
||||
name: path.basename(this.config.cwd),
|
||||
path: this.config.cwd,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create a WorkspacePathAdapter
|
||||
*
|
||||
* @param config - The task configuration
|
||||
* @returns A new WorkspacePathAdapter instance
|
||||
*/
|
||||
export function createWorkspacePathAdapter(config: WorkspaceAdapterConfig): WorkspacePathAdapter {
|
||||
return new WorkspacePathAdapter(config)
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
/**
|
||||
* Unit tests for WorkspacePathAdapter
|
||||
* Tests the core functionality of path resolution in single and multi-root workspaces
|
||||
*/
|
||||
|
||||
import { expect } from "chai"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as path from "path"
|
||||
import * as sinon from "sinon"
|
||||
import { createWorkspacePathAdapter, WorkspacePathAdapter } from "../WorkspacePathAdapter"
|
||||
import { VcsType, WorkspaceRoot } from "../WorkspaceRoot"
|
||||
import { WorkspaceRootManager } from "../WorkspaceRootManager"
|
||||
|
||||
describe("WorkspacePathAdapter", () => {
|
||||
let consoleWarnStub: sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
consoleWarnStub = sinon.stub(console, "warn")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
consoleWarnStub.restore()
|
||||
})
|
||||
|
||||
describe("Single-Root Mode", () => {
|
||||
const testCwd = "/test/workspace"
|
||||
let adapter: WorkspacePathAdapter
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new WorkspacePathAdapter({
|
||||
cwd: testCwd,
|
||||
isMultiRootEnabled: false,
|
||||
})
|
||||
})
|
||||
|
||||
it("should resolve relative paths", () => {
|
||||
const result = adapter.resolvePath("src/file.ts")
|
||||
expect(result).to.equal(path.resolve(testCwd, "src/file.ts"))
|
||||
})
|
||||
|
||||
it("should handle absolute paths", () => {
|
||||
const absolutePath = "/absolute/path/file.ts"
|
||||
const result = adapter.resolvePath(absolutePath)
|
||||
expect(result).to.equal(path.resolve(testCwd, absolutePath))
|
||||
})
|
||||
|
||||
it("should get workspace for path within cwd", () => {
|
||||
const workspace = adapter.getWorkspaceForPath("/test/workspace/src/file.ts")
|
||||
expect(workspace).to.deep.equal({
|
||||
name: "workspace",
|
||||
path: testCwd,
|
||||
})
|
||||
})
|
||||
|
||||
it("should return undefined for path outside cwd", () => {
|
||||
const workspace = adapter.getWorkspaceForPath("/other/path/file.ts")
|
||||
expect(workspace).to.be.undefined
|
||||
})
|
||||
|
||||
it("should get relative path from cwd", () => {
|
||||
const result = adapter.getRelativePath("/test/workspace/src/file.ts")
|
||||
expect(result).to.equal("src/file.ts")
|
||||
})
|
||||
|
||||
it("should return single workspace root", () => {
|
||||
const roots = adapter.getWorkspaceRoots()
|
||||
expect(roots).to.have.length(1)
|
||||
expect(roots[0]).to.deep.equal({
|
||||
name: "workspace",
|
||||
path: testCwd,
|
||||
})
|
||||
})
|
||||
|
||||
it("should report multi-root as disabled", () => {
|
||||
expect(adapter.isMultiRootEnabled()).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
describe("Multi-Root Mode", () => {
|
||||
const roots: WorkspaceRoot[] = [
|
||||
{ path: "/workspace/frontend", name: "frontend", vcs: VcsType.Git },
|
||||
{ path: "/workspace/backend", name: "backend", vcs: VcsType.Git },
|
||||
{ path: "/workspace/shared", name: "shared", vcs: VcsType.None },
|
||||
]
|
||||
let adapter: WorkspacePathAdapter
|
||||
let mockManager: WorkspaceRootManager
|
||||
|
||||
beforeEach(() => {
|
||||
mockManager = new WorkspaceRootManager(roots, 0)
|
||||
adapter = new WorkspacePathAdapter({
|
||||
cwd: "/workspace/frontend",
|
||||
isMultiRootEnabled: true,
|
||||
workspaceManager: mockManager,
|
||||
})
|
||||
})
|
||||
|
||||
it("should resolve path with workspace hint by name", () => {
|
||||
const result = adapter.resolvePath("src/index.ts", "backend")
|
||||
expect(result).to.equal("/workspace/backend/src/index.ts")
|
||||
})
|
||||
|
||||
it("should resolve path with workspace hint by path", () => {
|
||||
const result = adapter.resolvePath("src/index.ts", "/workspace/shared")
|
||||
expect(result).to.equal("/workspace/shared/src/index.ts")
|
||||
})
|
||||
|
||||
it("should default to primary workspace without hint", () => {
|
||||
const result = adapter.resolvePath("src/index.ts")
|
||||
expect(result).to.equal("/workspace/frontend/src/index.ts")
|
||||
})
|
||||
|
||||
it("should handle absolute paths belonging to a workspace", () => {
|
||||
const absolutePath = "/workspace/backend/src/api.ts"
|
||||
const result = adapter.resolvePath(absolutePath)
|
||||
expect(result).to.equal(absolutePath)
|
||||
})
|
||||
|
||||
it("should warn for absolute paths outside workspaces", () => {
|
||||
const absolutePath = "/other/path/file.ts"
|
||||
const result = adapter.resolvePath(absolutePath)
|
||||
|
||||
expect(result).to.equal(absolutePath)
|
||||
expect(consoleWarnStub.calledOnce).to.be.true
|
||||
expect(consoleWarnStub.firstCall.args[0]).to.include("doesn't belong to any workspace")
|
||||
})
|
||||
|
||||
it("should get all possible paths across workspaces", () => {
|
||||
const paths = adapter.getAllPossiblePaths("src/config.ts")
|
||||
expect(paths).to.have.length(3)
|
||||
expect(paths).to.deep.equal([
|
||||
"/workspace/frontend/src/config.ts",
|
||||
"/workspace/backend/src/config.ts",
|
||||
"/workspace/shared/src/config.ts",
|
||||
])
|
||||
})
|
||||
|
||||
it("should identify workspace for path", () => {
|
||||
const workspace = adapter.getWorkspaceForPath("/workspace/backend/src/api.ts")
|
||||
expect(workspace).to.deep.equal({
|
||||
name: "backend",
|
||||
path: "/workspace/backend",
|
||||
})
|
||||
})
|
||||
|
||||
it("should get relative path from appropriate workspace", () => {
|
||||
const result = adapter.getRelativePath("/workspace/backend/src/api.ts")
|
||||
expect(result).to.equal("src/api.ts")
|
||||
})
|
||||
|
||||
it("should return all workspace roots", () => {
|
||||
const workspaceRoots = adapter.getWorkspaceRoots()
|
||||
expect(workspaceRoots).to.have.length(3)
|
||||
expect(workspaceRoots[0].name).to.equal("frontend")
|
||||
expect(workspaceRoots[1].name).to.equal("backend")
|
||||
expect(workspaceRoots[2].name).to.equal("shared")
|
||||
})
|
||||
|
||||
it("should get primary workspace", () => {
|
||||
const primary = adapter.getPrimaryWorkspace()
|
||||
expect(primary).to.deep.equal({
|
||||
name: "frontend",
|
||||
path: "/workspace/frontend",
|
||||
})
|
||||
})
|
||||
|
||||
it("should warn for invalid workspace hint", () => {
|
||||
const result = adapter.resolvePath("src/file.ts", "nonexistent")
|
||||
|
||||
expect(result).to.equal("/workspace/frontend/src/file.ts") // Falls back to primary
|
||||
expect(consoleWarnStub.calledOnce).to.be.true
|
||||
expect(consoleWarnStub.firstCall.args[0]).to.include("not found")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("should handle empty workspace manager gracefully", () => {
|
||||
const mockManager = new WorkspaceRootManager([], 0)
|
||||
const adapter = new WorkspacePathAdapter({
|
||||
cwd: "/fallback",
|
||||
isMultiRootEnabled: true,
|
||||
workspaceManager: mockManager,
|
||||
})
|
||||
|
||||
const result = adapter.resolvePath("src/file.ts")
|
||||
expect(result).to.include("/fallback/src/file.ts")
|
||||
expect(consoleWarnStub.called).to.be.true
|
||||
})
|
||||
|
||||
it("should handle paths with special characters", () => {
|
||||
const adapter = new WorkspacePathAdapter({
|
||||
cwd: "/test/workspace",
|
||||
isMultiRootEnabled: false,
|
||||
})
|
||||
|
||||
const specialPath = "src/file with spaces & symbols!.ts"
|
||||
const result = adapter.resolvePath(specialPath)
|
||||
expect(result).to.equal(path.resolve("/test/workspace", specialPath))
|
||||
})
|
||||
})
|
||||
|
||||
describe("Factory Function", () => {
|
||||
it("should create adapter using factory function", () => {
|
||||
const adapter = createWorkspacePathAdapter({
|
||||
cwd: "/test/workspace",
|
||||
isMultiRootEnabled: false,
|
||||
})
|
||||
|
||||
expect(adapter).to.be.instanceOf(WorkspacePathAdapter)
|
||||
expect(adapter.isMultiRootEnabled()).to.be.false
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,154 +0,0 @@
|
||||
import { expect } from "chai"
|
||||
import { describe, it } from "mocha"
|
||||
import {
|
||||
addWorkspaceHint,
|
||||
hasWorkspaceHint,
|
||||
parseMultipleWorkspacePaths,
|
||||
parseWorkspaceInlinePath,
|
||||
removeWorkspaceHint,
|
||||
} from "../../../core/workspace/utils/parseWorkspaceInlinePath"
|
||||
|
||||
describe("parseWorkspaceInlinePath", () => {
|
||||
describe("basic parsing", () => {
|
||||
it("should parse path with workspace hint", () => {
|
||||
const result = parseWorkspaceInlinePath("@frontend:src/index.ts")
|
||||
expect(result).to.deep.equal({
|
||||
workspaceHint: "frontend",
|
||||
relPath: "src/index.ts",
|
||||
})
|
||||
})
|
||||
|
||||
it("should parse path without workspace hint", () => {
|
||||
const result = parseWorkspaceInlinePath("src/index.ts")
|
||||
expect(result).to.deep.equal({
|
||||
workspaceHint: undefined,
|
||||
relPath: "src/index.ts",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle workspace names with hyphens", () => {
|
||||
const result = parseWorkspaceInlinePath("@my-frontend-app:package.json")
|
||||
expect(result).to.deep.equal({
|
||||
workspaceHint: "my-frontend-app",
|
||||
relPath: "package.json",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle workspace names with underscores", () => {
|
||||
const result = parseWorkspaceInlinePath("@backend_service:src/main.py")
|
||||
expect(result).to.deep.equal({
|
||||
workspaceHint: "backend_service",
|
||||
relPath: "src/main.py",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle paths with multiple colons", () => {
|
||||
const result = parseWorkspaceInlinePath("@backend:src/config:prod.json")
|
||||
expect(result).to.deep.equal({
|
||||
workspaceHint: "backend",
|
||||
relPath: "src/config:prod.json",
|
||||
})
|
||||
})
|
||||
|
||||
it("should trim whitespace", () => {
|
||||
const result = parseWorkspaceInlinePath("@ frontend : src/index.ts ")
|
||||
expect(result).to.deep.equal({
|
||||
workspaceHint: "frontend",
|
||||
relPath: "src/index.ts",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle empty string", () => {
|
||||
const result = parseWorkspaceInlinePath("")
|
||||
expect(result).to.deep.equal({
|
||||
workspaceHint: undefined,
|
||||
relPath: "",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle null/undefined", () => {
|
||||
const result = parseWorkspaceInlinePath(null as any)
|
||||
expect(result).to.deep.equal({
|
||||
workspaceHint: undefined,
|
||||
relPath: "",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle @ without colon", () => {
|
||||
const result = parseWorkspaceInlinePath("@frontend")
|
||||
expect(result).to.deep.equal({
|
||||
workspaceHint: undefined,
|
||||
relPath: "@frontend",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle colon without @", () => {
|
||||
const result = parseWorkspaceInlinePath("frontend:src/index.ts")
|
||||
expect(result).to.deep.equal({
|
||||
workspaceHint: undefined,
|
||||
relPath: "frontend:src/index.ts",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle @ at the end", () => {
|
||||
const result = parseWorkspaceInlinePath("src/index.ts@")
|
||||
expect(result).to.deep.equal({
|
||||
workspaceHint: undefined,
|
||||
relPath: "src/index.ts@",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("hasWorkspaceHint", () => {
|
||||
it("should return true for paths with hints", () => {
|
||||
expect(hasWorkspaceHint("@frontend:src/index.ts")).to.be.true
|
||||
expect(hasWorkspaceHint("@backend:package.json")).to.be.true
|
||||
})
|
||||
|
||||
it("should return false for paths without hints", () => {
|
||||
expect(hasWorkspaceHint("src/index.ts")).to.be.false
|
||||
expect(hasWorkspaceHint("@frontend")).to.be.false
|
||||
expect(hasWorkspaceHint("frontend:src")).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
describe("addWorkspaceHint", () => {
|
||||
it("should add hint to path without hint", () => {
|
||||
const result = addWorkspaceHint("frontend", "src/index.ts")
|
||||
expect(result).to.equal("@frontend:src/index.ts")
|
||||
})
|
||||
|
||||
it("should replace existing hint", () => {
|
||||
const result = addWorkspaceHint("backend", "@frontend:src/index.ts")
|
||||
expect(result).to.equal("@backend:src/index.ts")
|
||||
})
|
||||
})
|
||||
|
||||
describe("removeWorkspaceHint", () => {
|
||||
it("should remove hint from path with hint", () => {
|
||||
const result = removeWorkspaceHint("@frontend:src/index.ts")
|
||||
expect(result).to.equal("src/index.ts")
|
||||
})
|
||||
|
||||
it("should return original path if no hint", () => {
|
||||
const result = removeWorkspaceHint("src/index.ts")
|
||||
expect(result).to.equal("src/index.ts")
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseMultipleWorkspacePaths", () => {
|
||||
it("should parse multiple paths", () => {
|
||||
const paths = ["@frontend:src/index.ts", "package.json", "@backend:src/server.js"]
|
||||
|
||||
const results = parseMultipleWorkspacePaths(paths)
|
||||
|
||||
expect(results).to.deep.equal([
|
||||
{ workspaceHint: "frontend", relPath: "src/index.ts" },
|
||||
{ workspaceHint: undefined, relPath: "package.json" },
|
||||
{ workspaceHint: "backend", relPath: "src/server.js" },
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -2,17 +2,6 @@
|
||||
* Workspace module exports for multi-workspace support
|
||||
*/
|
||||
|
||||
// Export workspace path parsing utilities
|
||||
export type { ParsedWorkspacePath } from "./utils/parseWorkspaceInlinePath"
|
||||
export {
|
||||
addWorkspaceHint,
|
||||
hasWorkspaceHint,
|
||||
parseMultipleWorkspacePaths,
|
||||
parseWorkspaceInlinePath,
|
||||
removeWorkspaceHint,
|
||||
} from "./utils/parseWorkspaceInlinePath"
|
||||
export type { WorkspaceAdapterConfig } from "./WorkspacePathAdapter"
|
||||
export { createWorkspacePathAdapter, WorkspacePathAdapter } from "./WorkspacePathAdapter"
|
||||
export {
|
||||
getWorkspaceBasename,
|
||||
isWorkspaceTraceEnabled,
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
/**
|
||||
* parseWorkspaceInlinePath - Utility for parsing workspace-prefixed paths
|
||||
*
|
||||
* This utility extracts workspace hints from paths using the @workspace:path syntax.
|
||||
* This allows tools to target specific workspaces in multi-root environments.
|
||||
*
|
||||
* Examples:
|
||||
* "@frontend:src/index.ts" -> { workspaceHint: "frontend", relPath: "src/index.ts" }
|
||||
* "@backend:package.json" -> { workspaceHint: "backend", relPath: "package.json" }
|
||||
* "src/index.ts" -> { workspaceHint: undefined, relPath: "src/index.ts" }
|
||||
* "@my-app:src/components/Button.tsx" -> { workspaceHint: "my-app", relPath: "src/components/Button.tsx" }
|
||||
*/
|
||||
|
||||
export interface ParsedWorkspacePath {
|
||||
/**
|
||||
* The workspace hint extracted from the path (if any)
|
||||
* This can be a workspace name or partial path to match
|
||||
*/
|
||||
workspaceHint?: string
|
||||
|
||||
/**
|
||||
* The relative path after removing the workspace prefix
|
||||
*/
|
||||
relPath: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a path that may contain a workspace hint prefix
|
||||
*
|
||||
* @param value - The input path that may contain @workspace: prefix
|
||||
* @returns Parsed result with optional workspace hint and the relative path
|
||||
*/
|
||||
export function parseWorkspaceInlinePath(value: string): ParsedWorkspacePath {
|
||||
// Handle null/undefined/empty inputs
|
||||
if (!value) {
|
||||
return { workspaceHint: undefined, relPath: value || "" }
|
||||
}
|
||||
|
||||
// Regex to match @workspace:path pattern
|
||||
// Captures:
|
||||
// - Group 1: workspace name (anything except colon)
|
||||
// - Group 2: the path after the colon
|
||||
const match = value.match(/^@([^:]+):(.+)$/)
|
||||
|
||||
if (match) {
|
||||
const [, workspaceHint, relPath] = match
|
||||
return {
|
||||
workspaceHint: workspaceHint.trim(),
|
||||
relPath: relPath.trim(),
|
||||
}
|
||||
}
|
||||
|
||||
// No workspace hint found, return original value as relative path
|
||||
return { workspaceHint: undefined, relPath: value }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path contains a workspace hint
|
||||
*
|
||||
* @param value - The path to check
|
||||
* @returns True if the path contains a workspace hint
|
||||
*/
|
||||
export function hasWorkspaceHint(value: string): boolean {
|
||||
return /^@[^:]+:/.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a workspace hint to a path
|
||||
*
|
||||
* @param workspaceName - The workspace name to add as hint
|
||||
* @param path - The relative path
|
||||
* @returns The path with workspace hint prefix
|
||||
*/
|
||||
export function addWorkspaceHint(workspaceName: string, path: string): string {
|
||||
// Remove any existing hint first
|
||||
const { relPath } = parseWorkspaceInlinePath(path)
|
||||
return `@${workspaceName}:${relPath}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove workspace hint from a path if present
|
||||
*
|
||||
* @param value - The path that may contain a workspace hint
|
||||
* @returns The path without workspace hint
|
||||
*/
|
||||
export function removeWorkspaceHint(value: string): string {
|
||||
const { relPath } = parseWorkspaceInlinePath(value)
|
||||
return relPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse multiple paths that may contain workspace hints
|
||||
* Useful for batch operations
|
||||
*
|
||||
* @param paths - Array of paths that may contain workspace hints
|
||||
* @returns Array of parsed results
|
||||
*/
|
||||
export function parseMultipleWorkspacePaths(paths: string[]): ParsedWorkspacePath[] {
|
||||
return paths.map((path) => parseWorkspaceInlinePath(path))
|
||||
}
|
||||
Reference in New Issue
Block a user