Compare commits

...
9 changed files with 1 additions and 782 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Adding Workspace path adapter and workspace hints logic for tools
-7
View File
@@ -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
View File
@@ -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
-5
View File
@@ -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
-223
View File
@@ -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" },
])
})
})
})
-11
View File
@@ -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))
}