Skip to content

Commit fceda92

Browse files
authored
feat(vscode): rerender on save model (#4637)
1 parent f1da1d8 commit fceda92

3 files changed

Lines changed: 156 additions & 5 deletions

File tree

vscode/extension/src/commands/renderModel.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,51 @@ import { isErr } from '@bus/result'
44
import { RenderModelEntry } from '../lsp/custom'
55
import { RenderedModelProvider } from '../providers/renderedModelProvider'
66

7+
export async function reRenderModelForSourceFile(
8+
sourceUri: string,
9+
lspClient: LSPClient,
10+
renderedModelProvider: RenderedModelProvider,
11+
): Promise<void> {
12+
const renderedUri = renderedModelProvider.getRenderedUriForSource(sourceUri)
13+
if (!renderedUri) {
14+
return // No rendered model exists for this source file
15+
}
16+
17+
// Call the render model API
18+
const result = await lspClient.call_custom_method('sqlmesh/render_model', {
19+
textDocumentUri: sourceUri,
20+
})
21+
22+
if (isErr(result)) {
23+
// Silently fail on auto-rerender errors to avoid spamming user
24+
return
25+
}
26+
27+
// Check if we got any models
28+
if (!result.value.models || result.value.models.length === 0) {
29+
return
30+
}
31+
32+
// Get the originally rendered model information
33+
const originalModelInfo =
34+
renderedModelProvider.getModelInfoForRendered(renderedUri)
35+
36+
// Find the specific model that was originally rendered, or fall back to the first model
37+
const selectedModel = originalModelInfo
38+
? result.value.models.find(
39+
model =>
40+
model.name === originalModelInfo.name &&
41+
model.fqn === originalModelInfo.fqn,
42+
) || result.value.models[0]
43+
: result.value.models[0]
44+
45+
// Update the existing rendered model content
46+
renderedModelProvider.updateRenderedModel(
47+
renderedUri,
48+
selectedModel.rendered_query,
49+
)
50+
}
51+
752
export function renderModel(
853
lspClient?: LSPClient,
954
renderedModelProvider?: RenderedModelProvider,
@@ -114,6 +159,8 @@ export function renderModel(
114159
const uri = renderedModelProvider.storeRenderedModel(
115160
selectedModel.name,
116161
selectedModel.rendered_query,
162+
documentUri,
163+
selectedModel,
117164
)
118165

119166
// Open the virtual document

vscode/extension/src/extension.ts

Lines changed: 21 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, reRenderModelForSourceFile } from './commands/renderModel'
1616
import { isErr } from '@bus/result'
1717
import {
1818
handleNotSginedInError,
@@ -23,6 +23,7 @@ import {
2323
import { selector, completionProvider } from './completion/completion'
2424
import { LineagePanel } from './webviews/lineagePanel'
2525
import { RenderedModelProvider } from './providers/renderedModelProvider'
26+
import { sleep } from './utilities/sleep'
2627

2728
let lspClient: LSPClient | undefined
2829

@@ -99,6 +100,25 @@ export async function activate(context: vscode.ExtensionContext) {
99100
),
100101
)
101102

103+
// Add file save listener for auto-rerendering models
104+
context.subscriptions.push(
105+
vscode.workspace.onDidSaveTextDocument(async document => {
106+
if (
107+
lspClient &&
108+
renderedModelProvider.hasRenderedModelForSource(
109+
document.uri.toString(true),
110+
)
111+
) {
112+
await sleep(100)
113+
await reRenderModelForSourceFile(
114+
document.uri.toString(true),
115+
lspClient,
116+
renderedModelProvider,
117+
)
118+
}
119+
}),
120+
)
121+
102122
const restart = async () => {
103123
if (lspClient) {
104124
traceVerbose('Restarting LSP client')

vscode/extension/src/providers/renderedModelProvider.ts

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import * as vscode from 'vscode'
2+
import { RenderModelEntry } from '../lsp/custom'
3+
4+
interface RenderedModelInfo {
5+
content: string
6+
sourceUri?: string
7+
modelInfo?: RenderModelEntry
8+
}
29

310
/**
411
* Content provider for read-only rendered SQL models
@@ -8,7 +15,10 @@ export class RenderedModelProvider
815
{
916
private static readonly scheme = 'sqlmesh-rendered'
1017

11-
private renderedModels = new Map<string, string>()
18+
// Single map containing all rendered model information
19+
private renderedModels = new Map<string, RenderedModelInfo>()
20+
// Track which source file URIs are associated with rendered models
21+
private sourceToRenderedUri = new Map<string, vscode.Uri>()
1222

1323
// Event emitter for content changes
1424
private _onDidChange = new vscode.EventEmitter<vscode.Uri>()
@@ -19,13 +29,19 @@ export class RenderedModelProvider
1929
*/
2030
provideTextDocumentContent(uri: vscode.Uri): string {
2131
const key = uri.toString()
22-
return this.renderedModels.get(key) || ''
32+
const modelInfo = this.renderedModels.get(key)
33+
return modelInfo?.content || ''
2334
}
2435

2536
/**
2637
* Store rendered model content and create a URI for it
2738
*/
28-
storeRenderedModel(modelName: string, content: string): vscode.Uri {
39+
storeRenderedModel(
40+
modelName: string,
41+
content: string,
42+
sourceUri?: string,
43+
modelInfo?: RenderModelEntry,
44+
): vscode.Uri {
2945
const fileName = `${modelName} (rendered)`
3046
// Add a timestamp to make the URI unique for each render
3147
const timestamp = Date.now()
@@ -35,11 +51,78 @@ export class RenderedModelProvider
3551
path: fileName,
3652
fragment: timestamp.toString(),
3753
})
38-
this.renderedModels.set(uri.toString(), content)
54+
55+
const uriString = uri.toString()
56+
57+
// Store all information in single map
58+
this.renderedModels.set(uriString, {
59+
content,
60+
sourceUri,
61+
modelInfo,
62+
})
63+
64+
// Track the association between a source file and the rendered model
65+
if (sourceUri) {
66+
// Remove any existing mapping for this source file
67+
const existingRenderedUri = this.sourceToRenderedUri.get(sourceUri)
68+
if (existingRenderedUri) {
69+
this.renderedModels.delete(existingRenderedUri.toString())
70+
}
71+
72+
this.sourceToRenderedUri.set(sourceUri, uri)
73+
}
74+
3975
this._onDidChange.fire(uri)
4076
return uri
4177
}
4278

79+
/**
80+
* Update an existing rendered model with new content
81+
*/
82+
updateRenderedModel(uri: vscode.Uri, content: string): void {
83+
const uriString = uri.toString()
84+
const existingInfo = this.renderedModels.get(uriString)
85+
if (existingInfo) {
86+
this.renderedModels.set(uriString, {
87+
...existingInfo,
88+
content,
89+
})
90+
}
91+
this._onDidChange.fire(uri)
92+
}
93+
94+
/**
95+
* Get the rendered URI for a given source file URI
96+
*/
97+
getRenderedUriForSource(sourceUri: string): vscode.Uri | undefined {
98+
return this.sourceToRenderedUri.get(sourceUri)
99+
}
100+
101+
/**
102+
* Get the source URI for a given rendered model URI
103+
*/
104+
getSourceUriForRendered(renderedUri: string): string | undefined {
105+
const modelInfo = this.renderedModels.get(renderedUri)
106+
return modelInfo?.sourceUri
107+
}
108+
109+
/**
110+
* Get the model information for a given rendered model URI
111+
*/
112+
getModelInfoForRendered(
113+
renderedUri: vscode.Uri,
114+
): RenderModelEntry | undefined {
115+
const modelInfo = this.renderedModels.get(renderedUri.toString())
116+
return modelInfo?.modelInfo
117+
}
118+
119+
/**
120+
* Check if a source file has an associated rendered model
121+
*/
122+
hasRenderedModelForSource(sourceUri: string): boolean {
123+
return this.sourceToRenderedUri.has(sourceUri)
124+
}
125+
43126
/**
44127
* Get the URI scheme for rendered models
45128
*/
@@ -52,6 +135,7 @@ export class RenderedModelProvider
52135
*/
53136
dispose() {
54137
this.renderedModels.clear()
138+
this.sourceToRenderedUri.clear()
55139
this._onDidChange.dispose()
56140
}
57141
}

0 commit comments

Comments
 (0)