Skip to content

Commit 01114b5

Browse files
authored
feat(vscode): add ability to specify project path (#4520)
1 parent 419336a commit 01114b5

File tree

6 files changed

+292
-52
lines changed

6 files changed

+292
-52
lines changed

sqlmesh/lsp/main.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,7 @@ def initialize(ls: LanguageServer, params: types.InitializeParams) -> None:
6969
# Use user-provided instantiator to build the context
7070
created_context = self.context_class(paths=[folder_path])
7171
self.lsp_context = LSPContext(created_context)
72-
ls.show_message(
73-
f"Loaded SQLMesh context from {config_path}",
74-
types.MessageType.Info,
75-
)
72+
loaded_sqlmesh_message(ls, folder_path)
7673
return # Exit after successfully loading any config
7774
except Exception as e:
7875
ls.show_message(
@@ -94,6 +91,9 @@ def all_models(ls: LanguageServer, params: AllModelsRequest) -> AllModelsRespons
9491
@self.server.feature(API_FEATURE)
9592
def api(ls: LanguageServer, request: ApiRequest) -> t.Dict[str, t.Any]:
9693
ls.log_trace(f"API request: {request}")
94+
if self.lsp_context is None:
95+
current_path = Path.cwd()
96+
self._ensure_context_in_folder(current_path)
9797
if self.lsp_context is None:
9898
raise RuntimeError("No context found")
9999

@@ -291,6 +291,20 @@ def _context_get_or_load(self, document_uri: URI) -> LSPContext:
291291
raise RuntimeError("No context found")
292292
return self.lsp_context
293293

294+
def _ensure_context_in_folder(self, folder_uri: Path) -> None:
295+
if self.lsp_context is not None:
296+
return
297+
for ext in ("py", "yml", "yaml"):
298+
config_path = folder_uri / f"config.{ext}"
299+
if config_path.exists():
300+
try:
301+
created_context = self.context_class(paths=[folder_uri])
302+
self.lsp_context = LSPContext(created_context)
303+
loaded_sqlmesh_message(self.server, folder_uri)
304+
return
305+
except Exception as e:
306+
self.server.show_message(f"Error loading context: {e}", types.MessageType.Error)
307+
294308
def _ensure_context_for_document(
295309
self,
296310
document_uri: URI,
@@ -382,6 +396,13 @@ def start(self) -> None:
382396
self.server.start_io()
383397

384398

399+
def loaded_sqlmesh_message(ls: LanguageServer, folder: Path) -> None:
400+
ls.show_message(
401+
f"Loaded SQLMesh context from {folder}",
402+
types.MessageType.Info,
403+
)
404+
405+
385406
def main() -> None:
386407
# Example instantiator that just uses the same signature as your original `Context` usage.
387408
sqlmesh_server = SQLMeshLanguageServer(context_class=Context)

vscode/extension/package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@
2727
"ms-python.python"
2828
],
2929
"contributes": {
30+
"configuration": {
31+
"type": "object",
32+
"title": "SQLMesh",
33+
"properties": {
34+
"sqlmesh.projectPath": {
35+
"type": "string",
36+
"default": "",
37+
"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."
38+
}
39+
}
40+
},
3041
"viewsContainers": {
3142
"panel": [
3243
{

vscode/extension/src/lsp/lsp.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import { sqlmeshLspExec } from '../utilities/sqlmesh/sqlmesh'
99
import { err, isErr, ok, Result } from '@bus/result'
1010
import { getWorkspaceFolders } from '../utilities/common/vscodeapi'
11-
import { traceError } from '../utilities/common/log'
11+
import { traceError, traceInfo } from '../utilities/common/log'
1212
import { ErrorType } from '../utilities/errors'
1313
import { CustomLSPMethods } from './custom'
1414

@@ -43,9 +43,7 @@ export class LSPClient implements Disposable {
4343
message: 'Invalid number of workspace folders',
4444
})
4545
}
46-
47-
const folder = workspaceFolders[0]
48-
const workspacePath = workspaceFolders[0].uri.fsPath
46+
const workspacePath = sqlmesh.value.workspacePath
4947
const serverOptions: ServerOptions = {
5048
run: {
5149
command: sqlmesh.value.bin,
@@ -66,11 +64,13 @@ export class LSPClient implements Disposable {
6664
}
6765
const clientOptions: LanguageClientOptions = {
6866
documentSelector: [{ scheme: 'file', pattern: `**/*.sql` }],
69-
workspaceFolder: folder,
7067
diagnosticCollectionName: 'sqlmesh',
7168
outputChannel: outputChannel,
7269
}
7370

71+
traceInfo(
72+
`Starting SQLMesh Language Server with workspace path: ${workspacePath} with server options ${JSON.stringify(serverOptions)} and client options ${JSON.stringify(clientOptions)}`,
73+
)
7474
this.client = new LanguageClient(
7575
'sqlmesh-lsp',
7676
'SQLMesh Language Server',
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { workspace, WorkspaceFolder } from 'vscode'
2+
import path from 'path'
3+
import fs from 'fs'
4+
import { Result, err, ok } from '@bus/result'
5+
import { traceVerbose, traceInfo } from './common/log'
6+
7+
export interface SqlmeshConfiguration {
8+
projectPath: string
9+
}
10+
11+
/**
12+
* Get the SQLMesh configuration from VS Code settings.
13+
*
14+
* @returns The SQLMesh configuration
15+
*/
16+
export function getSqlmeshConfiguration(): SqlmeshConfiguration {
17+
const config = workspace.getConfiguration('sqlmesh')
18+
const projectPath = config.get<string>('projectPath', '')
19+
return {
20+
projectPath,
21+
}
22+
}
23+
24+
/**
25+
* Validate and resolve the project path from configuration.
26+
* If no project path is configured, use the workspace folder.
27+
* If the project path is configured, it must be a directory that contains a SQLMesh project.
28+
*
29+
* @param workspaceFolder The current workspace folder
30+
* @returns A Result containing the resolved project path or an error
31+
*/
32+
export function resolveProjectPath(
33+
workspaceFolder: WorkspaceFolder,
34+
): Result<string, string> {
35+
const config = getSqlmeshConfiguration()
36+
37+
if (!config.projectPath) {
38+
// If no project path is configured, use the workspace folder
39+
traceVerbose('No project path configured, using workspace folder')
40+
return ok(workspaceFolder.uri.fsPath)
41+
}
42+
let resolvedPath: string
43+
44+
// Check if the path is absolute
45+
if (path.isAbsolute(config.projectPath)) {
46+
resolvedPath = config.projectPath
47+
} else {
48+
// Resolve relative path from workspace root
49+
resolvedPath = path.join(workspaceFolder.uri.fsPath, config.projectPath)
50+
}
51+
52+
// Normalize the path
53+
resolvedPath = path.normalize(resolvedPath)
54+
55+
// Validate that the path exists
56+
if (!fs.existsSync(resolvedPath)) {
57+
return err(`Configured project path does not exist: ${resolvedPath}`)
58+
}
59+
60+
// Validate that it's a directory
61+
const stats = fs.statSync(resolvedPath)
62+
if (!stats.isDirectory()) {
63+
return err(`Configured project path is not a directory: ${resolvedPath}`)
64+
}
65+
66+
// Check if it contains SQLMesh project files (config.yaml, config.yml, or config.py)
67+
const configFiles = ['config.yaml', 'config.yml', 'config.py']
68+
const hasConfigFile = configFiles.some(file =>
69+
fs.existsSync(path.join(resolvedPath, file)),
70+
)
71+
if (!hasConfigFile) {
72+
traceInfo(`Warning: No SQLMesh configuration file found in ${resolvedPath}`)
73+
}
74+
75+
traceVerbose(`Using project path: ${resolvedPath}`)
76+
return ok(resolvedPath)
77+
}

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

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +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'
1415

1516
export interface SqlmeshExecInfo {
1617
workspacePath: string
@@ -28,8 +29,12 @@ export interface SqlmeshExecInfo {
2829
*/
2930
export const isTcloudProject = async (): Promise<Result<boolean, string>> => {
3031
const projectRoot = await getProjectRoot()
31-
const tcloudYamlPath = path.join(projectRoot.uri.fsPath, 'tcloud.yaml')
32-
const tcloudYmlPath = path.join(projectRoot.uri.fsPath, 'tcloud.yml')
32+
const resolvedPath = resolveProjectPath(projectRoot)
33+
if (isErr(resolvedPath)) {
34+
return err(resolvedPath.error)
35+
}
36+
const tcloudYamlPath = path.join(resolvedPath.value, 'tcloud.yaml')
37+
const tcloudYmlPath = path.join(resolvedPath.value, 'tcloud.yml')
3338
const isTcloudYamlFilePresent = fs.existsSync(tcloudYamlPath)
3439
const isTcloudYmlFilePresent = fs.existsSync(tcloudYmlPath)
3540
if (isTcloudYamlFilePresent || isTcloudYmlFilePresent) {
@@ -83,8 +88,15 @@ export const isSqlmeshEnterpriseInstalled = async (): Promise<
8388
return tcloudBin
8489
}
8590
const projectRoot = await getProjectRoot()
91+
const resolvedPath = resolveProjectPath(projectRoot)
92+
if (isErr(resolvedPath)) {
93+
return err({
94+
type: 'generic',
95+
message: resolvedPath.error,
96+
})
97+
}
8698
const called = await execAsync(tcloudBin.value, ['is_sqlmesh_installed'], {
87-
cwd: projectRoot.uri.fsPath,
99+
cwd: resolvedPath.value,
88100
})
89101
if (called.exitCode !== 0) {
90102
return err({
@@ -183,7 +195,14 @@ export const sqlmeshExec = async (): Promise<
183195
> => {
184196
const sqlmesh = IS_WINDOWS ? 'sqlmesh.exe' : 'sqlmesh'
185197
const projectRoot = await getProjectRoot()
186-
const workspacePath = projectRoot.uri.fsPath
198+
const resolvedPath = resolveProjectPath(projectRoot)
199+
if (isErr(resolvedPath)) {
200+
return err({
201+
type: 'generic',
202+
message: resolvedPath.error,
203+
})
204+
}
205+
const workspacePath = resolvedPath.value
187206
const interpreterDetails = await getInterpreterDetails()
188207
traceLog(`Interpreter details: ${JSON.stringify(interpreterDetails)}`)
189208
if (interpreterDetails.path) {
@@ -300,7 +319,14 @@ export const sqlmeshLspExec = async (): Promise<
300319
> => {
301320
const sqlmeshLSP = IS_WINDOWS ? 'sqlmesh_lsp.exe' : 'sqlmesh_lsp'
302321
const projectRoot = await getProjectRoot()
303-
const workspacePath = projectRoot.uri.fsPath
322+
const resolvedPath = resolveProjectPath(projectRoot)
323+
if (isErr(resolvedPath)) {
324+
return err({
325+
type: 'generic',
326+
message: resolvedPath.error,
327+
})
328+
}
329+
const workspacePath = resolvedPath.value
304330
const interpreterDetails = await getInterpreterDetails()
305331
traceLog(`Interpreter details: ${JSON.stringify(interpreterDetails)}`)
306332
if (interpreterDetails.path) {

0 commit comments

Comments
 (0)