mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat: add per-workspace control for codebase indexing
- Add workspace-level configuration property enableCodebaseIndexing - Update CodeIndexConfigManager to check workspace settings first - Workspace setting overrides global configuration when set - Add comprehensive tests for per-workspace functionality Fixes #8131
This commit is contained in:
parent
94c997c9d6
commit
67662b0f5b
5 changed files with 288 additions and 3 deletions
|
|
@ -407,6 +407,12 @@
|
|||
"maximum": 200,
|
||||
"description": "%settings.codeIndex.embeddingBatchSize.description%"
|
||||
},
|
||||
"roo-cline.enableCodebaseIndexing": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "%settings.enableCodebaseIndexing.description%",
|
||||
"scope": "resource"
|
||||
},
|
||||
"roo-cline.debug": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
|
|
|
|||
|
|
@ -42,5 +42,6 @@
|
|||
"settings.apiRequestTimeout.description": "Maximum time in seconds to wait for API responses (0 = no timeout, 1-3600s, default: 600s). Higher values are recommended for local providers like LM Studio and Ollama that may need more processing time.",
|
||||
"settings.newTaskRequireTodos.description": "Require todos parameter when creating new tasks with the new_task tool",
|
||||
"settings.codeIndex.embeddingBatchSize.description": "The batch size for embedding operations during code indexing. Adjust this based on your API provider's limits. Default is 60.",
|
||||
"settings.enableCodebaseIndexing.description": "Enable codebase indexing for this workspace. When enabled, the extension will index your code for semantic search capabilities. This setting overrides any global configuration.",
|
||||
"settings.debug.description": "Enable debug mode to show additional buttons for viewing API conversation history and UI messages as prettified JSON in temporary files."
|
||||
}
|
||||
|
|
|
|||
260
src/services/code-index/__tests__/per-workspace-indexing.spec.ts
Normal file
260
src/services/code-index/__tests__/per-workspace-indexing.spec.ts
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"
|
||||
import * as vscode from "vscode"
|
||||
import { CodeIndexConfigManager } from "../config-manager"
|
||||
import { ContextProxy } from "../../../core/config/ContextProxy"
|
||||
|
||||
// Mock vscode module
|
||||
vi.mock("vscode", () => ({
|
||||
workspace: {
|
||||
workspaceFolders: undefined as any,
|
||||
getConfiguration: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe("Per-workspace codebase indexing", () => {
|
||||
let mockContextProxy: any
|
||||
let configManager: CodeIndexConfigManager
|
||||
const testWorkspacePath = "/test/workspace"
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Setup mock context proxy
|
||||
mockContextProxy = {
|
||||
getGlobalState: vi.fn(),
|
||||
getSecret: vi.fn(),
|
||||
refreshSecrets: vi.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
|
||||
// Default mock implementations
|
||||
mockContextProxy.getGlobalState.mockReturnValue({
|
||||
codebaseIndexEnabled: false, // Global setting disabled by default
|
||||
codebaseIndexQdrantUrl: "http://localhost:6333",
|
||||
codebaseIndexEmbedderProvider: "openai",
|
||||
})
|
||||
|
||||
mockContextProxy.getSecret.mockImplementation((key: string) => {
|
||||
if (key === "codeIndexOpenAiKey") return "test-key"
|
||||
if (key === "codeIndexQdrantApiKey") return "test-key"
|
||||
return undefined
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("Workspace setting overrides global setting", () => {
|
||||
it("should enable indexing when workspace setting is true and global is false", () => {
|
||||
// Setup workspace folders
|
||||
const mockWorkspaceFolder = {
|
||||
uri: { fsPath: testWorkspacePath },
|
||||
}
|
||||
;(vscode.workspace as any).workspaceFolders = [mockWorkspaceFolder]
|
||||
|
||||
// Mock workspace configuration to return true for the workspace setting
|
||||
const mockConfig = {
|
||||
get: vi.fn().mockReturnValue(true), // Workspace setting enabled
|
||||
}
|
||||
;(vscode.workspace.getConfiguration as any).mockReturnValue(mockConfig)
|
||||
|
||||
// Global setting is false (set in beforeEach)
|
||||
configManager = new CodeIndexConfigManager(mockContextProxy, testWorkspacePath)
|
||||
|
||||
// Workspace setting should override global setting
|
||||
expect(configManager.isFeatureEnabled).toBe(true)
|
||||
expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-cline", mockWorkspaceFolder.uri)
|
||||
expect(mockConfig.get).toHaveBeenCalledWith("enableCodebaseIndexing")
|
||||
})
|
||||
|
||||
it("should disable indexing when workspace setting is false and global is true", () => {
|
||||
// Setup workspace folders
|
||||
const mockWorkspaceFolder = {
|
||||
uri: { fsPath: testWorkspacePath },
|
||||
}
|
||||
;(vscode.workspace as any).workspaceFolders = [mockWorkspaceFolder]
|
||||
|
||||
// Mock workspace configuration to return false for the workspace setting
|
||||
const mockConfig = {
|
||||
get: vi.fn().mockReturnValue(false), // Workspace setting disabled
|
||||
}
|
||||
;(vscode.workspace.getConfiguration as any).mockReturnValue(mockConfig)
|
||||
|
||||
// Set global setting to true
|
||||
mockContextProxy.getGlobalState.mockReturnValue({
|
||||
codebaseIndexEnabled: true, // Global setting enabled
|
||||
codebaseIndexQdrantUrl: "http://localhost:6333",
|
||||
codebaseIndexEmbedderProvider: "openai",
|
||||
})
|
||||
|
||||
configManager = new CodeIndexConfigManager(mockContextProxy, testWorkspacePath)
|
||||
|
||||
// Workspace setting should override global setting
|
||||
expect(configManager.isFeatureEnabled).toBe(false)
|
||||
})
|
||||
|
||||
it("should use global setting when workspace setting is undefined", () => {
|
||||
// Setup workspace folders
|
||||
const mockWorkspaceFolder = {
|
||||
uri: { fsPath: testWorkspacePath },
|
||||
}
|
||||
;(vscode.workspace as any).workspaceFolders = [mockWorkspaceFolder]
|
||||
|
||||
// Mock workspace configuration to return undefined (not set)
|
||||
const mockConfig = {
|
||||
get: vi.fn().mockReturnValue(undefined), // Workspace setting not configured
|
||||
}
|
||||
;(vscode.workspace.getConfiguration as any).mockReturnValue(mockConfig)
|
||||
|
||||
// Set global setting to true
|
||||
mockContextProxy.getGlobalState.mockReturnValue({
|
||||
codebaseIndexEnabled: true, // Global setting enabled
|
||||
codebaseIndexQdrantUrl: "http://localhost:6333",
|
||||
codebaseIndexEmbedderProvider: "openai",
|
||||
})
|
||||
|
||||
configManager = new CodeIndexConfigManager(mockContextProxy, testWorkspacePath)
|
||||
|
||||
// Should fall back to global setting
|
||||
expect(configManager.isFeatureEnabled).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Multiple workspaces", () => {
|
||||
it("should only check setting for the specific workspace", () => {
|
||||
// Setup multiple workspace folders
|
||||
const workspace1 = {
|
||||
uri: { fsPath: "/workspace1" },
|
||||
}
|
||||
const workspace2 = {
|
||||
uri: { fsPath: "/workspace2" },
|
||||
}
|
||||
;(vscode.workspace as any).workspaceFolders = [workspace1, workspace2]
|
||||
|
||||
// Mock different configurations for each workspace
|
||||
const mockConfig1 = {
|
||||
get: vi.fn().mockReturnValue(true), // Workspace 1 enabled
|
||||
}
|
||||
const mockConfig2 = {
|
||||
get: vi.fn().mockReturnValue(false), // Workspace 2 disabled
|
||||
}
|
||||
|
||||
;(vscode.workspace.getConfiguration as any).mockImplementation((section: string, uri: any) => {
|
||||
if (uri.fsPath === "/workspace1") return mockConfig1
|
||||
if (uri.fsPath === "/workspace2") return mockConfig2
|
||||
return { get: vi.fn().mockReturnValue(undefined) }
|
||||
})
|
||||
|
||||
// Test workspace 1
|
||||
const configManager1 = new CodeIndexConfigManager(mockContextProxy, "/workspace1")
|
||||
expect(configManager1.isFeatureEnabled).toBe(true)
|
||||
|
||||
// Test workspace 2
|
||||
const configManager2 = new CodeIndexConfigManager(mockContextProxy, "/workspace2")
|
||||
expect(configManager2.isFeatureEnabled).toBe(false)
|
||||
|
||||
// Verify correct workspace was queried
|
||||
expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-cline", workspace1.uri)
|
||||
expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-cline", workspace2.uri)
|
||||
})
|
||||
})
|
||||
|
||||
describe("No workspace folder", () => {
|
||||
it("should use global setting when no workspace folder is found", () => {
|
||||
// No workspace folders
|
||||
;(vscode.workspace as any).workspaceFolders = []
|
||||
|
||||
// Set global setting to true
|
||||
mockContextProxy.getGlobalState.mockReturnValue({
|
||||
codebaseIndexEnabled: true,
|
||||
codebaseIndexQdrantUrl: "http://localhost:6333",
|
||||
codebaseIndexEmbedderProvider: "openai",
|
||||
})
|
||||
|
||||
// Create config manager without workspace path
|
||||
configManager = new CodeIndexConfigManager(mockContextProxy, undefined)
|
||||
|
||||
// Should use global setting
|
||||
expect(configManager.isFeatureEnabled).toBe(true)
|
||||
expect(vscode.workspace.getConfiguration).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should use global setting when workspace path doesn't match any folder", () => {
|
||||
// Setup workspace folders
|
||||
const mockWorkspaceFolder = {
|
||||
uri: { fsPath: "/different/workspace" },
|
||||
}
|
||||
;(vscode.workspace as any).workspaceFolders = [mockWorkspaceFolder]
|
||||
|
||||
// Set global setting to true
|
||||
mockContextProxy.getGlobalState.mockReturnValue({
|
||||
codebaseIndexEnabled: true,
|
||||
codebaseIndexQdrantUrl: "http://localhost:6333",
|
||||
codebaseIndexEmbedderProvider: "openai",
|
||||
})
|
||||
|
||||
// Create config manager with non-matching workspace path
|
||||
configManager = new CodeIndexConfigManager(mockContextProxy, "/non/matching/path")
|
||||
|
||||
// Should use global setting since workspace doesn't match
|
||||
expect(configManager.isFeatureEnabled).toBe(true)
|
||||
expect(vscode.workspace.getConfiguration).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Configuration reload", () => {
|
||||
it("should pick up workspace setting changes on reload", async () => {
|
||||
// Setup workspace folders
|
||||
const mockWorkspaceFolder = {
|
||||
uri: { fsPath: testWorkspacePath },
|
||||
}
|
||||
;(vscode.workspace as any).workspaceFolders = [mockWorkspaceFolder]
|
||||
|
||||
// Initially workspace setting is false
|
||||
const mockConfig = {
|
||||
get: vi.fn().mockReturnValue(false),
|
||||
}
|
||||
;(vscode.workspace.getConfiguration as any).mockReturnValue(mockConfig)
|
||||
|
||||
configManager = new CodeIndexConfigManager(mockContextProxy, testWorkspacePath)
|
||||
expect(configManager.isFeatureEnabled).toBe(false)
|
||||
|
||||
// Change workspace setting to true
|
||||
mockConfig.get.mockReturnValue(true)
|
||||
|
||||
// Reload configuration
|
||||
await configManager.loadConfiguration()
|
||||
|
||||
// Should pick up the new workspace setting
|
||||
expect(configManager.isFeatureEnabled).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect restart requirement when workspace setting changes", async () => {
|
||||
// Setup workspace folders
|
||||
const mockWorkspaceFolder = {
|
||||
uri: { fsPath: testWorkspacePath },
|
||||
}
|
||||
;(vscode.workspace as any).workspaceFolders = [mockWorkspaceFolder]
|
||||
|
||||
// Initially workspace setting is false
|
||||
const mockConfig = {
|
||||
get: vi.fn().mockReturnValue(false),
|
||||
}
|
||||
;(vscode.workspace.getConfiguration as any).mockReturnValue(mockConfig)
|
||||
|
||||
configManager = new CodeIndexConfigManager(mockContextProxy, testWorkspacePath)
|
||||
expect(configManager.isFeatureEnabled).toBe(false)
|
||||
|
||||
// Change workspace setting to true (and ensure it's configured)
|
||||
mockConfig.get.mockReturnValue(true)
|
||||
|
||||
// Reload configuration
|
||||
const result = await configManager.loadConfiguration()
|
||||
|
||||
// Should require restart when enabling
|
||||
expect(result.requiresRestart).toBe(true)
|
||||
expect(configManager.isFeatureEnabled).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import * as vscode from "vscode"
|
||||
import { ApiHandlerOptions } from "../../shared/api"
|
||||
import { ContextProxy } from "../../core/config/ContextProxy"
|
||||
import { EmbedderProvider } from "./interfaces/manager"
|
||||
|
|
@ -27,7 +28,10 @@ export class CodeIndexConfigManager {
|
|||
private searchMinScore?: number
|
||||
private searchMaxResults?: number
|
||||
|
||||
constructor(private readonly contextProxy: ContextProxy) {
|
||||
constructor(
|
||||
private readonly contextProxy: ContextProxy,
|
||||
private readonly workspacePath?: string,
|
||||
) {
|
||||
// Initialize with current configuration to avoid false restart triggers
|
||||
this._loadAndSetConfiguration()
|
||||
}
|
||||
|
|
@ -44,6 +48,19 @@ export class CodeIndexConfigManager {
|
|||
* This eliminates code duplication between initializeWithCurrentConfig() and loadConfiguration().
|
||||
*/
|
||||
private _loadAndSetConfiguration(): void {
|
||||
// Check workspace-level setting first
|
||||
let workspaceIndexingEnabled: boolean | undefined
|
||||
if (this.workspacePath) {
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.find(
|
||||
(folder) => folder.uri.fsPath === this.workspacePath,
|
||||
)
|
||||
if (workspaceFolder) {
|
||||
workspaceIndexingEnabled = vscode.workspace
|
||||
.getConfiguration("roo-cline", workspaceFolder.uri)
|
||||
.get<boolean>("enableCodebaseIndexing")
|
||||
}
|
||||
}
|
||||
|
||||
// Load configuration from storage
|
||||
const codebaseIndexConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? {
|
||||
codebaseIndexEnabled: false,
|
||||
|
|
@ -81,7 +98,8 @@ export class CodeIndexConfigManager {
|
|||
const openRouterSpecificProvider = codebaseIndexConfig.codebaseIndexOpenRouterSpecificProvider ?? ""
|
||||
|
||||
// Update instance variables with configuration
|
||||
this.codebaseIndexEnabled = codebaseIndexEnabled ?? false
|
||||
// Use workspace setting if available, otherwise fall back to global setting
|
||||
this.codebaseIndexEnabled = workspaceIndexingEnabled ?? codebaseIndexEnabled ?? false
|
||||
this.qdrantUrl = codebaseIndexQdrantUrl
|
||||
this.qdrantApiKey = qdrantApiKey ?? ""
|
||||
this.searchMinScore = codebaseIndexSearchMinScore
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export class CodeIndexManager {
|
|||
public async initialize(contextProxy: ContextProxy): Promise<{ requiresRestart: boolean }> {
|
||||
// 1. ConfigManager Initialization and Configuration Loading
|
||||
if (!this._configManager) {
|
||||
this._configManager = new CodeIndexConfigManager(contextProxy)
|
||||
this._configManager = new CodeIndexConfigManager(contextProxy, this.workspacePath)
|
||||
}
|
||||
// Load configuration once to get current state and restart requirements
|
||||
const { requiresRestart } = await this._configManager.loadConfiguration()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue