Skip to content

Commit a52053f

Browse files
authored
feat(vscode): better errors for missing sqlmesh_lsp (#4288)
1 parent 1109d97 commit a52053f

5 files changed

Lines changed: 160 additions & 14 deletions

File tree

vscode/extension/eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default tseslint.config(
1616
},
1717
{
1818
rules: {
19+
'no-fallthrough': 'error',
1920
'@typescript-eslint/switch-exhaustiveness-check': 'error',
2021
'@typescript-eslint/no-unsafe-assignment': 'off',
2122
'@typescript-eslint/no-explicit-any': 'off',

vscode/extension/src/commands/format.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,35 @@ import { execSync } from 'child_process'
33
import { sqlmesh_exec } from '../utilities/sqlmesh/sqlmesh'
44
import { err, isErr, ok, Result } from '../utilities/functional/result'
55
import * as vscode from 'vscode'
6-
import { ErrorType, handleNotSginedInError } from '../utilities/errors'
6+
import {
7+
ErrorType,
8+
handleNotSginedInError,
9+
handleSqlmeshLspNotFoundError,
10+
handleSqlmeshLspDependenciesMissingError,
11+
} from '../utilities/errors'
712
import { AuthenticationProviderTobikoCloud } from '../auth/auth'
813

914
export const format =
10-
(authProvider: AuthenticationProviderTobikoCloud) => async () => {
15+
(authProvider: AuthenticationProviderTobikoCloud) =>
16+
async (): Promise<void> => {
1117
traceLog('Calling format')
1218
const out = await internalFormat()
1319
if (isErr(out)) {
14-
if (out.error.type === 'not_signed_in') {
15-
await handleNotSginedInError(authProvider)
16-
return
17-
} else {
18-
vscode.window.showErrorMessage(
19-
`Project format failed: ${out.error.message}`,
20-
)
21-
return
20+
switch (out.error.type) {
21+
case 'not_signed_in':
22+
await handleNotSginedInError(authProvider)
23+
return
24+
case 'sqlmesh_lsp_not_found':
25+
await handleSqlmeshLspNotFoundError()
26+
return
27+
case 'sqlmesh_lsp_dependencies_missing':
28+
await handleSqlmeshLspDependenciesMissingError(out.error)
29+
return
30+
case 'generic':
31+
await vscode.window.showErrorMessage(
32+
`Project format failed: ${out.error.message}`,
33+
)
34+
return
2235
}
2336
}
2437
vscode.window.showInformationMessage('Project formatted successfully')

vscode/extension/src/extension.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import { signOut } from './commands/signout'
1313
import { signIn } from './commands/signin'
1414
import { signInSpecifyFlow } from './commands/signinSpecifyFlow'
1515
import { isErr } from './utilities/functional/result'
16-
import { handleNotSginedInError } from './utilities/errors'
16+
import {
17+
handleNotSginedInError,
18+
handleSqlmeshLspNotFoundError,
19+
handleSqlmeshLspDependenciesMissingError,
20+
} from './utilities/errors'
1721

1822
let lspClient: LSPClient | undefined
1923

@@ -58,17 +62,49 @@ export async function activate(context: vscode.ExtensionContext) {
5862
lspClient = new LSPClient()
5963
const result = await lspClient.start()
6064
if (isErr(result)) {
61-
await handleNotSginedInError(authProvider)
65+
switch (result.error.type) {
66+
case 'not_signed_in':
67+
await handleNotSginedInError(authProvider)
68+
break
69+
case 'sqlmesh_lsp_not_found':
70+
await handleSqlmeshLspNotFoundError()
71+
break
72+
case 'sqlmesh_lsp_dependencies_missing':
73+
await handleSqlmeshLspDependenciesMissingError(result.error)
74+
break
75+
case 'generic':
76+
await vscode.window.showErrorMessage(
77+
`Failed to start LSP: ${result.error.message}`,
78+
)
79+
break
80+
}
81+
} else {
82+
context.subscriptions.push(lspClient)
6283
}
63-
context.subscriptions.push(lspClient)
6484

6585
const restart = async () => {
6686
if (lspClient) {
6787
traceVerbose('Restarting LSP client')
6888
const restartResult = await lspClient.restart()
6989
if (isErr(restartResult)) {
70-
await handleNotSginedInError(authProvider)
90+
switch (restartResult.error.type) {
91+
case 'not_signed_in':
92+
await handleNotSginedInError(authProvider)
93+
return
94+
case 'sqlmesh_lsp_not_found':
95+
await handleSqlmeshLspNotFoundError()
96+
return
97+
case 'sqlmesh_lsp_dependencies_missing':
98+
await handleSqlmeshLspDependenciesMissingError(restartResult.error)
99+
return
100+
case 'generic':
101+
await vscode.window.showErrorMessage(
102+
`Failed to restart LSP: ${restartResult.error.message}`,
103+
)
104+
return
105+
}
71106
}
107+
context.subscriptions.push(lspClient)
72108
}
73109
}
74110

vscode/extension/src/utilities/errors.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ import { traceInfo } from './common/log'
99
export type ErrorType =
1010
| { type: 'generic'; message: string }
1111
| { type: 'not_signed_in' }
12+
| { type: 'sqlmesh_lsp_not_found' }
13+
// sqlmesh_lsp_dependencies_missing is used when the sqlmesh_lsp is found but the lsp extras are missing.
14+
| SqlmeshLspDependenciesMissingError
15+
16+
interface SqlmeshLspDependenciesMissingError {
17+
type: 'sqlmesh_lsp_dependencies_missing'
18+
is_missing_pygls: boolean
19+
is_missing_lsprotocol: boolean
20+
is_tobiko_cloud: boolean
21+
}
1222

1323
/**
1424
* Handles the case where the user is not signed in to Tobiko Cloud.
@@ -26,3 +36,40 @@ export const handleNotSginedInError = async (
2636
await signIn(authProvider)()
2737
}
2838
}
39+
40+
/**
41+
* Handles the case where the sqlmesh_lsp is not found.
42+
*/
43+
export const handleSqlmeshLspNotFoundError = async (): Promise<void> => {
44+
traceInfo('handleSqlmeshLspNotFoundError')
45+
await window.showErrorMessage(
46+
'SQLMesh LSP not found, please check installation',
47+
)
48+
}
49+
50+
/**
51+
* Handles the case where the sqlmesh_lsp is found but the lsp extras are missing.
52+
*/
53+
export const handleSqlmeshLspDependenciesMissingError = async (
54+
error: SqlmeshLspDependenciesMissingError,
55+
): Promise<void> => {
56+
traceInfo('handleSqlmeshLspDependenciesMissingError')
57+
if (error.is_tobiko_cloud) {
58+
await window.showErrorMessage(
59+
'LSP dependencies missing, make sure to include `lsp` in the `extras` section of your `tcloud.yaml` file.',
60+
)
61+
} else {
62+
const install = await window.showErrorMessage(
63+
'LSP dependencies missing, make sure to install `sqlmesh[lsp]`.',
64+
'Install',
65+
)
66+
if (install === 'Install') {
67+
const terminal = window.createTerminal({
68+
name: 'SQLMesh LSP Install',
69+
hideFromUser: false,
70+
})
71+
terminal.show()
72+
terminal.sendText("pip install 'sqlmesh[lsp]'", false)
73+
}
74+
}
75+
}

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,46 @@ export const sqlmesh_exec = async (): Promise<
231231
}
232232
}
233233

234+
/**
235+
* Ensure that the sqlmesh_lsp dependencies are installed.
236+
*
237+
* @returns A Result indicating whether the sqlmesh_lsp dependencies were installed.
238+
*/
239+
export const ensureSqlmeshLspDependenciesInstalled = async (): Promise<
240+
Result<undefined, ErrorType>
241+
> => {
242+
const isPyglsInstalled = await isPythonModuleInstalled('pygls')
243+
if (isErr(isPyglsInstalled)) {
244+
return err({
245+
type: 'generic',
246+
message: isPyglsInstalled.error,
247+
})
248+
}
249+
const isLsprotocolInstalled = await isPythonModuleInstalled('lsprotocol')
250+
if (isErr(isLsprotocolInstalled)) {
251+
return err({
252+
type: 'generic',
253+
message: isLsprotocolInstalled.error,
254+
})
255+
}
256+
const isTobikoCloudInstalled = await isTcloudProject()
257+
if (isErr(isTobikoCloudInstalled)) {
258+
return err({
259+
type: 'generic',
260+
message: isTobikoCloudInstalled.error,
261+
})
262+
}
263+
if (!isPyglsInstalled.value || !isLsprotocolInstalled.value) {
264+
return err({
265+
type: 'sqlmesh_lsp_dependencies_missing',
266+
is_missing_pygls: !isPyglsInstalled.value,
267+
is_missing_lsprotocol: !isLsprotocolInstalled.value,
268+
is_tobiko_cloud: isTobikoCloudInstalled.value,
269+
})
270+
}
271+
return ok(undefined)
272+
}
273+
234274
/**
235275
* Get the sqlmesh_lsp executable for the current workspace.
236276
*
@@ -282,8 +322,17 @@ export const sqlmesh_lsp_exec = async (): Promise<
282322
})
283323
}
284324
}
325+
const ensuredDependencies = await ensureSqlmeshLspDependenciesInstalled()
326+
if (isErr(ensuredDependencies)) {
327+
return ensuredDependencies
328+
}
285329
const binPath = path.join(interpreterDetails.binPath!, 'sqlmesh_lsp')
286330
traceLog(`Bin path: ${binPath}`)
331+
if (!fs.existsSync(binPath)) {
332+
return err({
333+
type: 'sqlmesh_lsp_not_found',
334+
})
335+
}
287336
return ok({
288337
bin: binPath,
289338
workspacePath,

0 commit comments

Comments
 (0)