Skip to content

Commit 64e9774

Browse files
committed
feat(vscode): allow the lsp loaded to be specified
- allow the extension to specify the lsp script entrypoint
1 parent 3555e73 commit 64e9774

File tree

4 files changed

+267
-10
lines changed

4 files changed

+267
-10
lines changed

vscode/extension/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
"type": "string",
3737
"default": "",
3838
"markdownDescription": "The path to the SQLMesh project. If not set, the extension will try to find the project root automatically. If set, the extension will use the project root as the workspace path, e.g. it will run `sqlmesh` and `sqlmesh_lsp` in the project root. The path can be absolute `/Users/sqlmesh_user/sqlmesh_project/sushi` or relative `./project_folder/sushi` to the workspace root."
39+
},
40+
"sqlmesh.lspEntrypoint": {
41+
"type": "string",
42+
"default": "",
43+
"markdownDescription": "The entry point for the SQLMesh LSP server. If not set the extension looks for the default lsp. If set, the extension will use the entry point as the LSP path, The path can be absolute `/Users/sqlmesh_user/sqlmesh_project/sushi/sqlmesh_lsp` or relative `./project_folder/sushi/sqlmesh_lsp` to the workspace root. It can also have arguments, e.g. `./project_folder/sushi/sqlmesh_lsp --port 5000`."
3944
}
4045
}
4146
},

vscode/extension/src/utilities/config.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,48 @@ import { traceVerbose, traceInfo } from './common/log'
66

77
export interface SqlmeshConfiguration {
88
projectPath: string
9+
lspEntryPoint: string
910
}
1011

1112
/**
1213
* Get the SQLMesh configuration from VS Code settings.
1314
*
1415
* @returns The SQLMesh configuration
1516
*/
16-
export function getSqlmeshConfiguration(): SqlmeshConfiguration {
17+
function getSqlmeshConfiguration(): SqlmeshConfiguration {
1718
const config = workspace.getConfiguration('sqlmesh')
1819
const projectPath = config.get<string>('projectPath', '')
20+
const lspEntryPoint = config.get<string>('lspEntrypoint', '')
1921
return {
2022
projectPath,
23+
lspEntryPoint,
2124
}
2225
}
2326

27+
/**
28+
* Get the SQLMesh LSP entry point from VS Code settings. undefined if not set
29+
* it's expected to be a string in the format "command arg1 arg2 ...".
30+
*/
31+
export function getSqlmeshLspEntryPoint():
32+
| {
33+
entrypoint: string
34+
args: string[]
35+
}
36+
| undefined {
37+
const config = getSqlmeshConfiguration()
38+
if (config.lspEntryPoint === '') {
39+
return undefined
40+
}
41+
// Split the entry point into command and arguments
42+
const parts = config.lspEntryPoint.split(' ')
43+
const entrypoint = parts[0]
44+
const args = parts.slice(1)
45+
if (args.length === 0) {
46+
return { entrypoint, args: [] }
47+
}
48+
return { entrypoint, args }
49+
}
50+
2451
/**
2552
* Validate and resolve the project path from configuration.
2653
* If no project path is configured, use the workspace folder.

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

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { execAsync } from '../exec'
1111
import z from 'zod'
1212
import { ProgressLocation, window } from 'vscode'
1313
import { IS_WINDOWS } from '../isWindows'
14-
import { resolveProjectPath } from '../config'
14+
import { getSqlmeshLspEntryPoint, resolveProjectPath } from '../config'
1515
import { isSemVerGreaterThanOrEqual } from '../semver'
1616

1717
export interface SqlmeshExecInfo {
@@ -413,15 +413,7 @@ export const ensureSqlmeshLspDependenciesInstalled = async (): Promise<
413413
export const sqlmeshLspExec = async (): Promise<
414414
Result<SqlmeshExecInfo, ErrorType>
415415
> => {
416-
const sqlmeshLSP = IS_WINDOWS ? 'sqlmesh_lsp.exe' : 'sqlmesh_lsp'
417416
const projectRoot = await getProjectRoot()
418-
const envVariables = await getPythonEnvVariables()
419-
if (isErr(envVariables)) {
420-
return err({
421-
type: 'generic',
422-
message: envVariables.error,
423-
})
424-
}
425417
const resolvedPath = resolveProjectPath(projectRoot)
426418
if (isErr(resolvedPath)) {
427419
return err({
@@ -430,6 +422,26 @@ export const sqlmeshLspExec = async (): Promise<
430422
})
431423
}
432424
const workspacePath = resolvedPath.value
425+
426+
const configuredLSPExec = getSqlmeshLspEntryPoint()
427+
if (configuredLSPExec) {
428+
traceLog(`Using configured SQLMesh LSP entry point: ${configuredLSPExec.entrypoint} ${configuredLSPExec.args.join(' ')}`)
429+
return ok({
430+
bin: configuredLSPExec.entrypoint,
431+
workspacePath,
432+
env: process.env,
433+
args: configuredLSPExec.args,
434+
})
435+
}
436+
const sqlmeshLSP = IS_WINDOWS ? 'sqlmesh_lsp.exe' : 'sqlmesh_lsp'
437+
const envVariables = await getPythonEnvVariables()
438+
if (isErr(envVariables)) {
439+
return err({
440+
type: 'generic',
441+
message: envVariables.error,
442+
})
443+
}
444+
433445
const interpreterDetails = await getInterpreterDetails()
434446
traceLog(`Interpreter details: ${JSON.stringify(interpreterDetails)}`)
435447
if (interpreterDetails.path) {
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { test, expect } from './fixtures'
2+
import {
3+
createVirtualEnvironment,
4+
openServerPage,
5+
pipInstall,
6+
REPO_ROOT,
7+
SUSHI_SOURCE_PATH,
8+
waitForLoadedSQLMesh,
9+
} from './utils'
10+
import path from 'path'
11+
import fs from 'fs-extra'
12+
13+
async function setupPythonEnvironment(tempDir: string): Promise<void> {
14+
// Create a temporary directory for the virtual environment
15+
const venvDir = path.join(tempDir, '.venv')
16+
fs.mkdirSync(venvDir, { recursive: true })
17+
18+
// Create virtual environment
19+
const pythonDetails = await createVirtualEnvironment(venvDir)
20+
21+
// Install sqlmesh from the local repository with LSP support
22+
const customMaterializations = path.join(
23+
REPO_ROOT,
24+
'examples',
25+
'custom_materializations',
26+
)
27+
const sqlmeshWithExtras = `${REPO_ROOT}[lsp,bigquery]`
28+
await pipInstall(pythonDetails, [sqlmeshWithExtras, customMaterializations])
29+
}
30+
31+
/**
32+
* Creates an entrypoint file used to test the LSP configuration.
33+
*
34+
* The entrypoint file is a bash script that simply calls out to the
35+
*/
36+
const createEntrypointFile = (
37+
tempDir: string,
38+
entrypointFileName: string,
39+
bitToStripFromArgs = '',
40+
): {
41+
entrypointFile: string
42+
fileWhereStoredInputs: string
43+
} => {
44+
const entrypointFile = path.join(tempDir, entrypointFileName)
45+
const fileWhereStoredInputs = path.join(tempDir, 'inputs.txt')
46+
const sqlmeshLSPFile = path.join(tempDir, '.venv/bin/sqlmesh_lsp')
47+
48+
// Create the entrypoint file
49+
fs.writeFileSync(
50+
entrypointFile,
51+
`#!/bin/bash
52+
echo "$@" > ${fileWhereStoredInputs}
53+
# Strip bitToStripFromArgs from the beginning of the args if it matches
54+
if [[ "$1" == "${bitToStripFromArgs}" ]]; then
55+
shift
56+
fi
57+
# Call the sqlmesh_lsp with the remaining arguments
58+
${sqlmeshLSPFile} "$@"`,
59+
{ mode: 0o755 }, // Make it executable
60+
)
61+
62+
return {
63+
entrypointFile,
64+
fileWhereStoredInputs,
65+
}
66+
}
67+
68+
test.describe('Test LSP Entrypoint configuration', () => {
69+
test('specify single entrypoint relalative path', async ({
70+
page,
71+
sharedCodeServer,
72+
tempDir,
73+
}) => {
74+
await fs.copy(SUSHI_SOURCE_PATH, tempDir)
75+
76+
await setupPythonEnvironment(tempDir)
77+
78+
const { fileWhereStoredInputs } = createEntrypointFile(
79+
tempDir,
80+
'entrypoint.sh',
81+
)
82+
83+
const settings = {
84+
'sqlmesh.lspEntrypoint': './entrypoint.sh',
85+
}
86+
// Write the settings to the settings.json file
87+
const settingsPath = path.join(tempDir, '.vscode', 'settings.json')
88+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
89+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
90+
91+
await openServerPage(page, tempDir, sharedCodeServer)
92+
93+
// Wait for the models folder to be visible
94+
await page.waitForSelector('text=models')
95+
96+
// Click on the models folder, excluding external_models
97+
await page
98+
.getByRole('treeitem', { name: 'models', exact: true })
99+
.locator('a')
100+
.click()
101+
102+
// Open the customer_revenue_lifetime model
103+
await page
104+
.getByRole('treeitem', { name: 'customers.sql', exact: true })
105+
.locator('a')
106+
.click()
107+
108+
await waitForLoadedSQLMesh(page)
109+
110+
// Check that the output file exists and contains the entrypoint script arguments
111+
expect(fs.existsSync(fileWhereStoredInputs)).toBe(true)
112+
expect(fs.readFileSync(fileWhereStoredInputs, 'utf8')).toBe(`--stdio
113+
`)
114+
})
115+
116+
test('specify one entrypoint absolute path', async ({
117+
page,
118+
sharedCodeServer,
119+
tempDir,
120+
}) => {
121+
await fs.copy(SUSHI_SOURCE_PATH, tempDir)
122+
123+
await setupPythonEnvironment(tempDir)
124+
125+
const { entrypointFile, fileWhereStoredInputs } = createEntrypointFile(
126+
tempDir,
127+
'entrypoint.sh',
128+
)
129+
// Assert that the entrypoint file is an absolute path
130+
expect(path.isAbsolute(entrypointFile)).toBe(true)
131+
132+
const settings = {
133+
'sqlmesh.lspEntrypoint': `${entrypointFile}`,
134+
}
135+
// Write the settings to the settings.json file
136+
const settingsPath = path.join(tempDir, '.vscode', 'settings.json')
137+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
138+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
139+
140+
await openServerPage(page, tempDir, sharedCodeServer)
141+
142+
// Wait for the models folder to be visible
143+
await page.waitForSelector('text=models')
144+
145+
// Click on the models folder, excluding external_models
146+
await page
147+
.getByRole('treeitem', { name: 'models', exact: true })
148+
.locator('a')
149+
.click()
150+
151+
// Open the customer_revenue_lifetime model
152+
await page
153+
.getByRole('treeitem', { name: 'customers.sql', exact: true })
154+
.locator('a')
155+
.click()
156+
157+
await waitForLoadedSQLMesh(page)
158+
159+
// Check that the output file exists and contains the entrypoint script arguments
160+
expect(fs.existsSync(fileWhereStoredInputs)).toBe(true)
161+
expect(fs.readFileSync(fileWhereStoredInputs, 'utf8')).toBe(`--stdio
162+
`)
163+
})
164+
165+
test('specify entrypoint with arguments', async ({
166+
page,
167+
sharedCodeServer,
168+
tempDir,
169+
}) => {
170+
await fs.copy(SUSHI_SOURCE_PATH, tempDir)
171+
172+
await setupPythonEnvironment(tempDir)
173+
174+
const { fileWhereStoredInputs } = createEntrypointFile(
175+
tempDir,
176+
'entrypoint.sh',
177+
'--argToIgnore',
178+
)
179+
180+
const settings = {
181+
'sqlmesh.lspEntrypoint': './entrypoint.sh --argToIgnore',
182+
}
183+
// Write the settings to the settings.json file
184+
const settingsPath = path.join(tempDir, '.vscode', 'settings.json')
185+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
186+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
187+
188+
await openServerPage(page, tempDir, sharedCodeServer)
189+
190+
// Wait for the models folder to be visible
191+
await page.waitForSelector('text=models')
192+
193+
// Click on the models folder, excluding external_models
194+
await page
195+
.getByRole('treeitem', { name: 'models', exact: true })
196+
.locator('a')
197+
.click()
198+
199+
// Open the customer_revenue_lifetime model
200+
await page
201+
.getByRole('treeitem', { name: 'customers.sql', exact: true })
202+
.locator('a')
203+
.click()
204+
205+
await waitForLoadedSQLMesh(page)
206+
207+
// Check that the output file exists and contains the entrypoint script arguments
208+
expect(fs.existsSync(fileWhereStoredInputs)).toBe(true)
209+
expect(fs.readFileSync(fileWhereStoredInputs, 'utf8'))
210+
.toBe(`--argToIgnore --stdio
211+
`)
212+
})
213+
})

0 commit comments

Comments
 (0)