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
810import * as vscode from "vscode"
@@ -12,42 +14,158 @@ import { CONFIGURATION } from "../../../configuration.js"
1214import { handleError } from "../../utils.js"
1315import * as crypto from "crypto"
1416import { 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
1721const fileHashes = new Map < string , string > ( )
22+ // Store message keys by file path to track deleted keys
23+ const fileMessageKeys = new Map < string , Set < string > > ( )
1824const 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
2026const processingFiles = new Set < string > ( ) // Prevent concurrent processing of the same file
2127const 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
2329let 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
2535async 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
35102function 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