Skip to content

Commit 0aa6c51

Browse files
Merge pull request #3562 from opral/fix-sherlock-updating
fix Sherlock updating
2 parents 1e68426 + 4a89f56 commit 0aa6c51

File tree

2 files changed

+229
-35
lines changed

2 files changed

+229
-35
lines changed

.changeset/curvy-mayflies-add.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"vs-code-extension": patch
3+
---
4+
5+
fix sherlock updating

inlang/packages/sherlock/src/utilities/fs/experimental/directMessageHandler.ts

Lines changed: 224 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
*
44
* This is a simpler approach to handling translation files that bypasses
55
* the complex pattern matching logic in setupFileSystemWatcher.ts
6+
*
7+
* Performance optimized version to fix slow editor updates and sync issues.
68
*/
79

810
import * as vscode from "vscode"
@@ -12,42 +14,158 @@ import { CONFIGURATION } from "../../../configuration.js"
1214
import { handleError } from "../../utils.js"
1315
import * as crypto from "crypto"
1416
import { saveProject } from "../../../main.js"
17+
import type { InlangDatabaseSchema } from "@inlang/sdk"
18+
import type { Kysely } from "kysely"
1519

1620
// Store file hashes to detect real changes
1721
const fileHashes = new Map<string, string>()
22+
// Store message keys by file path to track deleted keys
23+
const fileMessageKeys = new Map<string, Set<string>>()
1824
const selfModifiedFiles = new Set<string>() // Tracks files modified by the extension
19-
const DEBOUNCE_MS = 300
25+
const DEBOUNCE_MS = 150 // Reduced debounce time for faster updates
2026
const processingFiles = new Set<string>() // Prevent concurrent processing of the same file
2127
const lastFileUpdateTime = new Map<string, number>() // Track when files were last processed
22-
const FILE_UPDATE_COOLDOWN_MS = 3000 // Minimum time between processing the same file
28+
const FILE_UPDATE_COOLDOWN_MS = 1000 // Reduced cooldown to make the editor more responsive
2329
let isInEventLoop = false // Global flag to detect event loops
30+
let lastUIUpdateTime = 0 // Track when we last fired UI update events
31+
const MIN_UI_UPDATE_INTERVAL_MS = 300 // Minimum time between UI updates to prevent UI lag
32+
let lastSaveTime = 0 // Track when we last saved the project
2433

34+
// Optimized file hash function that avoids full content hashing for large files
2535
async function getFileHash(uri: vscode.Uri): Promise<string> {
2636
try {
2737
const content = await vscode.workspace.fs.readFile(uri)
28-
return crypto.createHash("sha256").update(content).digest("hex")
38+
// For large files, only hash the first 10KB which is enough for JSON files
39+
// This significantly improves performance
40+
const contentToHash = content.length > 10240 ? content.slice(0, 10240) : content
41+
return crypto.createHash("sha256").update(contentToHash).digest("hex")
2942
} catch {
3043
return ""
3144
}
3245
}
3346

34-
// Debounce function to prevent rapid-fire events
47+
// Extract message keys from a JSON file
48+
async function extractMessageKeys(uri: vscode.Uri): Promise<Set<string>> {
49+
try {
50+
const content = await vscode.workspace.fs.readFile(uri)
51+
const json = JSON.parse(new TextDecoder().decode(content))
52+
return new Set(Object.keys(json).filter((key) => key !== "$schema"))
53+
} catch {
54+
return new Set()
55+
}
56+
}
57+
58+
// Throttled UI update function to prevent UI lag from too many updates
59+
function throttledUIUpdate() {
60+
const now = Date.now()
61+
if (now - lastUIUpdateTime < MIN_UI_UPDATE_INTERVAL_MS) {
62+
// Don't update if it's too soon after the last update
63+
return false
64+
}
65+
66+
// Update the last update time
67+
lastUIUpdateTime = now
68+
69+
// Set the event loop flag
70+
isInEventLoop = true
71+
72+
// Fire the event
73+
CONFIGURATION.EVENTS.ON_DID_EDIT_MESSAGE.fire()
74+
75+
// Reset the flag after a short delay
76+
setTimeout(() => {
77+
isInEventLoop = false
78+
}, 500) // Shorter timeout for better responsiveness
79+
80+
return true
81+
}
82+
83+
// Throttled save function to prevent excessive saves
84+
async function throttledSaveProject() {
85+
const now = Date.now()
86+
if (now - lastSaveTime < 1000) {
87+
// Limit to once per second
88+
return false
89+
}
90+
91+
lastSaveTime = now
92+
try {
93+
await saveProject()
94+
return true
95+
} catch (error) {
96+
console.error("Error saving project:", error)
97+
return false
98+
}
99+
}
100+
101+
// Optimized debounce function to prevent rapid-fire events
35102
function debounce<T extends (...args: any[]) => Promise<void>>(
36103
func: T,
37104
wait: number
38105
): (...args: Parameters<T>) => void {
39106
let timeout: NodeJS.Timeout | null = null
107+
40108
return function (...args: Parameters<T>) {
41109
if (timeout) {
42110
clearTimeout(timeout)
43111
}
44112
timeout = setTimeout(() => {
45-
func(...args)
113+
func(...args).catch((error) => {
114+
console.error("Error in debounced function:", error)
115+
})
46116
timeout = null
47117
}, wait)
48118
}
49119
}
50120

121+
// Helper function to delete message variants by locale
122+
async function deleteMessageVariantsByLocale(
123+
db: Kysely<InlangDatabaseSchema>,
124+
bundleId: string,
125+
locale: string
126+
): Promise<boolean> {
127+
try {
128+
// First, get the message id for this bundle and locale
129+
const messageIds = await db
130+
.selectFrom("message")
131+
.select("id")
132+
.where("bundleId", "=", bundleId)
133+
.where("locale", "=", locale)
134+
.execute()
135+
136+
if (messageIds.length === 0) return false
137+
138+
// Delete the variants for these messages
139+
for (const { id: messageId } of messageIds) {
140+
await db.deleteFrom("variant").where("messageId", "=", messageId).execute()
141+
}
142+
143+
// Delete the messages themselves
144+
await db
145+
.deleteFrom("message")
146+
.where("bundleId", "=", bundleId)
147+
.where("locale", "=", locale)
148+
.execute()
149+
150+
// Check if there are any messages left for this bundle
151+
const remainingMessages = await db
152+
.selectFrom("message")
153+
.select("id")
154+
.where("bundleId", "=", bundleId)
155+
.execute()
156+
157+
// If no messages are left, delete the bundle
158+
if (remainingMessages.length === 0) {
159+
await db.deleteFrom("bundle").where("id", "=", bundleId).execute()
160+
}
161+
162+
return true
163+
} catch (error) {
164+
console.error(`Error deleting message variant:`, error)
165+
return false
166+
}
167+
}
168+
51169
/**
52170
* Set up a watcher for message JSON files specifically
53171
*/
@@ -73,15 +191,17 @@ export async function setupDirectMessageWatcher(args: {
73191
console.log(`Already processing ${filePath}, skipping`)
74192
return
75193
}
76-
194+
77195
// Check cooldown period to prevent rapid-fire updates to the same file
78196
const now = Date.now()
79197
const lastUpdate = lastFileUpdateTime.get(filePath) || 0
80198
if (now - lastUpdate < FILE_UPDATE_COOLDOWN_MS) {
81-
console.log(`File ${filePath} was updated too recently, skipping (cooldown: ${FILE_UPDATE_COOLDOWN_MS}ms)`)
199+
console.log(
200+
`File ${filePath} was updated too recently, skipping (cooldown: ${FILE_UPDATE_COOLDOWN_MS}ms)`
201+
)
82202
return
83203
}
84-
204+
85205
// Check if we're in an event loop
86206
if (isInEventLoop) {
87207
console.log("Detected potential event loop, breaking the cycle")
@@ -118,6 +238,8 @@ export async function setupDirectMessageWatcher(args: {
118238
fileHashes.set(filePath, newHash)
119239
} else {
120240
fileHashes.delete(filePath)
241+
// Clear all message keys for deleted files
242+
fileMessageKeys.delete(filePath)
121243
}
122244

123245
console.log(`Processing message file ${eventType} event: ${filePath}`)
@@ -130,6 +252,14 @@ export async function setupDirectMessageWatcher(args: {
130252
return
131253
}
132254

255+
// Get the database from the project
256+
const db = currentProject.db as Kysely<InlangDatabaseSchema>
257+
if (!db) {
258+
console.log("Project database not available")
259+
processingFiles.delete(filePath)
260+
return
261+
}
262+
133263
// Get current plugins and find message format plugin
134264
const currentPlugins = await currentProject.plugins.get()
135265
const messageFormatPlugin = currentPlugins.find((p) =>
@@ -150,11 +280,23 @@ export async function setupDirectMessageWatcher(args: {
150280
console.log(`Extracted locale from filename: ${locale}`)
151281

152282
if (eventType !== "Deleted") {
153-
// Read file content
283+
// Extract current message keys from file
284+
const currentKeys = await extractMessageKeys(uri)
285+
// Get previously known keys (if any)
286+
const previousKeys = fileMessageKeys.get(filePath) || new Set()
287+
288+
// Find keys that have been deleted (present in previous but not in current)
289+
const deletedKeys = new Set([...previousKeys].filter((key) => !currentKeys.has(key)))
290+
console.log(`Detected ${deletedKeys.size} deleted keys in ${filePath}`)
291+
292+
// Update stored keys for this file
293+
fileMessageKeys.set(filePath, currentKeys)
294+
295+
// Read file content for import
154296
const content = await vscode.workspace.fs.readFile(uri)
155297

156298
try {
157-
// Import the file
299+
// Import the file to update existing messages
158300
await currentProject.importFiles({
159301
pluginKey,
160302
files: [
@@ -167,49 +309,95 @@ export async function setupDirectMessageWatcher(args: {
167309

168310
console.log(`Imported messages for locale: ${locale}`)
169311

170-
// Mark as self-modified to prevent loops
312+
// Handle deleted keys if any were detected
313+
if (deletedKeys.size > 0) {
314+
console.log(`Processing ${deletedKeys.size} deleted keys for ${locale}`)
315+
316+
// Track if we've made changes to update the UI
317+
let madeChanges = false
318+
319+
// Process each deleted key
320+
for (const key of deletedKeys) {
321+
console.log(`Removing deleted key "${key}" for locale ${locale}`)
322+
const result = await deleteMessageVariantsByLocale(db, key, locale)
323+
if (result) {
324+
madeChanges = true
325+
}
326+
}
327+
328+
// If we made changes, make sure to update UI and save
329+
if (madeChanges) {
330+
console.log(`Made changes due to deleted keys, refreshing UI...`)
331+
332+
// Save the project after removing variants
333+
await throttledSaveProject()
334+
335+
// Update UI with throttling for better performance
336+
throttledUIUpdate()
337+
}
338+
}
339+
340+
// Mark as self-modified to prevent loops with automatic cleanup
171341
selfModifiedFiles.add(filePath)
342+
setTimeout(() => {
343+
selfModifiedFiles.delete(filePath)
344+
}, 3000) // 3 second auto-cleanup
172345

173346
// Only update UI if this wasn't a self-triggered change or if the file has changed
174347
if (!wasModifiedBySelf) {
175348
console.log(`External change detected, updating UI for locale: ${locale}`)
176-
177-
// Set the event loop flag to prevent cyclic updates
178-
isInEventLoop = true
179-
CONFIGURATION.EVENTS.ON_DID_EDIT_MESSAGE.fire()
180-
181-
// Reset loop flag after a delay
182-
setTimeout(() => {
183-
isInEventLoop = false
184-
}, 1000);
349+
throttledUIUpdate()
185350
} else {
186351
console.log(`Self-triggered change, not updating UI for locale: ${locale}`)
187352
}
188-
353+
189354
// Record this update time
190355
lastFileUpdateTime.set(filePath, Date.now())
191356

192-
// Save project
193-
await saveProject()
357+
// Save project with throttling for better performance
358+
await throttledSaveProject()
194359
} catch (error) {
195360
console.error(`Error importing message file:`, error)
196361
handleError(error)
197362
}
198363
} else {
199-
console.log(`File deleted: ${filePath} - skipping import but updating UI`)
200-
201-
// Only update UI for genuine deletions
364+
console.log(`File deleted: ${filePath} - handling deletion for locale ${locale}`)
365+
366+
// When a file is deleted, all its messages should be removed for that locale
367+
const previousKeys = fileMessageKeys.get(filePath) || new Set()
368+
369+
if (previousKeys.size > 0) {
370+
// Track if we've made changes to update the UI
371+
let madeChanges = false
372+
373+
// Process each bundle that had keys in this file
374+
for (const key of previousKeys) {
375+
console.log(`Removing message "${key}" for deleted locale ${locale}`)
376+
377+
const result = await deleteMessageVariantsByLocale(db, key, locale)
378+
if (result) {
379+
madeChanges = true
380+
}
381+
}
382+
383+
// Clear all message keys for deleted files
384+
fileMessageKeys.delete(filePath)
385+
386+
// If we made changes, make sure to update UI and save
387+
if (madeChanges) {
388+
// Save project
389+
await throttledSaveProject()
390+
391+
// Force update UI with throttling
392+
throttledUIUpdate()
393+
}
394+
}
395+
396+
// Only update UI for genuine deletions with throttling
202397
if (!wasModifiedBySelf) {
203-
// Set the event loop flag to prevent cyclic updates
204-
isInEventLoop = true
205-
CONFIGURATION.EVENTS.ON_DID_EDIT_MESSAGE.fire()
206-
207-
// Reset loop flag after a delay
208-
setTimeout(() => {
209-
isInEventLoop = false
210-
}, 1000);
398+
throttledUIUpdate()
211399
}
212-
400+
213401
// Record this update
214402
lastFileUpdateTime.set(filePath, Date.now())
215403
}
@@ -232,4 +420,5 @@ export async function setupDirectMessageWatcher(args: {
232420
console.error("Error setting up direct message watcher:", error)
233421
handleError(error)
234422
}
423+
235424
}

0 commit comments

Comments
 (0)