Skip to content

Commit b66c8d7

Browse files
committed
feat(vscode): work on rerender on save
[ci skip]
1 parent c49bed2 commit b66c8d7

4 files changed

Lines changed: 127 additions & 2 deletions

File tree

examples/sushi/models/customer_revenue_by_day.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ SELECT
3030
o.customer_id::INT AS customer_id, /* Customer id */
3131
SUM(ot.total)::DOUBLE AS revenue, /* Revenue from orders made by this customer */
3232
MAX(0) AS "country code", /* Customer country code, used for testing spaces */
33-
o.event_date::DATE AS event_date /* Date */
33+
o.event_date::DATE AS event_date /* Date */,
34+
o.event_date::DATE AS event_d /* Date */
3435
FROM sushi.orders AS o
3536
LEFT JOIN order_total AS ot
3637
ON o.id = ot.order_id AND o.event_date = ot.event_date

vscode/extension/src/commands/renderModel.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ import { isErr } from '@bus/result'
44
import { RenderModelEntry } from '../lsp/custom'
55
import { 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+
716
export 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+
}

vscode/extension/src/extension.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { AuthenticationProviderTobikoCloud } from './auth/auth'
1212
import { signOut } from './commands/signout'
1313
import { signIn } from './commands/signin'
1414
import { signInSpecifyFlow } from './commands/signinSpecifyFlow'
15-
import { renderModel } from './commands/renderModel'
15+
import { renderModel, disposeRenderModelWatchers } from './commands/renderModel'
1616
import { isErr } from '@bus/result'
1717
import {
1818
handleNotSginedInError,
@@ -170,6 +170,9 @@ export async function activate(context: vscode.ExtensionContext) {
170170

171171
// This method is called when your extension is deactivated
172172
export async function deactivate() {
173+
// Clean up render model watchers
174+
disposeRenderModelWatchers()
175+
173176
if (lspClient) {
174177
await lspClient.dispose()
175178
}

vscode/extension/src/providers/renderedModelProvider.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ export class RenderedModelProvider
4040
return uri
4141
}
4242

43+
/**
44+
* Update existing rendered model content
45+
*/
46+
updateRenderedModel(uri: vscode.Uri, content: string): void {
47+
this.renderedModels.set(uri.toString(), content)
48+
this._onDidChange.fire(uri)
49+
}
50+
4351
/**
4452
* Get the URI scheme for rendered models
4553
*/

0 commit comments

Comments
 (0)