Skip to content

Commit 587e0ea

Browse files
authored
feat(vscode): move format functionality to lsp (#4681)
1 parent 321b118 commit 587e0ea

File tree

7 files changed

+140
-7
lines changed

7 files changed

+140
-7
lines changed

sqlmesh/lsp/custom.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,22 @@ class SupportedMethodsResponse(PydanticModel):
9999
"""
100100

101101
methods: t.List[CustomMethod]
102+
103+
104+
FORMAT_PROJECT_FEATURE = "sqlmesh/format_project"
105+
106+
107+
class FormatProjectRequest(PydanticModel):
108+
"""
109+
Request to format all models in the current project.
110+
"""
111+
112+
pass
113+
114+
115+
class FormatProjectResponse(PydanticModel):
116+
"""
117+
Response to format project request.
118+
"""
119+
120+
pass

sqlmesh/lsp/main.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
ALL_MODELS_FOR_RENDER_FEATURE,
3131
RENDER_MODEL_FEATURE,
3232
SUPPORTED_METHODS_FEATURE,
33+
FORMAT_PROJECT_FEATURE,
3334
AllModelsRequest,
3435
AllModelsResponse,
3536
AllModelsForRenderRequest,
@@ -38,6 +39,8 @@
3839
RenderModelResponse,
3940
SupportedMethodsRequest,
4041
SupportedMethodsResponse,
42+
FormatProjectRequest,
43+
FormatProjectResponse,
4144
CustomMethod,
4245
)
4346
from sqlmesh.lsp.hints import get_hints
@@ -52,6 +55,7 @@
5255
ALL_MODELS_FOR_RENDER_FEATURE,
5356
API_FEATURE,
5457
SUPPORTED_METHODS_FEATURE,
58+
FORMAT_PROJECT_FEATURE,
5559
]
5660

5761

@@ -175,6 +179,25 @@ def supported_methods(
175179
]
176180
)
177181

182+
@self.server.feature(FORMAT_PROJECT_FEATURE)
183+
def format_project(
184+
ls: LanguageServer, params: FormatProjectRequest
185+
) -> FormatProjectResponse:
186+
"""Format all models in the current project."""
187+
try:
188+
if self.lsp_context is None:
189+
current_path = Path.cwd()
190+
self._ensure_context_in_folder(current_path)
191+
if self.lsp_context is None:
192+
raise RuntimeError("No context found")
193+
194+
# Call the format method on the context
195+
self.lsp_context.context.format()
196+
return FormatProjectResponse()
197+
except Exception as e:
198+
ls.log_trace(f"Error formatting project: {e}")
199+
return FormatProjectResponse()
200+
178201
@self.server.feature(API_FEATURE)
179202
def api(ls: LanguageServer, request: ApiRequest) -> t.Dict[str, t.Any]:
180203
ls.log_trace(f"API request: {request}")

vscode/extension/src/commands/format.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,45 @@ import * as vscode from 'vscode'
55
import { ErrorType, handleError } from '../utilities/errors'
66
import { AuthenticationProviderTobikoCloud } from '../auth/auth'
77
import { execAsync } from '../utilities/exec'
8+
import { LSPClient } from '../lsp/lsp'
89

910
export const format =
10-
(authProvider: AuthenticationProviderTobikoCloud) =>
11+
(
12+
authProvider: AuthenticationProviderTobikoCloud,
13+
lsp: LSPClient | undefined,
14+
) =>
1115
async (): Promise<void> => {
1216
traceLog('Calling format')
13-
const out = await internalFormat()
17+
const out = await internalFormat(lsp)
1418
if (isErr(out)) {
1519
return handleError(authProvider, out.error, 'Project format failed')
1620
}
1721
vscode.window.showInformationMessage('Project formatted successfully')
1822
}
1923

20-
const internalFormat = async (): Promise<Result<undefined, ErrorType>> => {
24+
const internalFormat = async (
25+
lsp: LSPClient | undefined,
26+
): Promise<Result<undefined, ErrorType>> => {
27+
try {
28+
// Try LSP method first
29+
if (lsp) {
30+
const response = await lsp.call_custom_method(
31+
'sqlmesh/format_project',
32+
{},
33+
)
34+
if (isErr(response)) {
35+
return response
36+
}
37+
return ok(undefined)
38+
}
39+
} catch (error) {
40+
traceLog(`LSP format failed, falling back to CLI: ${JSON.stringify(error)}`)
41+
}
42+
43+
// Fallback to CLI method if LSP is not available
44+
// TODO This is a solution in order to be backwards compatible in the cases
45+
// where the LSP method is not implemented yet. This should be removed at
46+
// some point in the future.
2147
const exec = await sqlmeshExec()
2248
if (isErr(exec)) {
2349
return exec

vscode/extension/src/extension.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,6 @@ export async function activate(context: vscode.ExtensionContext) {
5656
context.subscriptions.push(
5757
vscode.commands.registerCommand('sqlmesh.signout', signOut(authProvider)),
5858
)
59-
context.subscriptions.push(
60-
vscode.commands.registerCommand('sqlmesh.format', format(authProvider)),
61-
)
6259

6360
lspClient = new LSPClient()
6461

@@ -79,6 +76,13 @@ export async function activate(context: vscode.ExtensionContext) {
7976
),
8077
)
8178

79+
context.subscriptions.push(
80+
vscode.commands.registerCommand(
81+
'sqlmesh.format',
82+
format(authProvider, lspClient),
83+
),
84+
)
85+
8286
// Register the webview
8387
const lineagePanel = new LineagePanel(context.extensionUri, lspClient)
8488
context.subscriptions.push(

vscode/extension/src/lsp/custom.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type CustomLSPMethods =
3232
| RenderModelMethod
3333
| AllModelsForRenderMethod
3434
| SupportedMethodsMethod
35+
| FormatProjectMethod
3536

3637
interface AllModelsRequest {
3738
textDocument: {
@@ -93,3 +94,15 @@ interface SupportedMethodsResponse {
9394
interface CustomMethod {
9495
name: string
9596
}
97+
98+
export interface FormatProjectMethod {
99+
method: 'sqlmesh/format_project'
100+
request: FormatProjectRequest
101+
response: FormatProjectResponse
102+
}
103+
104+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
105+
interface FormatProjectRequest {}
106+
107+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
108+
interface FormatProjectResponse {}

vscode/extension/src/utilities/sqlmesh/sqlmesh.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export const ensureSqlmeshEnterpriseInstalled = async (): Promise<
197197
/**
198198
* Get the sqlmesh executable for the current workspace.
199199
*
200-
* @returns The sqlmesh executable for the current workspace.
200+
* @deprecated Use LSP instead of direct sqlmesh execution for any new functionality.
201201
*/
202202
export const sqlmeshExec = async (): Promise<
203203
Result<SqlmeshExecInfo, ErrorType>

vscode/extension/tests/format.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { test, expect } from '@playwright/test'
2+
import path from 'path'
3+
import fs from 'fs-extra'
4+
import os from 'os'
5+
import { startVSCode, SUSHI_SOURCE_PATH } from './utils'
6+
7+
test('Format project works correctly', async () => {
8+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-'))
9+
await fs.copy(SUSHI_SOURCE_PATH, tempDir)
10+
11+
try {
12+
const { window, close } = await startVSCode(tempDir)
13+
14+
// Wait for the models folder to be visible
15+
await window.waitForSelector('text=models')
16+
17+
// Click on the models folder, excluding external_models
18+
await window
19+
.getByRole('treeitem', { name: 'models', exact: true })
20+
.locator('a')
21+
.click()
22+
23+
// Open the customer_revenue_lifetime model
24+
await window
25+
.getByRole('treeitem', { name: 'customers.sql', exact: true })
26+
.locator('a')
27+
.click()
28+
29+
await window.waitForSelector('text=grain')
30+
await window.waitForSelector('text=Loaded SQLMesh Context')
31+
32+
// Render the model
33+
await window.keyboard.press(
34+
process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P',
35+
)
36+
await window.keyboard.type('Format SQLMesh Project')
37+
await window.keyboard.press('Enter')
38+
39+
// Check that the notification appears saying 'Project formatted successfully'
40+
await expect(
41+
window.getByText('Project formatted successfully', { exact: true }),
42+
).toBeVisible()
43+
44+
await close()
45+
} finally {
46+
await fs.remove(tempDir)
47+
}
48+
})

0 commit comments

Comments
 (0)