mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
add multiple workspaces support (#1725)
feat: add multiple workspaces support - Add getWorkspacePath function to centralize workspace directory path retrieval - Use the new workspace directory retrieval logic in Cline, Mentions, ClineProvider, and WorkspaceTracker - Update WorkspaceFile on tab switch and prevent redundant updates by checking prevWorkSpacePath - Fix the bug that loads the contents of the previous tab when quickly switching tabs - Optimize getWorkspacePath return value for better reliability Co-authored-by: xiong <yueminxiong.xym@alibaba-inc.com>
This commit is contained in:
parent
a4853f2faa
commit
23af4c2609
12 changed files with 369 additions and 79 deletions
5
.changeset/fast-taxis-speak.md
Normal file
5
.changeset/fast-taxis-speak.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
add multiple workspaces support
|
||||
|
|
@ -78,9 +78,7 @@ import { DiffStrategy, getDiffStrategy } from "./diff/DiffStrategy"
|
|||
import { insertGroups } from "./diff/insert-groups"
|
||||
import { telemetryService } from "../services/telemetry/TelemetryService"
|
||||
import { validateToolUse, isToolAllowedForMode, ToolName } from "./mode-validator"
|
||||
|
||||
const cwd =
|
||||
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
|
||||
import { getWorkspacePath } from "../utils/path"
|
||||
|
||||
type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
|
||||
type UserContent = Array<Anthropic.Messages.ContentBlockParam>
|
||||
|
|
@ -118,7 +116,9 @@ export type ClineOptions = {
|
|||
export class Cline extends EventEmitter<ClineEvents> {
|
||||
readonly taskId: string
|
||||
readonly instanceId: string
|
||||
|
||||
get cwd() {
|
||||
return getWorkspacePath(path.join(os.homedir(), "Desktop"))
|
||||
}
|
||||
// Subtasks
|
||||
readonly rootTask: Cline | undefined = undefined
|
||||
readonly parentTask: Cline | undefined = undefined
|
||||
|
|
@ -195,7 +195,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
throw new Error("Either historyItem or task/images must be provided")
|
||||
}
|
||||
|
||||
this.rooIgnoreController = new RooIgnoreController(cwd)
|
||||
this.rooIgnoreController = new RooIgnoreController(this.cwd)
|
||||
this.rooIgnoreController.initialize().catch((error) => {
|
||||
console.error("Failed to initialize RooIgnoreController:", error)
|
||||
})
|
||||
|
|
@ -211,7 +211,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
this.diffEnabled = enableDiff ?? false
|
||||
this.fuzzyMatchThreshold = fuzzyMatchThreshold ?? 1.0
|
||||
this.providerRef = new WeakRef(provider)
|
||||
this.diffViewProvider = new DiffViewProvider(cwd)
|
||||
this.diffViewProvider = new DiffViewProvider(this.cwd)
|
||||
this.enableCheckpoints = enableCheckpoints
|
||||
this.checkpointStorage = checkpointStorage
|
||||
|
||||
|
|
@ -844,7 +844,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
newUserContent.push({
|
||||
type: "text",
|
||||
text:
|
||||
`[TASK RESUMPTION] This task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'. If the task has not been completed, retry the last step before interruption and proceed with completing the task.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful and assess whether you should retry. If the last tool was a browser_action, the browser has been closed and you must launch a new browser if needed.${
|
||||
`[TASK RESUMPTION] This task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now '${this.cwd.toPosix()}'. If the task has not been completed, retry the last step before interruption and proceed with completing the task.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful and assess whether you should retry. If the last tool was a browser_action, the browser has been closed and you must launch a new browser if needed.${
|
||||
wasRecent
|
||||
? "\n\nIMPORTANT: If the last tool use was a write_to_file that was interrupted, the file was reverted back to its original state before the interrupted edit, and you do NOT need to re-read the file as you already have its up-to-date contents."
|
||||
: ""
|
||||
|
|
@ -941,11 +941,11 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
async executeCommandTool(command: string, customCwd?: string): Promise<[boolean, ToolResponse]> {
|
||||
let workingDir: string
|
||||
if (!customCwd) {
|
||||
workingDir = cwd
|
||||
workingDir = this.cwd
|
||||
} else if (path.isAbsolute(customCwd)) {
|
||||
workingDir = customCwd
|
||||
} else {
|
||||
workingDir = path.resolve(cwd, customCwd)
|
||||
workingDir = path.resolve(this.cwd, customCwd)
|
||||
}
|
||||
|
||||
// Check if directory exists
|
||||
|
|
@ -1125,7 +1125,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
}
|
||||
return SYSTEM_PROMPT(
|
||||
provider.context,
|
||||
cwd,
|
||||
this.cwd,
|
||||
(this.api.getModel().info.supportsComputerUse ?? false) && (browserToolEnabled ?? true),
|
||||
mcpHub,
|
||||
this.diffStrategy,
|
||||
|
|
@ -1549,7 +1549,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
if (this.diffViewProvider.editType !== undefined) {
|
||||
fileExists = this.diffViewProvider.editType === "modify"
|
||||
} else {
|
||||
const absolutePath = path.resolve(cwd, relPath)
|
||||
const absolutePath = path.resolve(this.cwd, relPath)
|
||||
fileExists = await fileExistsAtPath(absolutePath)
|
||||
this.diffViewProvider.editType = fileExists ? "modify" : "create"
|
||||
}
|
||||
|
|
@ -1579,7 +1579,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
tool: fileExists ? "editedExistingFile" : "newFileCreated",
|
||||
path: getReadablePath(cwd, removeClosingTag("path", relPath)),
|
||||
path: getReadablePath(this.cwd, removeClosingTag("path", relPath)),
|
||||
}
|
||||
try {
|
||||
if (block.partial) {
|
||||
|
|
@ -1696,7 +1696,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
"user_feedback_diff",
|
||||
JSON.stringify({
|
||||
tool: fileExists ? "editedExistingFile" : "newFileCreated",
|
||||
path: getReadablePath(cwd, relPath),
|
||||
path: getReadablePath(this.cwd, relPath),
|
||||
diff: userEdits,
|
||||
} satisfies ClineSayTool),
|
||||
)
|
||||
|
|
@ -1732,7 +1732,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
tool: "appliedDiff",
|
||||
path: getReadablePath(cwd, removeClosingTag("path", relPath)),
|
||||
path: getReadablePath(this.cwd, removeClosingTag("path", relPath)),
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -1769,7 +1769,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
break
|
||||
}
|
||||
|
||||
const absolutePath = path.resolve(cwd, relPath)
|
||||
const absolutePath = path.resolve(this.cwd, relPath)
|
||||
const fileExists = await fileExistsAtPath(absolutePath)
|
||||
|
||||
if (!fileExists) {
|
||||
|
|
@ -1865,7 +1865,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
"user_feedback_diff",
|
||||
JSON.stringify({
|
||||
tool: fileExists ? "editedExistingFile" : "newFileCreated",
|
||||
path: getReadablePath(cwd, relPath),
|
||||
path: getReadablePath(this.cwd, relPath),
|
||||
diff: userEdits,
|
||||
} satisfies ClineSayTool),
|
||||
)
|
||||
|
|
@ -1904,7 +1904,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
tool: "appliedDiff",
|
||||
path: getReadablePath(cwd, removeClosingTag("path", relPath)),
|
||||
path: getReadablePath(this.cwd, removeClosingTag("path", relPath)),
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -1927,7 +1927,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
break
|
||||
}
|
||||
|
||||
const absolutePath = path.resolve(cwd, relPath)
|
||||
const absolutePath = path.resolve(this.cwd, relPath)
|
||||
const fileExists = await fileExistsAtPath(absolutePath)
|
||||
|
||||
if (!fileExists) {
|
||||
|
|
@ -2021,7 +2021,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
|
||||
const userFeedbackDiff = JSON.stringify({
|
||||
tool: "appliedDiff",
|
||||
path: getReadablePath(cwd, relPath),
|
||||
path: getReadablePath(this.cwd, relPath),
|
||||
diff: userEdits,
|
||||
} satisfies ClineSayTool)
|
||||
|
||||
|
|
@ -2051,7 +2051,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
tool: "appliedDiff",
|
||||
path: getReadablePath(cwd, removeClosingTag("path", relPath)),
|
||||
path: getReadablePath(this.cwd, removeClosingTag("path", relPath)),
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -2078,7 +2078,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
break
|
||||
}
|
||||
|
||||
const absolutePath = path.resolve(cwd, relPath)
|
||||
const absolutePath = path.resolve(this.cwd, relPath)
|
||||
const fileExists = await fileExistsAtPath(absolutePath)
|
||||
|
||||
if (!fileExists) {
|
||||
|
|
@ -2183,7 +2183,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
"user_feedback_diff",
|
||||
JSON.stringify({
|
||||
tool: fileExists ? "editedExistingFile" : "newFileCreated",
|
||||
path: getReadablePath(cwd, relPath),
|
||||
path: getReadablePath(this.cwd, relPath),
|
||||
diff: userEdits,
|
||||
} satisfies ClineSayTool),
|
||||
)
|
||||
|
|
@ -2216,7 +2216,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
const relPath: string | undefined = block.params.path
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
tool: "readFile",
|
||||
path: getReadablePath(cwd, removeClosingTag("path", relPath)),
|
||||
path: getReadablePath(this.cwd, removeClosingTag("path", relPath)),
|
||||
}
|
||||
try {
|
||||
if (block.partial) {
|
||||
|
|
@ -2242,7 +2242,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
}
|
||||
|
||||
this.consecutiveMistakeCount = 0
|
||||
const absolutePath = path.resolve(cwd, relPath)
|
||||
const absolutePath = path.resolve(this.cwd, relPath)
|
||||
const completeMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
content: absolutePath,
|
||||
|
|
@ -2267,7 +2267,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
const recursive = recursiveRaw?.toLowerCase() === "true"
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
tool: !recursive ? "listFilesTopLevel" : "listFilesRecursive",
|
||||
path: getReadablePath(cwd, removeClosingTag("path", relDirPath)),
|
||||
path: getReadablePath(this.cwd, removeClosingTag("path", relDirPath)),
|
||||
}
|
||||
try {
|
||||
if (block.partial) {
|
||||
|
|
@ -2284,7 +2284,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
const absolutePath = path.resolve(cwd, relDirPath)
|
||||
const absolutePath = path.resolve(this.cwd, relDirPath)
|
||||
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
|
||||
const { showRooIgnoredFiles } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||
const result = formatResponse.formatFilesList(
|
||||
|
|
@ -2314,7 +2314,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
const relDirPath: string | undefined = block.params.path
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
tool: "listCodeDefinitionNames",
|
||||
path: getReadablePath(cwd, removeClosingTag("path", relDirPath)),
|
||||
path: getReadablePath(this.cwd, removeClosingTag("path", relDirPath)),
|
||||
}
|
||||
try {
|
||||
if (block.partial) {
|
||||
|
|
@ -2333,7 +2333,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
const absolutePath = path.resolve(cwd, relDirPath)
|
||||
const absolutePath = path.resolve(this.cwd, relDirPath)
|
||||
const result = await parseSourceCodeForDefinitionsTopLevel(
|
||||
absolutePath,
|
||||
this.rooIgnoreController,
|
||||
|
|
@ -2360,7 +2360,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
const filePattern: string | undefined = block.params.file_pattern
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
tool: "searchFiles",
|
||||
path: getReadablePath(cwd, removeClosingTag("path", relDirPath)),
|
||||
path: getReadablePath(this.cwd, removeClosingTag("path", relDirPath)),
|
||||
regex: removeClosingTag("regex", regex),
|
||||
filePattern: removeClosingTag("file_pattern", filePattern),
|
||||
}
|
||||
|
|
@ -2384,9 +2384,9 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
const absolutePath = path.resolve(cwd, relDirPath)
|
||||
const absolutePath = path.resolve(this.cwd, relDirPath)
|
||||
const results = await regexSearchFiles(
|
||||
cwd,
|
||||
this.cwd,
|
||||
absolutePath,
|
||||
regex,
|
||||
filePattern,
|
||||
|
|
@ -3469,7 +3469,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
if (shouldProcessMentions(block.text)) {
|
||||
return {
|
||||
...block,
|
||||
text: await parseMentions(block.text, cwd, this.urlContentFetcher),
|
||||
text: await parseMentions(block.text, this.cwd, this.urlContentFetcher),
|
||||
}
|
||||
}
|
||||
return block
|
||||
|
|
@ -3478,7 +3478,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
if (shouldProcessMentions(block.content)) {
|
||||
return {
|
||||
...block,
|
||||
content: await parseMentions(block.content, cwd, this.urlContentFetcher),
|
||||
content: await parseMentions(block.content, this.cwd, this.urlContentFetcher),
|
||||
}
|
||||
}
|
||||
return block
|
||||
|
|
@ -3488,7 +3488,11 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
if (contentBlock.type === "text" && shouldProcessMentions(contentBlock.text)) {
|
||||
return {
|
||||
...contentBlock,
|
||||
text: await parseMentions(contentBlock.text, cwd, this.urlContentFetcher),
|
||||
text: await parseMentions(
|
||||
contentBlock.text,
|
||||
this.cwd,
|
||||
this.urlContentFetcher,
|
||||
),
|
||||
}
|
||||
}
|
||||
return contentBlock
|
||||
|
|
@ -3518,7 +3522,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
const visibleFilePaths = vscode.window.visibleTextEditors
|
||||
?.map((editor) => editor.document?.uri?.fsPath)
|
||||
.filter(Boolean)
|
||||
.map((absolutePath) => path.relative(cwd, absolutePath))
|
||||
.map((absolutePath) => path.relative(this.cwd, absolutePath))
|
||||
.slice(0, maxWorkspaceFiles ?? 200)
|
||||
|
||||
// Filter paths through rooIgnoreController
|
||||
|
|
@ -3539,7 +3543,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
.flatMap((group) => group.tabs)
|
||||
.map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
|
||||
.filter(Boolean)
|
||||
.map((absolutePath) => path.relative(cwd, absolutePath).toPosix())
|
||||
.map((absolutePath) => path.relative(this.cwd, absolutePath).toPosix())
|
||||
.slice(0, maxTabs)
|
||||
|
||||
// Filter paths through rooIgnoreController
|
||||
|
|
@ -3582,7 +3586,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
for (const [uri, fileDiagnostics] of diagnostics) {
|
||||
const problems = fileDiagnostics.filter((d) => d.severity === vscode.DiagnosticSeverity.Error)
|
||||
if (problems.length > 0) {
|
||||
diagnosticsDetails += `\n## ${path.relative(cwd, uri.fsPath)}`
|
||||
diagnosticsDetails += `\n## ${path.relative(this.cwd, uri.fsPath)}`
|
||||
for (const diagnostic of problems) {
|
||||
// let severity = diagnostic.severity === vscode.DiagnosticSeverity.Error ? "Error" : "Warning"
|
||||
const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed
|
||||
|
|
@ -3696,7 +3700,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
} = (await this.providerRef.deref()?.getState()) ?? {}
|
||||
const currentMode = mode ?? defaultModeSlug
|
||||
const modeDetails = await getFullModeDetails(currentMode, customModes, customModePrompts, {
|
||||
cwd,
|
||||
cwd: this.cwd,
|
||||
globalCustomInstructions,
|
||||
language: language ?? formatLanguage(vscode.env.language),
|
||||
})
|
||||
|
|
@ -3723,17 +3727,17 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
}
|
||||
|
||||
if (includeFileDetails) {
|
||||
details += `\n\n# Current Working Directory (${cwd.toPosix()}) Files\n`
|
||||
const isDesktop = arePathsEqual(cwd, path.join(os.homedir(), "Desktop"))
|
||||
details += `\n\n# Current Working Directory (${this.cwd.toPosix()}) Files\n`
|
||||
const isDesktop = arePathsEqual(this.cwd, path.join(os.homedir(), "Desktop"))
|
||||
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 maxFiles = maxWorkspaceFiles ?? 200
|
||||
const [files, didHitLimit] = await listFiles(cwd, true, maxFiles)
|
||||
const [files, didHitLimit] = await listFiles(this.cwd, true, maxFiles)
|
||||
const { showRooIgnoredFiles } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||
const result = formatResponse.formatFilesList(
|
||||
cwd,
|
||||
this.cwd,
|
||||
files,
|
||||
didHitLimit,
|
||||
this.rooIgnoreController,
|
||||
|
|
@ -3768,7 +3772,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
}
|
||||
|
||||
try {
|
||||
const workspaceDir = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
const workspaceDir = getWorkspacePath()
|
||||
|
||||
if (!workspaceDir) {
|
||||
log("[Cline#initializeCheckpoints] workspace folder not found, disabling checkpoints")
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as fs from "fs/promises"
|
|||
import { CustomModesSettingsSchema } from "./CustomModesSchema"
|
||||
import { ModeConfig } from "../../shared/modes"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { arePathsEqual } from "../../utils/path"
|
||||
import { arePathsEqual, getWorkspacePath } from "../../utils/path"
|
||||
import { logger } from "../../utils/logging"
|
||||
|
||||
const ROOMODES_FILENAME = ".roomodes"
|
||||
|
|
@ -51,7 +51,7 @@ export class CustomModesManager {
|
|||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
const workspaceRoot = workspaceFolders[0].uri.fsPath
|
||||
const workspaceRoot = getWorkspacePath()
|
||||
const roomodesPath = path.join(workspaceRoot, ROOMODES_FILENAME)
|
||||
const exists = await fileExistsAtPath(roomodesPath)
|
||||
return exists ? roomodesPath : undefined
|
||||
|
|
@ -226,7 +226,7 @@ export class CustomModesManager {
|
|||
logger.error("Failed to update project mode: No workspace folder found", { slug })
|
||||
throw new Error("No workspace folder found for project-specific mode")
|
||||
}
|
||||
const workspaceRoot = workspaceFolders[0].uri.fsPath
|
||||
const workspaceRoot = getWorkspacePath()
|
||||
targetPath = path.join(workspaceRoot, ROOMODES_FILENAME)
|
||||
const exists = await fileExistsAtPath(targetPath)
|
||||
logger.info(`${exists ? "Updating" : "Creating"} project mode in ${ROOMODES_FILENAME}`, {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import * as fs from "fs/promises"
|
|||
import { CustomModesManager } from "../CustomModesManager"
|
||||
import { ModeConfig } from "../../../shared/modes"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
import { getWorkspacePath, arePathsEqual } from "../../../utils/path"
|
||||
|
||||
jest.mock("vscode")
|
||||
jest.mock("fs/promises")
|
||||
jest.mock("../../../utils/fs")
|
||||
jest.mock("../../../utils/path")
|
||||
|
||||
describe("CustomModesManager", () => {
|
||||
let manager: CustomModesManager
|
||||
|
|
@ -37,6 +39,7 @@ describe("CustomModesManager", () => {
|
|||
mockWorkspaceFolders = [{ uri: { fsPath: "/mock/workspace" } }]
|
||||
;(vscode.workspace as any).workspaceFolders = mockWorkspaceFolders
|
||||
;(vscode.workspace.onDidSaveTextDocument as jest.Mock).mockReturnValue({ dispose: jest.fn() })
|
||||
;(getWorkspacePath as jest.Mock).mockReturnValue("/mock/workspace")
|
||||
;(fileExistsAtPath as jest.Mock).mockImplementation(async (path: string) => {
|
||||
return path === mockSettingsPath || path === mockRoomodes
|
||||
})
|
||||
|
|
@ -362,8 +365,11 @@ describe("CustomModesManager", () => {
|
|||
|
||||
it("watches file for changes", async () => {
|
||||
const configPath = path.join(mockStoragePath, "settings", "cline_custom_modes.json")
|
||||
;(fs.readFile as jest.Mock).mockResolvedValue(JSON.stringify({ customModes: [] }))
|
||||
|
||||
;(fs.readFile as jest.Mock).mockResolvedValue(JSON.stringify({ customModes: [] }))
|
||||
;(arePathsEqual as jest.Mock).mockImplementation((path1: string, path2: string) => {
|
||||
return path.normalize(path1) === path.normalize(path2)
|
||||
})
|
||||
// Get the registered callback
|
||||
const registerCall = (vscode.workspace.onDidSaveTextDocument as jest.Mock).mock.calls[0]
|
||||
expect(registerCall).toBeDefined()
|
||||
|
|
|
|||
|
|
@ -27,7 +27,13 @@ const mockVscode = {
|
|||
{
|
||||
uri: { fsPath: "/test/workspace" },
|
||||
},
|
||||
],
|
||||
] as { uri: { fsPath: string } }[] | undefined,
|
||||
getWorkspaceFolder: jest.fn().mockReturnValue("/test/workspace"),
|
||||
fs: {
|
||||
stat: jest.fn(),
|
||||
writeFile: jest.fn(),
|
||||
},
|
||||
openTextDocument: jest.fn().mockResolvedValue({}),
|
||||
},
|
||||
window: {
|
||||
showErrorMessage: mockShowErrorMessage,
|
||||
|
|
@ -36,7 +42,14 @@ const mockVscode = {
|
|||
createTextEditorDecorationType: jest.fn(),
|
||||
createOutputChannel: jest.fn(),
|
||||
createWebviewPanel: jest.fn(),
|
||||
activeTextEditor: undefined,
|
||||
showTextDocument: jest.fn().mockResolvedValue({}),
|
||||
activeTextEditor: undefined as
|
||||
| undefined
|
||||
| {
|
||||
document: {
|
||||
uri: { fsPath: string }
|
||||
}
|
||||
},
|
||||
},
|
||||
commands: {
|
||||
executeCommand: mockExecuteCommand,
|
||||
|
|
@ -64,12 +77,16 @@ const mockVscode = {
|
|||
jest.mock("vscode", () => mockVscode)
|
||||
jest.mock("../../../services/browser/UrlContentFetcher")
|
||||
jest.mock("../../../utils/git")
|
||||
jest.mock("../../../utils/path")
|
||||
|
||||
// Now import the modules that use the mocks
|
||||
import { parseMentions, openMention } from "../index"
|
||||
import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher"
|
||||
import * as git from "../../../utils/git"
|
||||
|
||||
import { getWorkspacePath } from "../../../utils/path"
|
||||
;(getWorkspacePath as jest.Mock).mockReturnValue("/test/workspace")
|
||||
|
||||
describe("mentions", () => {
|
||||
const mockCwd = "/test/workspace"
|
||||
let mockUrlContentFetcher: UrlContentFetcher
|
||||
|
|
@ -83,6 +100,15 @@ describe("mentions", () => {
|
|||
closeBrowser: jest.fn().mockResolvedValue(undefined),
|
||||
urlToMarkdown: jest.fn().mockResolvedValue(""),
|
||||
} as unknown as UrlContentFetcher
|
||||
|
||||
// Reset all vscode mocks
|
||||
mockVscode.workspace.fs.stat.mockReset()
|
||||
mockVscode.workspace.fs.writeFile.mockReset()
|
||||
mockVscode.workspace.openTextDocument.mockReset().mockResolvedValue({})
|
||||
mockVscode.window.showTextDocument.mockReset().mockResolvedValue({})
|
||||
mockVscode.window.showErrorMessage.mockReset()
|
||||
mockExecuteCommand.mockReset()
|
||||
mockOpenExternal.mockReset()
|
||||
})
|
||||
|
||||
describe("parseMentions", () => {
|
||||
|
|
@ -122,11 +148,21 @@ Detailed commit message with multiple lines
|
|||
|
||||
describe("openMention", () => {
|
||||
it("should handle file paths and problems", async () => {
|
||||
// Mock stat to simulate file not existing
|
||||
mockVscode.workspace.fs.stat.mockRejectedValueOnce(new Error("File does not exist"))
|
||||
|
||||
// Call openMention and wait for it to complete
|
||||
await openMention("/path/to/file")
|
||||
|
||||
// Verify error handling
|
||||
expect(mockExecuteCommand).not.toHaveBeenCalled()
|
||||
expect(mockOpenExternal).not.toHaveBeenCalled()
|
||||
expect(mockShowErrorMessage).toHaveBeenCalledWith("Could not open file: File does not exist")
|
||||
expect(mockVscode.window.showErrorMessage).toHaveBeenCalledWith("Could not open file: File does not exist")
|
||||
|
||||
// Reset mocks for next test
|
||||
jest.clearAllMocks()
|
||||
|
||||
// Test problems command
|
||||
await openMention("problems")
|
||||
expect(mockExecuteCommand).toHaveBeenCalledWith("workbench.actions.view.problems")
|
||||
})
|
||||
|
|
@ -135,8 +171,8 @@ Detailed commit message with multiple lines
|
|||
const url = "https://example.com"
|
||||
await openMention(url)
|
||||
const mockUri = mockVscode.Uri.parse(url)
|
||||
expect(mockOpenExternal).toHaveBeenCalled()
|
||||
const calledArg = mockOpenExternal.mock.calls[0][0]
|
||||
expect(mockVscode.env.openExternal).toHaveBeenCalled()
|
||||
const calledArg = mockVscode.env.openExternal.mock.calls[0][0]
|
||||
expect(calledArg).toEqual(
|
||||
expect.objectContaining({
|
||||
scheme: mockUri.scheme,
|
||||
|
|
|
|||
|
|
@ -9,13 +9,14 @@ import { isBinaryFile } from "isbinaryfile"
|
|||
import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
|
||||
import { getCommitInfo, getWorkingState } from "../../utils/git"
|
||||
import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output"
|
||||
import { getWorkspacePath } from "../../utils/path"
|
||||
|
||||
export async function openMention(mention?: string): Promise<void> {
|
||||
if (!mention) {
|
||||
return
|
||||
}
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
const cwd = getWorkspacePath()
|
||||
if (!cwd) {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ import { getNonce } from "./getNonce"
|
|||
import { getUri } from "./getUri"
|
||||
import { telemetryService } from "../../services/telemetry/TelemetryService"
|
||||
import { TelemetrySetting } from "../../shared/TelemetrySetting"
|
||||
import { getWorkspacePath } from "../../utils/path"
|
||||
|
||||
/**
|
||||
* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
|
|
@ -87,7 +88,9 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
private contextProxy: ContextProxy
|
||||
configManager: ConfigManager
|
||||
customModesManager: CustomModesManager
|
||||
|
||||
get cwd() {
|
||||
return getWorkspacePath()
|
||||
}
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
private readonly outputChannel: vscode.OutputChannel,
|
||||
|
|
@ -501,7 +504,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
|
||||
const taskId = historyItem.id
|
||||
const globalStorageDir = this.contextProxy.globalStorageUri.fsPath
|
||||
const workspaceDir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ""
|
||||
const workspaceDir = this.cwd
|
||||
|
||||
const checkpoints: Pick<ClineOptions, "enableCheckpoints" | "checkpointStorage"> = {
|
||||
enableCheckpoints,
|
||||
|
|
@ -1685,7 +1688,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
}
|
||||
break
|
||||
case "searchCommits": {
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
const cwd = this.cwd
|
||||
if (cwd) {
|
||||
try {
|
||||
const commits = await searchCommits(message.query || "", cwd)
|
||||
|
|
@ -1953,7 +1956,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
fuzzyMatchThreshold,
|
||||
Experiments.isEnabled(experiments, EXPERIMENT_IDS.DIFF_STRATEGY),
|
||||
)
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || ""
|
||||
const cwd = this.cwd
|
||||
|
||||
const mode = message.mode ?? defaultModeSlug
|
||||
const customModes = await this.customModesManager.getCustomModes()
|
||||
|
|
@ -2301,13 +2304,10 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
// delete task from the task history state
|
||||
await this.deleteTaskFromState(id)
|
||||
|
||||
// get the base directory of the project
|
||||
const baseDir = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
|
||||
// Delete associated shadow repository or branch.
|
||||
// TODO: Store `workspaceDir` in the `HistoryItem` object.
|
||||
const globalStorageDir = this.contextProxy.globalStorageUri.fsPath
|
||||
const workspaceDir = baseDir ?? ""
|
||||
const workspaceDir = this.cwd
|
||||
|
||||
try {
|
||||
await ShadowCheckpointService.deleteTask({ taskId: id, globalStorageDir, workspaceDir })
|
||||
|
|
@ -2395,7 +2395,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
|
||||
const allowedCommands = vscode.workspace.getConfiguration("roo-cline").get<string[]>("allowedCommands") || []
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || ""
|
||||
const cwd = this.cwd
|
||||
|
||||
return {
|
||||
version: this.context.extension?.packageJSON?.version ?? "",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as path from "path"
|
||||
import * as os from "os"
|
||||
import * as vscode from "vscode"
|
||||
import { arePathsEqual } from "../../utils/path"
|
||||
import { arePathsEqual, getWorkspacePath } from "../../utils/path"
|
||||
|
||||
export async function openImage(dataUri: string) {
|
||||
const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/)
|
||||
|
|
@ -28,7 +28,7 @@ interface OpenFileOptions {
|
|||
export async function openFile(filePath: string, options: OpenFileOptions = {}) {
|
||||
try {
|
||||
// Get workspace root
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
const workspaceRoot = getWorkspacePath()
|
||||
if (!workspaceRoot) {
|
||||
throw new Error("No workspace root found")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ import * as path from "path"
|
|||
import { listFiles } from "../../services/glob/list-files"
|
||||
import { ClineProvider } from "../../core/webview/ClineProvider"
|
||||
import { toRelativePath } from "../../utils/path"
|
||||
import { getWorkspacePath } from "../../utils/path"
|
||||
import { logger } from "../../utils/logging"
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
const MAX_INITIAL_FILES = 1_000
|
||||
|
||||
// Note: this is not a drop-in replacement for listFiles at the start of tasks, since that will be done for Desktops when there is no workspace selected
|
||||
|
|
@ -13,7 +14,12 @@ class WorkspaceTracker {
|
|||
private disposables: vscode.Disposable[] = []
|
||||
private filePaths: Set<string> = new Set()
|
||||
private updateTimer: NodeJS.Timeout | null = null
|
||||
private prevWorkSpacePath: string | undefined
|
||||
private resetTimer: NodeJS.Timeout | null = null
|
||||
|
||||
get cwd() {
|
||||
return getWorkspacePath()
|
||||
}
|
||||
constructor(provider: ClineProvider) {
|
||||
this.providerRef = new WeakRef(provider)
|
||||
this.registerListeners()
|
||||
|
|
@ -21,17 +27,21 @@ class WorkspaceTracker {
|
|||
|
||||
async initializeFilePaths() {
|
||||
// should not auto get filepaths for desktop since it would immediately show permission popup before cline ever creates a file
|
||||
if (!cwd) {
|
||||
if (!this.cwd) {
|
||||
return
|
||||
}
|
||||
const tempCwd = this.cwd
|
||||
const [files, _] = await listFiles(tempCwd, true, MAX_INITIAL_FILES)
|
||||
if (this.prevWorkSpacePath !== tempCwd) {
|
||||
return
|
||||
}
|
||||
const [files, _] = await listFiles(cwd, true, MAX_INITIAL_FILES)
|
||||
files.slice(0, MAX_INITIAL_FILES).forEach((file) => this.filePaths.add(this.normalizeFilePath(file)))
|
||||
this.workspaceDidUpdate()
|
||||
}
|
||||
|
||||
private registerListeners() {
|
||||
const watcher = vscode.workspace.createFileSystemWatcher("**")
|
||||
|
||||
this.prevWorkSpacePath = this.cwd
|
||||
this.disposables.push(
|
||||
watcher.onDidCreate(async (uri) => {
|
||||
await this.addFilePath(uri.fsPath)
|
||||
|
|
@ -50,7 +60,7 @@ class WorkspaceTracker {
|
|||
|
||||
this.disposables.push(watcher)
|
||||
|
||||
this.disposables.push(vscode.window.tabGroups.onDidChangeTabs(() => this.workspaceDidUpdate()))
|
||||
this.disposables.push(vscode.window.tabGroups.onDidChangeTabs(() => this.workspaceDidReset()))
|
||||
}
|
||||
|
||||
private getOpenedTabsInfo() {
|
||||
|
|
@ -62,23 +72,40 @@ class WorkspaceTracker {
|
|||
return {
|
||||
label: tab.label,
|
||||
isActive: tab.isActive,
|
||||
path: toRelativePath(path, cwd || ""),
|
||||
path: toRelativePath(path, this.cwd || ""),
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private async workspaceDidReset() {
|
||||
if (this.resetTimer) {
|
||||
clearTimeout(this.resetTimer)
|
||||
}
|
||||
this.resetTimer = setTimeout(async () => {
|
||||
if (this.prevWorkSpacePath !== this.cwd) {
|
||||
await this.providerRef.deref()?.postMessageToWebview({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: [],
|
||||
openedTabs: this.getOpenedTabsInfo(),
|
||||
})
|
||||
this.filePaths.clear()
|
||||
this.prevWorkSpacePath = this.cwd
|
||||
this.initializeFilePaths()
|
||||
}
|
||||
}, 300) // Debounce for 300ms
|
||||
}
|
||||
|
||||
private workspaceDidUpdate() {
|
||||
if (this.updateTimer) {
|
||||
clearTimeout(this.updateTimer)
|
||||
}
|
||||
|
||||
this.updateTimer = setTimeout(() => {
|
||||
if (!cwd) {
|
||||
if (!this.cwd) {
|
||||
return
|
||||
}
|
||||
|
||||
const relativeFilePaths = Array.from(this.filePaths).map((file) => toRelativePath(file, cwd))
|
||||
const relativeFilePaths = Array.from(this.filePaths).map((file) => toRelativePath(file, this.cwd))
|
||||
this.providerRef.deref()?.postMessageToWebview({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: relativeFilePaths,
|
||||
|
|
@ -89,7 +116,7 @@ class WorkspaceTracker {
|
|||
}
|
||||
|
||||
private normalizeFilePath(filePath: string): string {
|
||||
const resolvedPath = cwd ? path.resolve(cwd, filePath) : path.resolve(filePath)
|
||||
const resolvedPath = this.cwd ? path.resolve(this.cwd, filePath) : path.resolve(filePath)
|
||||
return filePath.endsWith("/") ? resolvedPath + "/" : resolvedPath
|
||||
}
|
||||
|
||||
|
|
@ -123,6 +150,10 @@ class WorkspaceTracker {
|
|||
clearTimeout(this.updateTimer)
|
||||
this.updateTimer = null
|
||||
}
|
||||
if (this.resetTimer) {
|
||||
clearTimeout(this.resetTimer)
|
||||
this.resetTimer = null
|
||||
}
|
||||
this.disposables.forEach((d) => d.dispose())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,25 +2,40 @@ import * as vscode from "vscode"
|
|||
import WorkspaceTracker from "../WorkspaceTracker"
|
||||
import { ClineProvider } from "../../../core/webview/ClineProvider"
|
||||
import { listFiles } from "../../../services/glob/list-files"
|
||||
import { getWorkspacePath } from "../../../utils/path"
|
||||
|
||||
// Mock modules
|
||||
// Mock functions - must be defined before jest.mock calls
|
||||
const mockOnDidCreate = jest.fn()
|
||||
const mockOnDidDelete = jest.fn()
|
||||
const mockOnDidChange = jest.fn()
|
||||
const mockDispose = jest.fn()
|
||||
|
||||
// Store registered tab change callback
|
||||
let registeredTabChangeCallback: (() => Promise<void>) | null = null
|
||||
|
||||
// Mock workspace path
|
||||
jest.mock("../../../utils/path", () => ({
|
||||
getWorkspacePath: jest.fn().mockReturnValue("/test/workspace"),
|
||||
toRelativePath: jest.fn((path, cwd) => path.replace(`${cwd}/`, "")),
|
||||
}))
|
||||
|
||||
// Mock watcher - must be defined after mockDispose but before jest.mock("vscode")
|
||||
const mockWatcher = {
|
||||
onDidCreate: mockOnDidCreate.mockReturnValue({ dispose: mockDispose }),
|
||||
onDidDelete: mockOnDidDelete.mockReturnValue({ dispose: mockDispose }),
|
||||
dispose: mockDispose,
|
||||
}
|
||||
|
||||
// Mock vscode
|
||||
jest.mock("vscode", () => ({
|
||||
window: {
|
||||
tabGroups: {
|
||||
onDidChangeTabs: jest.fn(() => ({ dispose: jest.fn() })),
|
||||
onDidChangeTabs: jest.fn((callback) => {
|
||||
registeredTabChangeCallback = callback
|
||||
return { dispose: mockDispose }
|
||||
}),
|
||||
all: [],
|
||||
},
|
||||
onDidChangeActiveTextEditor: jest.fn(() => ({ dispose: jest.fn() })),
|
||||
},
|
||||
workspace: {
|
||||
workspaceFolders: [
|
||||
|
|
@ -48,6 +63,12 @@ describe("WorkspaceTracker", () => {
|
|||
jest.clearAllMocks()
|
||||
jest.useFakeTimers()
|
||||
|
||||
// Reset all mock implementations
|
||||
registeredTabChangeCallback = null
|
||||
|
||||
// Reset workspace path mock
|
||||
;(getWorkspacePath as jest.Mock).mockReturnValue("/test/workspace")
|
||||
|
||||
// Create provider mock
|
||||
mockProvider = {
|
||||
postMessageToWebview: jest.fn().mockResolvedValue(undefined),
|
||||
|
|
@ -55,6 +76,9 @@ describe("WorkspaceTracker", () => {
|
|||
|
||||
// Create tracker instance
|
||||
workspaceTracker = new WorkspaceTracker(mockProvider)
|
||||
|
||||
// Ensure the tab change callback was registered
|
||||
expect(registeredTabChangeCallback).not.toBeNull()
|
||||
})
|
||||
|
||||
it("should initialize with workspace files", async () => {
|
||||
|
|
@ -159,8 +183,148 @@ describe("WorkspaceTracker", () => {
|
|||
})
|
||||
|
||||
it("should clean up watchers and timers on dispose", () => {
|
||||
// Set up updateTimer
|
||||
const [[callback]] = mockOnDidCreate.mock.calls
|
||||
callback({ fsPath: "/test/workspace/file.ts" })
|
||||
|
||||
workspaceTracker.dispose()
|
||||
expect(mockDispose).toHaveBeenCalled()
|
||||
jest.runAllTimers() // Ensure any pending timers are cleared
|
||||
|
||||
// No more updates should happen after dispose
|
||||
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle workspace path changes when tabs change", async () => {
|
||||
expect(registeredTabChangeCallback).not.toBeNull()
|
||||
|
||||
// Set initial workspace path and create tracker
|
||||
;(getWorkspacePath as jest.Mock).mockReturnValue("/test/workspace")
|
||||
workspaceTracker = new WorkspaceTracker(mockProvider)
|
||||
|
||||
// Clear any initialization calls
|
||||
jest.clearAllMocks()
|
||||
|
||||
// Mock listFiles to return some files
|
||||
const mockFiles = [["/test/new-workspace/file1.ts"], false]
|
||||
;(listFiles as jest.Mock).mockResolvedValue(mockFiles)
|
||||
|
||||
// Change workspace path
|
||||
;(getWorkspacePath as jest.Mock).mockReturnValue("/test/new-workspace")
|
||||
|
||||
// Simulate tab change event
|
||||
await registeredTabChangeCallback!()
|
||||
|
||||
// Run the debounce timer for workspaceDidReset
|
||||
jest.advanceTimersByTime(300)
|
||||
|
||||
// Should clear file paths and reset workspace
|
||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: [],
|
||||
openedTabs: [],
|
||||
})
|
||||
|
||||
// Run all remaining timers to complete initialization
|
||||
await Promise.resolve() // Wait for initializeFilePaths to complete
|
||||
jest.runAllTimers()
|
||||
|
||||
// Should initialize file paths for new workspace
|
||||
expect(listFiles).toHaveBeenCalledWith("/test/new-workspace", true, 1000)
|
||||
jest.runAllTimers()
|
||||
})
|
||||
|
||||
it("should not update file paths if workspace changes during initialization", async () => {
|
||||
// Setup initial workspace path
|
||||
;(getWorkspacePath as jest.Mock).mockReturnValue("/test/workspace")
|
||||
workspaceTracker = new WorkspaceTracker(mockProvider)
|
||||
|
||||
// Clear any initialization calls
|
||||
jest.clearAllMocks()
|
||||
;(mockProvider.postMessageToWebview as jest.Mock).mockClear()
|
||||
|
||||
// Create a promise to control listFiles timing
|
||||
let resolveListFiles: (value: [string[], boolean]) => void
|
||||
const listFilesPromise = new Promise<[string[], boolean]>((resolve) => {
|
||||
resolveListFiles = resolve
|
||||
})
|
||||
|
||||
// Setup listFiles to use our controlled promise
|
||||
;(listFiles as jest.Mock).mockImplementation(() => {
|
||||
// Change workspace path before listFiles resolves
|
||||
;(getWorkspacePath as jest.Mock).mockReturnValue("/test/changed-workspace")
|
||||
return listFilesPromise
|
||||
})
|
||||
|
||||
// Start initialization
|
||||
const initPromise = workspaceTracker.initializeFilePaths()
|
||||
|
||||
// Resolve listFiles after workspace path change
|
||||
resolveListFiles!([["/test/workspace/file1.ts", "/test/workspace/file2.ts"], false])
|
||||
|
||||
// Wait for initialization to complete
|
||||
await initPromise
|
||||
jest.runAllTimers()
|
||||
|
||||
// Should not update file paths because workspace changed during initialization
|
||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||
filePaths: ["/test/workspace/file1.ts", "/test/workspace/file2.ts"],
|
||||
openedTabs: [],
|
||||
type: "workspaceUpdated",
|
||||
})
|
||||
})
|
||||
|
||||
it("should clear resetTimer when calling workspaceDidReset multiple times", async () => {
|
||||
expect(registeredTabChangeCallback).not.toBeNull()
|
||||
|
||||
// Set initial workspace path
|
||||
;(getWorkspacePath as jest.Mock).mockReturnValue("/test/workspace")
|
||||
|
||||
// Create tracker instance to set initial prevWorkSpacePath
|
||||
workspaceTracker = new WorkspaceTracker(mockProvider)
|
||||
|
||||
// Change workspace path to trigger update
|
||||
;(getWorkspacePath as jest.Mock).mockReturnValue("/test/new-workspace")
|
||||
|
||||
// Call workspaceDidReset through tab change event
|
||||
await registeredTabChangeCallback!()
|
||||
|
||||
// Call again before timer completes
|
||||
await registeredTabChangeCallback!()
|
||||
|
||||
// Advance timer
|
||||
jest.advanceTimersByTime(300)
|
||||
|
||||
// Should only have one call to postMessageToWebview
|
||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: [],
|
||||
openedTabs: [],
|
||||
})
|
||||
expect(mockProvider.postMessageToWebview).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should handle dispose with active resetTimer", async () => {
|
||||
expect(registeredTabChangeCallback).not.toBeNull()
|
||||
|
||||
// Mock workspace path change to trigger resetTimer
|
||||
;(getWorkspacePath as jest.Mock)
|
||||
.mockReturnValueOnce("/test/workspace")
|
||||
.mockReturnValueOnce("/test/new-workspace")
|
||||
|
||||
// Trigger resetTimer
|
||||
await registeredTabChangeCallback!()
|
||||
|
||||
// Dispose before timer completes
|
||||
workspaceTracker.dispose()
|
||||
|
||||
// Advance timer
|
||||
jest.advanceTimersByTime(300)
|
||||
|
||||
// Should have called dispose on all disposables
|
||||
expect(mockDispose).toHaveBeenCalled()
|
||||
|
||||
// No postMessage should be called after dispose
|
||||
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,12 +1,37 @@
|
|||
// npx jest src/utils/__tests__/path.test.ts
|
||||
|
||||
import os from "os"
|
||||
import * as path from "path"
|
||||
|
||||
import { arePathsEqual, getReadablePath } from "../path"
|
||||
import { arePathsEqual, getReadablePath, getWorkspacePath } from "../path"
|
||||
|
||||
// Mock modules
|
||||
|
||||
jest.mock("vscode", () => ({
|
||||
window: {
|
||||
activeTextEditor: {
|
||||
document: {
|
||||
uri: { fsPath: "/test/workspaceFolder/file.ts" },
|
||||
},
|
||||
},
|
||||
},
|
||||
workspace: {
|
||||
workspaceFolders: [
|
||||
{
|
||||
uri: { fsPath: "/test/workspace" },
|
||||
name: "test",
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
getWorkspaceFolder: jest.fn().mockReturnValue({
|
||||
uri: {
|
||||
fsPath: "/test/workspaceFolder",
|
||||
},
|
||||
}),
|
||||
},
|
||||
}))
|
||||
describe("Path Utilities", () => {
|
||||
const originalPlatform = process.platform
|
||||
// Helper to mock VS Code configuration
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, "platform", {
|
||||
|
|
@ -30,7 +55,14 @@ 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("/test/workspaceFolder")
|
||||
})
|
||||
|
||||
it("should return undefined when outside a workspace", () => {})
|
||||
})
|
||||
describe("arePathsEqual", () => {
|
||||
describe("on Windows", () => {
|
||||
beforeEach(() => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import * as path from "path"
|
||||
import os from "os"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/*
|
||||
The Node.js 'path' module resolves and normalizes paths differently depending on the platform:
|
||||
|
|
@ -104,3 +105,13 @@ export const toRelativePath = (filePath: string, cwd: string) => {
|
|||
const relativePath = path.relative(cwd, filePath).toPosix()
|
||||
return filePath.endsWith("/") ? relativePath + "/" : relativePath
|
||||
}
|
||||
|
||||
export const getWorkspacePath = (defaultCwdPath = "") => {
|
||||
const cwdPath = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || defaultCwdPath
|
||||
const currentFileUri = vscode.window.activeTextEditor?.document.uri
|
||||
if (currentFileUri) {
|
||||
const workspaceFolder = vscode.workspace.getWorkspaceFolder(currentFileUri)
|
||||
return workspaceFolder?.uri.fsPath || cwdPath
|
||||
}
|
||||
return cwdPath
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue