Skip to content

Commit ab76dc3

Browse files
committed
feat: render model without active editor
1 parent 28567cd commit ab76dc3

6 files changed

Lines changed: 177 additions & 10 deletions

File tree

sqlmesh/lsp/context.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from sqlmesh.core.model.definition import SqlModel
77
from sqlmesh.core.linter.definition import AnnotatedRuleViolation
8-
from sqlmesh.lsp.custom import RenderModelEntry
8+
from sqlmesh.lsp.custom import RenderModelEntry, ModelForRendering
99
from sqlmesh.lsp.uri import URI
1010

1111

@@ -148,3 +148,29 @@ def lint_model(self, uri: URI) -> t.List[AnnotatedRuleViolation]:
148148
# Store in cache
149149
self._lint_cache[path] = diagnostics
150150
return diagnostics
151+
152+
def list_of_models_for_rendering(self) -> t.List[ModelForRendering]:
153+
"""Get a list of models for rendering.
154+
155+
Returns:
156+
List of ModelForRendering objects.
157+
"""
158+
return [
159+
ModelForRendering(
160+
name=model.name,
161+
fqn=model.fqn,
162+
description=model.description,
163+
uri=URI.from_path(model._path).value,
164+
)
165+
for model in self.context.models.values()
166+
if isinstance(model, SqlModel) and model._path is not None
167+
] + [
168+
ModelForRendering(
169+
name=audit.name,
170+
fqn=audit.fqn,
171+
description=audit.description,
172+
uri=URI.from_path(audit._path).value,
173+
)
174+
for audit in self.context.standalone_audits.values()
175+
if audit._path is not None
176+
]

sqlmesh/lsp/custom.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,29 @@ class RenderModelResponse(PydanticModel):
4646
"""
4747

4848
models: t.List[RenderModelEntry]
49+
50+
51+
ALL_MODELS_FOR_RENDER_FEATURE = "sqlmesh/all_models_for_render"
52+
53+
54+
class ModelForRendering(PydanticModel):
55+
"""
56+
A model that is available for rendering.
57+
"""
58+
59+
name: str
60+
fqn: str
61+
description: t.Optional[str] = None
62+
uri: str
63+
64+
65+
class AllModelsForRenderRequest(PydanticModel):
66+
pass
67+
68+
69+
class AllModelsForRenderResponse(PydanticModel):
70+
"""
71+
Response to get all the models that are in the current project for rendering purposes.
72+
"""
73+
74+
models: t.List[ModelForRendering]

sqlmesh/lsp/main.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@
2727
)
2828
from sqlmesh.lsp.custom import (
2929
ALL_MODELS_FEATURE,
30+
ALL_MODELS_FOR_RENDER_FEATURE,
3031
RENDER_MODEL_FEATURE,
3132
AllModelsRequest,
3233
AllModelsResponse,
34+
AllModelsForRenderRequest,
35+
AllModelsForRenderResponse,
3336
RenderModelRequest,
3437
RenderModelResponse,
3538
)
@@ -117,6 +120,17 @@ def render_model(ls: LanguageServer, params: RenderModelRequest) -> RenderModelR
117120
context = self._context_get_or_load(uri)
118121
return RenderModelResponse(models=context.render_model(uri))
119122

123+
@self.server.feature(ALL_MODELS_FOR_RENDER_FEATURE)
124+
def all_models_for_render(
125+
ls: LanguageServer, params: AllModelsForRenderRequest
126+
) -> AllModelsForRenderResponse:
127+
if self.lsp_context is None:
128+
current_path = Path.cwd()
129+
self._ensure_context_in_folder(current_path)
130+
if self.lsp_context is None:
131+
raise RuntimeError("No context found")
132+
return AllModelsForRenderResponse(models=self.lsp_context.list_of_models_for_rendering())
133+
120134
@self.server.feature(API_FEATURE)
121135
def api(ls: LanguageServer, request: ApiRequest) -> t.Dict[str, t.Any]:
122136
ls.log_trace(f"API request: {request}")

vscode/extension/src/commands/renderModel.ts

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,60 @@ export function renderModel(
99
renderedModelProvider?: RenderedModelProvider,
1010
) {
1111
return async () => {
12+
if (!lspClient) {
13+
vscode.window.showErrorMessage('LSP client not available')
14+
return
15+
}
16+
1217
// Get the current active editor
1318
const activeEditor = vscode.window.activeTextEditor
1419

20+
let documentUri: string
21+
1522
if (!activeEditor) {
16-
vscode.window.showErrorMessage('No active editor found')
17-
return
18-
}
23+
// No active editor, show a list of all models
24+
const allModelsResult = await lspClient.call_custom_method(
25+
'sqlmesh/all_models_for_render',
26+
{},
27+
)
1928

20-
if (!lspClient) {
21-
vscode.window.showErrorMessage('LSP client not available')
22-
return
23-
}
29+
if (isErr(allModelsResult)) {
30+
vscode.window.showErrorMessage(
31+
`Failed to get models: ${allModelsResult.error}`,
32+
)
33+
return
34+
}
35+
36+
if (
37+
!allModelsResult.value.models ||
38+
allModelsResult.value.models.length === 0
39+
) {
40+
vscode.window.showInformationMessage('No models found in the project')
41+
return
42+
}
2443

25-
// Get the current document URI
26-
const documentUri = activeEditor.document.uri.toString(true)
44+
// Let user choose from all models
45+
const items = allModelsResult.value.models.map(model => ({
46+
label: model.name,
47+
description: model.fqn,
48+
detail: model.description ? model.description : undefined,
49+
model: model,
50+
}))
51+
52+
const selected = await vscode.window.showQuickPick(items, {
53+
placeHolder: 'Select a model to render',
54+
})
55+
56+
if (!selected) {
57+
return
58+
}
59+
60+
// Use the selected model's URI
61+
documentUri = selected.model.uri
62+
} else {
63+
// Get the current document URI
64+
documentUri = activeEditor.document.uri.toString(true)
65+
}
2766

2867
// Call the render model API
2968
const result = await lspClient.call_custom_method('sqlmesh/render_model', {

vscode/extension/src/lsp/custom.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export type CustomLSPMethods =
3030
| AllModelsMethod
3131
| AbstractAPICall
3232
| RenderModelMethod
33+
| AllModelsForRenderMethod
3334

3435
interface AllModelsRequest {
3536
textDocument: {
@@ -54,3 +55,23 @@ export interface AbstractAPICall {
5455
request: AbstractAPICallRequest
5556
response: object
5657
}
58+
59+
export interface AllModelsForRenderMethod {
60+
method: 'sqlmesh/all_models_for_render'
61+
request: AllModelsForRenderRequest
62+
response: AllModelsForRenderResponse
63+
}
64+
65+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
66+
interface AllModelsForRenderRequest {}
67+
68+
interface AllModelsForRenderResponse {
69+
models: ModelForRendering[]
70+
}
71+
72+
export interface ModelForRendering {
73+
name: string
74+
fqn: string
75+
description: string | null | undefined
76+
uri: string
77+
}

vscode/extension/tests/render.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,44 @@ test('Render works correctly with every rendered model opening a new tab', async
153153
await fs.remove(tempDir)
154154
}
155155
})
156+
157+
test('Render shows model picker when no active editor is open', async () => {
158+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-'))
159+
await fs.copy(SUSHI_SOURCE_PATH, tempDir)
160+
161+
try {
162+
const { window, close } = await startVSCode(tempDir)
163+
164+
// Load the lineage view to initialize SQLMesh context (like lineage.spec.ts does)
165+
await window.keyboard.press(
166+
process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P',
167+
)
168+
await window.keyboard.type('Lineage: Focus On View')
169+
await window.keyboard.press('Enter')
170+
171+
// Wait for "Loaded SQLmesh Context" text to appear
172+
const loadedContextText = window.locator('text=Loaded SQLMesh Context')
173+
await expect(loadedContextText.first()).toBeVisible({ timeout: 10_000 })
174+
175+
// Run the render command without any active editor
176+
await window.keyboard.press(
177+
process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P',
178+
)
179+
await window.keyboard.type('Render Model')
180+
await window.keyboard.press('Enter')
181+
182+
// Type to filter for customers model and select it
183+
await window.keyboard.type('customers')
184+
await window.waitForSelector('text=sushi.customers', { timeout: 5000 })
185+
await window.locator('text=sushi.customers').click()
186+
187+
// Verify the rendered model is shown
188+
await expect(window.locator('text=sushi.customers (rendered)')).toBeVisible(
189+
{ timeout: 15000 },
190+
)
191+
192+
await close()
193+
} finally {
194+
await fs.remove(tempDir)
195+
}
196+
})

0 commit comments

Comments
 (0)