@@ -4,6 +4,15 @@ import { isErr } from '@bus/result'
44import { RenderModelEntry } from '../lsp/custom'
55import { RenderedModelProvider } from '../providers/renderedModelProvider'
66
7+ const DEBOUNCE_DELAY = 500 // milliseconds - balanced for responsiveness without overwhelming the server
8+
9+ // Track associations between source documents and their rendered views
10+ const sourceToRenderedMap = new Map < string , { uri : vscode . Uri ; modelName : string ; documentUri : string } > ( )
11+ // Track file watchers
12+ const fileWatchers = new Map < string , vscode . Disposable > ( )
13+ // Track active render operations to prevent overlapping requests
14+ const activeRenders = new Map < string , { inProgress : boolean ; lastRequestTime : number } > ( )
15+
716export function renderModel (
817 lspClient ?: LSPClient ,
918 renderedModelProvider ?: RenderedModelProvider ,
@@ -104,5 +113,109 @@ export function renderModel(
104113
105114 // Explicitly set the language mode to SQL for syntax highlighting
106115 await vscode . languages . setTextDocumentLanguage ( document , 'sql' )
116+
117+ // Store the association between source and rendered view
118+ sourceToRenderedMap . set ( documentUri , {
119+ uri : uri ,
120+ modelName : selectedModel . name ,
121+ documentUri : documentUri
122+ } )
123+
124+ // Set up a file watcher if not already watching this file
125+ if ( ! fileWatchers . has ( documentUri ) ) {
126+ // Create a debounced update function to avoid too many rapid updates
127+ let updateTimeout : NodeJS . Timeout | undefined
128+ let consecutiveErrors = 0
129+ const debouncedUpdate = ( ) => {
130+ if ( updateTimeout ) {
131+ clearTimeout ( updateTimeout )
132+ }
133+
134+ updateTimeout = setTimeout ( ( ) => {
135+ // Get the stored association
136+ const association = sourceToRenderedMap . get ( documentUri )
137+ if ( ! association ) return
138+
139+ // Check if there's already a render in progress
140+ const renderState = activeRenders . get ( documentUri )
141+ if ( renderState ?. inProgress ) {
142+ // Skip this update if one is already in progress
143+ return
144+ }
145+
146+ // Mark render as in progress
147+ activeRenders . set ( documentUri , { inProgress : true , lastRequestTime : Date . now ( ) } )
148+
149+ // Re-render the model
150+ void ( async ( ) => {
151+ try {
152+ const result = await lspClient . call_custom_method ( 'sqlmesh/render_model' , {
153+ textDocumentUri : documentUri ,
154+ } )
155+
156+ if ( isErr ( result ) ) {
157+ consecutiveErrors ++
158+ console . error ( `Failed to re-render model: ${ result . error } ` )
159+
160+ // Show error message after 3 consecutive failures
161+ if ( consecutiveErrors >= 3 ) {
162+ vscode . window . showWarningMessage (
163+ `Model rendering is experiencing issues. Check the SQLMesh output for details.`
164+ )
165+ consecutiveErrors = 0 // Reset counter after showing message
166+ }
167+ return
168+ }
169+
170+ // Reset error counter on success
171+ consecutiveErrors = 0
172+
173+ // Find the model with the same name
174+ const model = result . value . models ?. find ( m => m . name === association . modelName )
175+ if ( model ) {
176+ // Update the content in the provider
177+ renderedModelProvider . updateRenderedModel ( association . uri , model . rendered_query )
178+ }
179+ } finally {
180+ // Mark render as complete
181+ activeRenders . set ( documentUri , { inProgress : false , lastRequestTime : Date . now ( ) } )
182+ }
183+ } ) ( )
184+ } , DEBOUNCE_DELAY )
185+ }
186+
187+ // Watch for changes to the source document
188+ const watcher = vscode . workspace . onDidChangeTextDocument ( event => {
189+ if ( event . document . uri . toString ( true ) === documentUri ) {
190+ debouncedUpdate ( )
191+ }
192+ } )
193+
194+ fileWatchers . set ( documentUri , watcher )
195+ }
196+
197+ // Clean up watcher when the rendered document is closed
198+ const closeListener : vscode . Disposable = vscode . workspace . onDidCloseTextDocument ( closedDoc => {
199+ if ( closedDoc . uri . toString ( ) === uri . toString ( ) ) {
200+ const watcher = fileWatchers . get ( documentUri )
201+ if ( watcher ) {
202+ watcher . dispose ( )
203+ fileWatchers . delete ( documentUri )
204+ }
205+ sourceToRenderedMap . delete ( documentUri )
206+ activeRenders . delete ( documentUri )
207+ closeListener . dispose ( )
208+ }
209+ } )
107210 }
108211}
212+
213+ // Export a function to clean up watchers when extension is deactivated
214+ export function disposeRenderModelWatchers ( ) : void {
215+ fileWatchers . forEach ( ( watcher ) => {
216+ watcher . dispose ( )
217+ } )
218+ fileWatchers . clear ( )
219+ sourceToRenderedMap . clear ( )
220+ activeRenders . clear ( )
221+ }
0 commit comments