Skip to content

Commit ead391f

Browse files
committed
feat: add workspace-level toggle for codebase indexing
- Add workspace-specific indexing settings that override global settings - Implement UI toggle in CodeIndexPopover for workspace-level control - Store workspace settings using VSCode workspace state API - Support multi-root workspaces with per-folder settings - Add comprehensive unit tests for new functionality - Update translations for new UI elements Fixes #7926
1 parent 08d7f80 commit ead391f

File tree

7 files changed

+337
-10
lines changed

7 files changed

+337
-10
lines changed

src/core/webview/webviewMessageHandler.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2392,6 +2392,19 @@ export const webviewMessageHandler = async (
23922392
codebaseIndexSearchMinScore: settings.codebaseIndexSearchMinScore,
23932393
}
23942394

2395+
// Handle workspace-specific indexing setting
2396+
if (settings.workspaceIndexEnabled !== undefined) {
2397+
const currentCodeIndexManager = provider.getCurrentWorkspaceCodeIndexManager()
2398+
if (currentCodeIndexManager && provider.cwd) {
2399+
await currentCodeIndexManager.configManager?.setWorkspaceIndexEnabled(
2400+
provider.cwd,
2401+
settings.workspaceIndexEnabled,
2402+
)
2403+
// Also store in global config for UI state
2404+
globalStateConfig.workspaceIndexEnabled = settings.workspaceIndexEnabled
2405+
}
2406+
}
2407+
23952408
// Save global state first
23962409
await updateGlobalState("codebaseIndexConfig", globalStateConfig)
23972410

@@ -2528,7 +2541,7 @@ export const webviewMessageHandler = async (
25282541
processedItems: 0,
25292542
totalItems: 0,
25302543
currentItemUnit: "items",
2531-
workerspacePath: undefined,
2544+
workspacePath: undefined,
25322545
},
25332546
})
25342547
return
@@ -2545,6 +2558,14 @@ export const webviewMessageHandler = async (
25452558
workspacePath: undefined,
25462559
}
25472560

2561+
// Add workspace-specific indexing enabled state
2562+
if (manager && provider.cwd) {
2563+
const workspaceEnabled = manager.configManager?.getWorkspaceIndexEnabled(provider.cwd)
2564+
if (workspaceEnabled !== undefined) {
2565+
status.workspaceIndexEnabled = workspaceEnabled
2566+
}
2567+
}
2568+
25482569
provider.postMessageToWebview({
25492570
type: "indexingStatusUpdate",
25502571
values: status,

src/services/code-index/__tests__/manager.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,11 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
9797

9898
mockContext = {
9999
subscriptions: [],
100-
workspaceState: {} as any,
100+
workspaceState: {
101+
get: vi.fn().mockReturnValue(undefined),
102+
update: vi.fn().mockResolvedValue(undefined),
103+
keys: vi.fn().mockReturnValue([]),
104+
} as any,
101105
globalState: {} as any,
102106
extensionUri: {} as any,
103107
extensionPath: testExtensionPath,
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import * as vscode from "vscode"
2+
import { describe, it, expect, beforeEach, vi } from "vitest"
3+
import { CodeIndexConfigManager } from "../config-manager"
4+
import { ContextProxy } from "../../../core/config/ContextProxy"
5+
6+
describe("Workspace-level Indexing Toggle", () => {
7+
let configManager: CodeIndexConfigManager
8+
let mockContextProxy: ContextProxy
9+
let mockContext: vscode.ExtensionContext
10+
const testWorkspacePath = "/test/workspace"
11+
12+
beforeEach(() => {
13+
// Mock ContextProxy
14+
mockContextProxy = {
15+
getValue: vi.fn(),
16+
setValue: vi.fn(),
17+
getGlobalState: vi.fn(),
18+
updateGlobalState: vi.fn(),
19+
getSecret: vi.fn(),
20+
storeSecret: vi.fn(),
21+
} as any
22+
23+
// Mock VSCode Extension Context
24+
mockContext = {
25+
workspaceState: {
26+
get: vi.fn(),
27+
update: vi.fn(),
28+
},
29+
globalState: {
30+
get: vi.fn(),
31+
update: vi.fn(),
32+
},
33+
secrets: {
34+
get: vi.fn(),
35+
store: vi.fn(),
36+
},
37+
} as any
38+
39+
// Initialize config manager with mocks
40+
configManager = new CodeIndexConfigManager(mockContextProxy, testWorkspacePath, mockContext)
41+
})
42+
43+
describe("Workspace-specific settings", () => {
44+
it("should inherit global setting when workspace setting is not set", () => {
45+
// Mock global setting enabled
46+
vi.spyOn(mockContextProxy, "getGlobalState").mockReturnValue({
47+
codebaseIndexEnabled: true,
48+
})
49+
50+
// Mock no workspace-specific setting
51+
vi.spyOn(mockContext.workspaceState, "get").mockReturnValue(undefined)
52+
53+
// Should inherit global setting (true)
54+
expect(configManager.isFeatureEnabled).toBe(true)
55+
})
56+
57+
it("should use workspace setting when explicitly set to false", () => {
58+
// Mock global setting enabled
59+
vi.spyOn(mockContextProxy, "getGlobalState").mockReturnValue({
60+
codebaseIndexEnabled: true,
61+
})
62+
63+
// Mock workspace-specific setting disabled
64+
const workspaceKey = `codebaseIndexEnabled_${Buffer.from(testWorkspacePath).toString("base64")}`
65+
vi.spyOn(mockContext.workspaceState, "get").mockImplementation((key) => {
66+
if (key === workspaceKey) return false
67+
return undefined
68+
})
69+
70+
// Create a new instance to trigger loadWorkspaceSettings
71+
const newConfigManager = new CodeIndexConfigManager(mockContextProxy, testWorkspacePath, mockContext)
72+
73+
// Should use workspace setting (false) instead of global (true)
74+
expect(newConfigManager.getWorkspaceIndexEnabled(testWorkspacePath)).toBe(false)
75+
})
76+
77+
it("should use workspace setting when explicitly set to true", () => {
78+
// Mock global setting disabled
79+
vi.spyOn(mockContextProxy, "getGlobalState").mockReturnValue({
80+
codebaseIndexEnabled: false,
81+
})
82+
83+
// Mock workspace-specific setting enabled
84+
const workspaceKey = `codebaseIndexEnabled_${Buffer.from(testWorkspacePath).toString("base64")}`
85+
vi.spyOn(mockContext.workspaceState, "get").mockImplementation((key) => {
86+
if (key === workspaceKey) return true
87+
return undefined
88+
})
89+
90+
// Create a new instance to trigger loadWorkspaceSettings
91+
const newConfigManager = new CodeIndexConfigManager(mockContextProxy, testWorkspacePath, mockContext)
92+
93+
// Workspace setting should be true
94+
expect(newConfigManager.getWorkspaceIndexEnabled(testWorkspacePath)).toBe(true)
95+
// But overall feature should still be disabled due to global setting
96+
expect(newConfigManager.isFeatureEnabled).toBe(false)
97+
})
98+
99+
it("should persist workspace setting when changed", async () => {
100+
const updateSpy = vi.spyOn(mockContext.workspaceState, "update")
101+
102+
await configManager.setWorkspaceIndexEnabled(testWorkspacePath, false)
103+
104+
const expectedKey = `codebaseIndexEnabled_${Buffer.from(testWorkspacePath).toString("base64")}`
105+
expect(updateSpy).toHaveBeenCalledWith(expectedKey, false)
106+
})
107+
108+
it("should correctly identify when workspace has specific setting", () => {
109+
// No workspace-specific setting
110+
vi.spyOn(mockContext.workspaceState, "get").mockReturnValue(undefined)
111+
expect(configManager.hasWorkspaceSpecificSetting()).toBe(false)
112+
113+
// With workspace-specific setting
114+
const workspaceKey = `codebaseIndexEnabled_${Buffer.from(testWorkspacePath).toString("base64")}`
115+
vi.spyOn(mockContext.workspaceState, "get").mockImplementation((key) => {
116+
if (key === workspaceKey) return true
117+
return undefined
118+
})
119+
120+
const newConfigManager = new CodeIndexConfigManager(mockContextProxy, testWorkspacePath, mockContext)
121+
newConfigManager.loadWorkspaceSettings()
122+
123+
expect(newConfigManager.hasWorkspaceSpecificSetting()).toBe(true)
124+
})
125+
})
126+
127+
describe("Multi-root workspace handling", () => {
128+
it("should handle different settings for different workspace folders", () => {
129+
const workspace1 = "/workspace1"
130+
const workspace2 = "/workspace2"
131+
132+
// Mock different settings for each workspace
133+
vi.spyOn(mockContext.workspaceState, "get").mockImplementation((key) => {
134+
const key1 = `codebaseIndexEnabled_${Buffer.from(workspace1).toString("base64")}`
135+
const key2 = `codebaseIndexEnabled_${Buffer.from(workspace2).toString("base64")}`
136+
137+
if (key === key1) return true
138+
if (key === key2) return false
139+
return undefined
140+
})
141+
142+
// Create managers for each workspace
143+
const manager1 = new CodeIndexConfigManager(mockContextProxy, workspace1, mockContext)
144+
const manager2 = new CodeIndexConfigManager(mockContextProxy, workspace2, mockContext)
145+
146+
manager1.loadWorkspaceSettings()
147+
manager2.loadWorkspaceSettings()
148+
149+
expect(manager1.getWorkspaceIndexEnabled(workspace1)).toBe(true)
150+
expect(manager2.getWorkspaceIndexEnabled(workspace2)).toBe(false)
151+
})
152+
})
153+
154+
describe("Global setting disabled", () => {
155+
it("should always return false when global setting is disabled", () => {
156+
// Mock global setting disabled
157+
vi.spyOn(mockContextProxy, "getGlobalState").mockReturnValue({
158+
codebaseIndexEnabled: false,
159+
})
160+
161+
// Even with workspace setting enabled
162+
const workspaceKey = `codebaseIndexEnabled_${Buffer.from(testWorkspacePath).toString("base64")}`
163+
vi.spyOn(mockContext.workspaceState, "get").mockImplementation((key) => {
164+
if (key === workspaceKey) return true
165+
return undefined
166+
})
167+
168+
const newConfigManager = new CodeIndexConfigManager(mockContextProxy, testWorkspacePath, mockContext)
169+
170+
// Feature should be disabled
171+
expect(newConfigManager.isFeatureEnabled).toBe(false)
172+
})
173+
})
174+
})

src/services/code-index/config-manager.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as vscode from "vscode"
12
import { ApiHandlerOptions } from "../../shared/api"
23
import { ContextProxy } from "../../core/config/ContextProxy"
34
import { EmbedderProvider } from "./interfaces/manager"
@@ -11,6 +12,7 @@ import { getDefaultModelId, getModelDimension, getModelScoreThreshold } from "..
1112
*/
1213
export class CodeIndexConfigManager {
1314
private codebaseIndexEnabled: boolean = true
15+
private workspaceIndexEnabled: Map<string, boolean> = new Map()
1416
private embedderProvider: EmbedderProvider = "openai"
1517
private modelId?: string
1618
private modelDimension?: number
@@ -25,9 +27,15 @@ export class CodeIndexConfigManager {
2527
private searchMinScore?: number
2628
private searchMaxResults?: number
2729

28-
constructor(private readonly contextProxy: ContextProxy) {
30+
constructor(
31+
private readonly contextProxy: ContextProxy,
32+
private readonly workspacePath?: string,
33+
private readonly context?: vscode.ExtensionContext,
34+
) {
2935
// Initialize with current configuration to avoid false restart triggers
3036
this._loadAndSetConfiguration()
37+
// Load workspace-specific settings if available
38+
this.loadWorkspaceSettings()
3139
}
3240

3341
/**
@@ -404,8 +412,21 @@ export class CodeIndexConfigManager {
404412

405413
/**
406414
* Gets whether the code indexing feature is enabled
415+
* Takes into account both global and workspace-level settings
407416
*/
408417
public get isFeatureEnabled(): boolean {
418+
// First check global setting
419+
if (!this.codebaseIndexEnabled) {
420+
return false
421+
}
422+
423+
// Then check workspace-specific setting if workspace path is available
424+
if (this.workspacePath) {
425+
const workspaceEnabled = this.getWorkspaceIndexEnabled(this.workspacePath)
426+
// If workspace setting exists, use it; otherwise inherit global setting
427+
return workspaceEnabled !== undefined ? workspaceEnabled : this.codebaseIndexEnabled
428+
}
429+
409430
return this.codebaseIndexEnabled
410431
}
411432

@@ -480,4 +501,57 @@ export class CodeIndexConfigManager {
480501
public get currentSearchMaxResults(): number {
481502
return this.searchMaxResults ?? DEFAULT_MAX_SEARCH_RESULTS
482503
}
504+
505+
/**
506+
* Gets the workspace-specific indexing enabled state
507+
* @param workspacePath The workspace path to check
508+
* @returns The workspace-specific setting, or undefined if not set
509+
*/
510+
public getWorkspaceIndexEnabled(workspacePath: string): boolean | undefined {
511+
if (!this.context) {
512+
// If no context, check in-memory cache
513+
return this.workspaceIndexEnabled.get(workspacePath)
514+
}
515+
// Use a hash of the workspace path as the key to avoid issues with special characters
516+
const key = `codebaseIndexEnabled_${Buffer.from(workspacePath).toString("base64")}`
517+
const value = this.context.workspaceState.get<boolean>(key)
518+
return value
519+
}
520+
521+
/**
522+
* Sets the workspace-specific indexing enabled state
523+
* @param workspacePath The workspace path to set
524+
* @param enabled Whether indexing should be enabled for this workspace
525+
*/
526+
public async setWorkspaceIndexEnabled(workspacePath: string, enabled: boolean): Promise<void> {
527+
this.workspaceIndexEnabled.set(workspacePath, enabled)
528+
if (this.context) {
529+
// Use a hash of the workspace path as the key to avoid issues with special characters
530+
const key = `codebaseIndexEnabled_${Buffer.from(workspacePath).toString("base64")}`
531+
await this.context.workspaceState.update(key, enabled)
532+
}
533+
}
534+
535+
/**
536+
* Loads workspace-specific settings
537+
*/
538+
public loadWorkspaceSettings(): void {
539+
if (this.workspacePath) {
540+
const workspaceEnabled = this.getWorkspaceIndexEnabled(this.workspacePath)
541+
if (workspaceEnabled !== undefined) {
542+
this.workspaceIndexEnabled.set(this.workspacePath, workspaceEnabled)
543+
}
544+
}
545+
}
546+
547+
/**
548+
* Gets whether the workspace has a specific indexing setting
549+
* (as opposed to inheriting the global setting)
550+
*/
551+
public hasWorkspaceSpecificSetting(): boolean {
552+
if (!this.workspacePath) {
553+
return false
554+
}
555+
return this.getWorkspaceIndexEnabled(this.workspacePath) !== undefined
556+
}
483557
}

src/services/code-index/manager.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ export class CodeIndexManager {
101101
return this._configManager?.isFeatureConfigured ?? false
102102
}
103103

104+
public get configManager(): CodeIndexConfigManager | undefined {
105+
return this._configManager
106+
}
107+
104108
public get isInitialized(): boolean {
105109
try {
106110
this.assertInitialized()
@@ -118,7 +122,7 @@ export class CodeIndexManager {
118122
public async initialize(contextProxy: ContextProxy): Promise<{ requiresRestart: boolean }> {
119123
// 1. ConfigManager Initialization and Configuration Loading
120124
if (!this._configManager) {
121-
this._configManager = new CodeIndexConfigManager(contextProxy)
125+
this._configManager = new CodeIndexConfigManager(contextProxy, this.workspacePath, this.context)
122126
}
123127
// Load configuration once to get current state and restart requirements
124128
const { requiresRestart } = await this._configManager.loadConfiguration()

0 commit comments

Comments
 (0)