Skip to content

Commit a2dcc18

Browse files
authored
Improve performance of CustomModesManager (#3123)
1 parent 19ef52b commit a2dcc18

File tree

3 files changed

+318
-29
lines changed

3 files changed

+318
-29
lines changed

src/core/config/CustomModesManager.ts

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ import { GlobalFileNames } from "../../shared/globalFileNames"
1111
const ROOMODES_FILENAME = ".roomodes"
1212

1313
export class CustomModesManager {
14+
private static readonly cacheTTL = 10_000
15+
1416
private disposables: vscode.Disposable[] = []
1517
private isWriting = false
1618
private writeQueue: Array<() => Promise<void>> = []
19+
private cachedModes: ModeConfig[] | null = null
20+
private cachedAt: number = 0
1721

1822
constructor(
1923
private readonly context: vscode.ExtensionContext,
@@ -25,6 +29,7 @@ export class CustomModesManager {
2529

2630
private async queueWrite(operation: () => Promise<void>): Promise<void> {
2731
this.writeQueue.push(operation)
32+
2833
if (!this.isWriting) {
2934
await this.processWriteQueue()
3035
}
@@ -36,9 +41,11 @@ export class CustomModesManager {
3641
}
3742

3843
this.isWriting = true
44+
3945
try {
4046
while (this.writeQueue.length > 0) {
4147
const operation = this.writeQueue.shift()
48+
4249
if (operation) {
4350
await operation()
4451
}
@@ -50,9 +57,11 @@ export class CustomModesManager {
5057

5158
private async getWorkspaceRoomodes(): Promise<string | undefined> {
5259
const workspaceFolders = vscode.workspace.workspaceFolders
60+
5361
if (!workspaceFolders || workspaceFolders.length === 0) {
5462
return undefined
5563
}
64+
5665
const workspaceRoot = getWorkspacePath()
5766
const roomodesPath = path.join(workspaceRoot, ROOMODES_FILENAME)
5867
const exists = await fileExistsAtPath(roomodesPath)
@@ -73,10 +82,7 @@ export class CustomModesManager {
7382
const source = isRoomodes ? ("project" as const) : ("global" as const)
7483

7584
// Add source to each mode
76-
return result.data.customModes.map((mode) => ({
77-
...mode,
78-
source,
79-
}))
85+
return result.data.customModes.map((mode) => ({ ...mode, source }))
8086
} catch (error) {
8187
const errorMsg = `Failed to load modes from ${filePath}: ${error instanceof Error ? error.message : String(error)}`
8288
console.error(`[CustomModesManager] ${errorMsg}`)
@@ -92,36 +98,30 @@ export class CustomModesManager {
9298
for (const mode of projectModes) {
9399
if (!slugs.has(mode.slug)) {
94100
slugs.add(mode.slug)
95-
merged.push({
96-
...mode,
97-
source: "project",
98-
})
101+
merged.push({ ...mode, source: "project" })
99102
}
100103
}
101104

102105
// Add non-duplicate global modes
103106
for (const mode of globalModes) {
104107
if (!slugs.has(mode.slug)) {
105108
slugs.add(mode.slug)
106-
merged.push({
107-
...mode,
108-
source: "global",
109-
})
109+
merged.push({ ...mode, source: "global" })
110110
}
111111
}
112112

113113
return merged
114114
}
115115

116-
async getCustomModesFilePath(): Promise<string> {
116+
public async getCustomModesFilePath(): Promise<string> {
117117
const settingsDir = await this.ensureSettingsDirectoryExists()
118118
const filePath = path.join(settingsDir, GlobalFileNames.customModes)
119119
const fileExists = await fileExistsAtPath(filePath)
120+
120121
if (!fileExists) {
121-
await this.queueWrite(async () => {
122-
await fs.writeFile(filePath, JSON.stringify({ customModes: [] }, null, 2))
123-
})
122+
await this.queueWrite(() => fs.writeFile(filePath, JSON.stringify({ customModes: [] }, null, 2)))
124123
}
124+
125125
return filePath
126126
}
127127

@@ -133,10 +133,12 @@ export class CustomModesManager {
133133
vscode.workspace.onDidSaveTextDocument(async (document) => {
134134
if (arePathsEqual(document.uri.fsPath, settingsPath)) {
135135
const content = await fs.readFile(settingsPath, "utf-8")
136+
136137
const errorMessage =
137138
"Invalid custom modes format. Please ensure your settings follow the correct JSON format."
138139

139140
let config: any
141+
140142
try {
141143
config = JSON.parse(content)
142144
} catch (error) {
@@ -159,13 +161,15 @@ export class CustomModesManager {
159161
// Merge modes from both sources (.roomodes takes precedence)
160162
const mergedModes = await this.mergeCustomModes(roomodesModes, result.data.customModes)
161163
await this.context.globalState.update("customModes", mergedModes)
164+
this.clearCache()
162165
await this.onUpdate()
163166
}
164167
}),
165168
)
166169

167170
// Watch .roomodes file if it exists
168171
const roomodesPath = await this.getWorkspaceRoomodes()
172+
169173
if (roomodesPath) {
170174
this.disposables.push(
171175
vscode.workspace.onDidSaveTextDocument(async (document) => {
@@ -175,39 +179,47 @@ export class CustomModesManager {
175179
// .roomodes takes precedence
176180
const mergedModes = await this.mergeCustomModes(roomodesModes, settingsModes)
177181
await this.context.globalState.update("customModes", mergedModes)
182+
this.clearCache()
178183
await this.onUpdate()
179184
}
180185
}),
181186
)
182187
}
183188
}
184189

185-
async getCustomModes(): Promise<ModeConfig[]> {
186-
// Get modes from settings file
190+
public async getCustomModes(): Promise<ModeConfig[]> {
191+
// Check if we have a valid cached result.
192+
const now = Date.now()
193+
194+
if (this.cachedModes && now - this.cachedAt < CustomModesManager.cacheTTL) {
195+
return this.cachedModes
196+
}
197+
198+
// Get modes from settings file.
187199
const settingsPath = await this.getCustomModesFilePath()
188200
const settingsModes = await this.loadModesFromFile(settingsPath)
189201

190-
// Get modes from .roomodes if it exists
202+
// Get modes from .roomodes if it exists.
191203
const roomodesPath = await this.getWorkspaceRoomodes()
192204
const roomodesModes = roomodesPath ? await this.loadModesFromFile(roomodesPath) : []
193205

194-
// Create maps to store modes by source
206+
// Create maps to store modes by source.
195207
const projectModes = new Map<string, ModeConfig>()
196208
const globalModes = new Map<string, ModeConfig>()
197209

198-
// Add project modes (they take precedence)
210+
// Add project modes (they take precedence).
199211
for (const mode of roomodesModes) {
200212
projectModes.set(mode.slug, { ...mode, source: "project" as const })
201213
}
202214

203-
// Add global modes
215+
// Add global modes.
204216
for (const mode of settingsModes) {
205217
if (!projectModes.has(mode.slug)) {
206218
globalModes.set(mode.slug, { ...mode, source: "global" as const })
207219
}
208220
}
209221

210-
// Combine modes in the correct order: project modes first, then global modes
222+
// Combine modes in the correct order: project modes first, then global modes.
211223
const mergedModes = [
212224
...roomodesModes.map((mode) => ({ ...mode, source: "project" as const })),
213225
...settingsModes
@@ -216,22 +228,30 @@ export class CustomModesManager {
216228
]
217229

218230
await this.context.globalState.update("customModes", mergedModes)
231+
232+
this.cachedModes = mergedModes
233+
this.cachedAt = now
234+
219235
return mergedModes
220236
}
221-
async updateCustomMode(slug: string, config: ModeConfig): Promise<void> {
237+
238+
public async updateCustomMode(slug: string, config: ModeConfig): Promise<void> {
222239
try {
223240
const isProjectMode = config.source === "project"
224241
let targetPath: string
225242

226243
if (isProjectMode) {
227244
const workspaceFolders = vscode.workspace.workspaceFolders
245+
228246
if (!workspaceFolders || workspaceFolders.length === 0) {
229247
logger.error("Failed to update project mode: No workspace folder found", { slug })
230248
throw new Error("No workspace folder found for project-specific mode")
231249
}
250+
232251
const workspaceRoot = getWorkspacePath()
233252
targetPath = path.join(workspaceRoot, ROOMODES_FILENAME)
234253
const exists = await fileExistsAtPath(targetPath)
254+
235255
logger.info(`${exists ? "Updating" : "Creating"} project mode in ${ROOMODES_FILENAME}`, {
236256
slug,
237257
workspace: workspaceRoot,
@@ -241,7 +261,7 @@ export class CustomModesManager {
241261
}
242262

243263
await this.queueWrite(async () => {
244-
// Ensure source is set correctly based on target file
264+
// Ensure source is set correctly based on target file.
245265
const modeWithSource = {
246266
...config,
247267
source: isProjectMode ? ("project" as const) : ("global" as const),
@@ -253,6 +273,7 @@ export class CustomModesManager {
253273
return updatedModes
254274
})
255275

276+
this.clearCache()
256277
await this.refreshMergedState()
257278
})
258279
} catch (error) {
@@ -261,22 +282,26 @@ export class CustomModesManager {
261282
vscode.window.showErrorMessage(`Failed to update custom mode: ${errorMessage}`)
262283
}
263284
}
285+
264286
private async updateModesInFile(filePath: string, operation: (modes: ModeConfig[]) => ModeConfig[]): Promise<void> {
265287
let content = "{}"
288+
266289
try {
267290
content = await fs.readFile(filePath, "utf-8")
268291
} catch (error) {
269-
// File might not exist yet
292+
// File might not exist yet.
270293
content = JSON.stringify({ customModes: [] })
271294
}
272295

273296
let settings
297+
274298
try {
275299
settings = JSON.parse(content)
276300
} catch (error) {
277301
console.error(`[CustomModesManager] Failed to parse JSON from ${filePath}:`, error)
278302
settings = { customModes: [] }
279303
}
304+
280305
settings.customModes = operation(settings.customModes || [])
281306
await fs.writeFile(filePath, JSON.stringify(settings, null, 2), "utf-8")
282307
}
@@ -290,10 +315,13 @@ export class CustomModesManager {
290315
const mergedModes = await this.mergeCustomModes(roomodesModes, settingsModes)
291316

292317
await this.context.globalState.update("customModes", mergedModes)
318+
319+
this.clearCache()
320+
293321
await this.onUpdate()
294322
}
295323

296-
async deleteCustomMode(slug: string): Promise<void> {
324+
public async deleteCustomMode(slug: string): Promise<void> {
297325
try {
298326
const settingsPath = await this.getCustomModesFilePath()
299327
const roomodesPath = await this.getWorkspaceRoomodes()
@@ -320,6 +348,8 @@ export class CustomModesManager {
320348
await this.updateModesInFile(settingsPath, (modes) => modes.filter((m) => m.slug !== slug))
321349
}
322350

351+
// Clear cache when modes are deleted
352+
this.clearCache()
323353
await this.refreshMergedState()
324354
})
325355
} catch (error) {
@@ -335,11 +365,12 @@ export class CustomModesManager {
335365
return settingsDir
336366
}
337367

338-
async resetCustomModes(): Promise<void> {
368+
public async resetCustomModes(): Promise<void> {
339369
try {
340370
const filePath = await this.getCustomModesFilePath()
341371
await fs.writeFile(filePath, JSON.stringify({ customModes: [] }, null, 2))
342372
await this.context.globalState.update("customModes", [])
373+
this.clearCache()
343374
await this.onUpdate()
344375
} catch (error) {
345376
vscode.window.showErrorMessage(
@@ -348,10 +379,16 @@ export class CustomModesManager {
348379
}
349380
}
350381

382+
private clearCache(): void {
383+
this.cachedModes = null
384+
this.cachedAt = 0
385+
}
386+
351387
dispose(): void {
352388
for (const disposable of this.disposables) {
353389
disposable.dispose()
354390
}
391+
355392
this.disposables = []
356393
}
357394
}

0 commit comments

Comments
 (0)