Skip to content

Commit 74b3c43

Browse files
committed
feat(vscode): allow configuring project path
1 parent 8028656 commit 74b3c43

5 files changed

Lines changed: 140 additions & 79 deletions

File tree

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"sqlmesh.projectPath": {
3535
"type": "string",
3636
"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 or relative to the workspace root."
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."
3838
}
3939
}
4040
},

vscode/extension/src/lsp/lsp.ts

Lines changed: 4 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,6 @@ export class LSPClient implements Disposable {
4343
message: 'Invalid number of workspace folders',
4444
})
4545
}
46-
47-
const folder = workspaceFolders[0]
48-
// Use the workspace path from sqlmesh config, which respects the projectPath setting
4946
const workspacePath = sqlmesh.value.workspacePath
5047
const serverOptions: ServerOptions = {
5148
run: {
@@ -67,11 +64,13 @@ export class LSPClient implements Disposable {
6764
}
6865
const clientOptions: LanguageClientOptions = {
6966
documentSelector: [{ scheme: 'file', pattern: `**/*.sql` }],
70-
workspaceFolder: folder,
7167
diagnosticCollectionName: 'sqlmesh',
7268
outputChannel: outputChannel,
7369
}
7470

71+
traceInfo(
72+
`Starting SQLMesh Language Server with workspace path: ${workspacePath} with server options ${JSON.stringify(serverOptions)} and client options ${JSON.stringify(clientOptions)}`,
73+
)
7574
this.client = new LanguageClient(
7675
'sqlmesh-lsp',
7776
'SQLMesh Language Server',

vscode/extension/src/utilities/config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ export interface SqlmeshConfiguration {
1616
export function getSqlmeshConfiguration(): SqlmeshConfiguration {
1717
const config = workspace.getConfiguration('sqlmesh')
1818
const projectPath = config.get<string>('projectPath', '')
19-
2019
return {
2120
projectPath,
2221
}

vscode/extension/tests/lineage.spec.ts

Lines changed: 110 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -14,110 +14,152 @@ const SUSHI_SOURCE_PATH = path.join(__dirname, '..', '..', '..', 'examples', 'su
1414
* Helper function to launch VS Code and test lineage with given project path config
1515
*/
1616
async function testLineageWithProjectPath(
17-
workspaceDir: string,
18-
projectDir: string,
19-
projectPathConfig?: string
17+
window: Page,
2018
): Promise<void> {
21-
const ciArgs = process.env.CI ? [
22-
'--disable-gpu',
23-
'--headless',
24-
'--no-sandbox',
25-
'--disable-dev-shm-usage',
26-
'--window-position=-10000,0',
27-
] : [];
28-
29-
const userDataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-user-data-'));
30-
31-
try {
32-
// If projectPathConfig is provided, create .vscode/settings.json in the workspace
33-
if (projectPathConfig !== undefined) {
34-
const vscodeDir = path.join(workspaceDir, '.vscode');
35-
await fs.ensureDir(vscodeDir);
36-
const settings = {
37-
"sqlmesh.projectPath": projectPathConfig
38-
};
39-
await fs.writeJson(path.join(vscodeDir, 'settings.json'), settings, { spaces: 2 });
40-
}
41-
42-
const args = [
43-
...ciArgs,
44-
`--extensionDevelopmentPath=${EXT_PATH}`,
45-
'--disable-workspace-trust',
46-
'--disable-telemetry',
47-
`--user-data-dir=${userDataDir}`,
48-
workspaceDir,
49-
];
50-
51-
const electronApp = await electron.launch({
52-
executablePath: VS_CODE_EXE,
53-
args,
54-
});
55-
56-
const window = await electronApp.firstWindow();
57-
await window.waitForLoadState('domcontentloaded');
58-
await window.waitForLoadState('networkidle');
59-
60-
// Wait a bit for the extension to fully initialize with the settings
61-
await window.waitForTimeout(2000);
62-
6319
// Trigger lineage command
6420
await window.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+P' : 'Control+Shift+P');
6521
await window.keyboard.type('Lineage: Focus On View');
6622
await window.keyboard.press('Enter');
6723

6824
// Wait for "Loaded SQLmesh Context" text to appear
6925
const loadedContextText = window.locator('text=Loaded SQLMesh Context');
70-
await expect(loadedContextText.first()).toBeVisible({ timeout: 15000 });
26+
await expect(loadedContextText.first()).toBeVisible({ timeout: 10_000 });
27+
}
7128

29+
/**
30+
* Launch VS Code and return the window and a function to close the app.
31+
* @param workspaceDir The workspace directory to open.
32+
* @returns The window and a function to close the app.
33+
*/
34+
export const startVSCode = async (workspaceDir: string): Promise<{
35+
window: Page,
36+
close: () => Promise<void>,
37+
}> => {
38+
const userDataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-user-data-'));
39+
const ciArgs = process.env.CI ? [
40+
'--disable-gpu',
41+
'--headless',
42+
'--no-sandbox',
43+
'--disable-dev-shm-usage',
44+
'--window-position=-10000,0',
45+
] : [];
46+
const args = [
47+
...ciArgs,
48+
`--extensionDevelopmentPath=${EXT_PATH}`,
49+
'--disable-workspace-trust',
50+
'--disable-telemetry',
51+
`--user-data-dir=${userDataDir}`,
52+
workspaceDir,
53+
];
54+
const electronApp = await electron.launch({
55+
executablePath: VS_CODE_EXE,
56+
args,
57+
});
58+
const window = await electronApp.firstWindow();
59+
await window.waitForLoadState('domcontentloaded');
60+
await window.waitForLoadState('networkidle');
61+
await window.waitForTimeout(2_000);
62+
return { window, close: async () => {
7263
await electronApp.close();
73-
} finally {
7464
await fs.remove(userDataDir);
75-
}
76-
}
77-
78-
export const startVSCode = async (workspaceDir: string) => {
79-
65+
} };
8066
}
8167

8268
test('Lineage panel renders correctly - no project path config (default)', async () => {
8369
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-'));
8470
await fs.copy(SUSHI_SOURCE_PATH, tempDir);
85-
8671
try {
87-
await testLineageWithProjectPath(tempDir, tempDir);
88-
} finally {
72+
const { window, close } = await startVSCode(tempDir);
73+
await testLineageWithProjectPath(window);
74+
await close();
75+
} finally {
8976
await fs.remove(tempDir);
9077
}
9178
});
9279

9380
test('Lineage panel renders correctly - relative project path', async () => {
94-
// Create workspace directory with subdirectory containing the project
9581
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-workspace-'));
96-
const projectSubdir = path.join(workspaceDir, 'projects', 'sushi');
97-
await fs.ensureDir(path.dirname(projectSubdir));
98-
await fs.copy(SUSHI_SOURCE_PATH, projectSubdir);
82+
const projectDir = path.join(workspaceDir, 'projects', 'sushi');
83+
await fs.copy(SUSHI_SOURCE_PATH, projectDir);
84+
85+
const settings = {
86+
"sqlmesh.projectPath": "./projects/sushi",
87+
};
88+
await fs.ensureDir(path.join(workspaceDir, '.vscode'));
89+
await fs.writeJson(path.join(workspaceDir, '.vscode', 'settings.json'), settings, { spaces: 2 });
9990

10091
try {
101-
// Test with relative path
102-
await testLineageWithProjectPath(workspaceDir, projectSubdir, 'projects/sushi');
92+
const { window, close } = await startVSCode(workspaceDir);
93+
await testLineageWithProjectPath(window);
94+
await close();
10395
} finally {
10496
await fs.remove(workspaceDir);
10597
}
10698
});
10799

108100
test('Lineage panel renders correctly - absolute project path', async () => {
109-
// Create workspace directory
110101
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-workspace-'));
111-
112-
// Create project directory outside workspace
113-
const projectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-sushi-project-'));
102+
const projectDir = path.join(workspaceDir, 'projects', 'sushi');
103+
await fs.ensureDir(path.join(workspaceDir, '.vscode'));
114104
await fs.copy(SUSHI_SOURCE_PATH, projectDir);
105+
await fs.ensureDir(path.join(workspaceDir, '.vscode'));
106+
const settings = {
107+
"sqlmesh.projectPath": projectDir,
108+
};
109+
await fs.writeJson(path.join(workspaceDir, '.vscode', 'settings.json'), settings, { spaces: 2 });
115110

116111
try {
117-
// Test with absolute path
118-
await testLineageWithProjectPath(workspaceDir, projectDir, projectDir);
112+
const { window, close } = await startVSCode(workspaceDir);
113+
await testLineageWithProjectPath(window);
114+
await close();
119115
} finally {
120116
await fs.remove(workspaceDir);
121-
await fs.remove(projectDir);
122117
}
123118
});
119+
120+
121+
test("Lineage panel renders correctly - relative project outside of workspace", async () => {
122+
const tempFolder = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-workspace-'));
123+
const projectDir = path.join(tempFolder, 'projects', 'sushi');
124+
await fs.copy(SUSHI_SOURCE_PATH, projectDir);
125+
126+
const workspaceDir = path.join(tempFolder, 'workspace');
127+
await fs.ensureDir(workspaceDir);
128+
129+
const settings = {
130+
"sqlmesh.projectPath": "./../projects/sushi",
131+
};
132+
await fs.ensureDir(path.join(workspaceDir, '.vscode'));
133+
await fs.writeJson(path.join(workspaceDir, '.vscode', 'settings.json'), settings, { spaces: 2 });
134+
135+
try {
136+
const { window, close } = await startVSCode(workspaceDir);
137+
await testLineageWithProjectPath(window);
138+
await close();
139+
} finally {
140+
await fs.remove(tempFolder);
141+
}
142+
});
143+
144+
test("Lineage panel renders correctly - absolute path project outside of workspace", async () => {
145+
const tempFolder = await fs.mkdtemp(path.join(os.tmpdir(), 'vscode-test-workspace-'));
146+
const projectDir = path.join(tempFolder, 'projects', 'sushi');
147+
await fs.copy(SUSHI_SOURCE_PATH, projectDir);
148+
149+
const workspaceDir = path.join(tempFolder, 'workspace');
150+
await fs.ensureDir(workspaceDir);
151+
152+
const settings = {
153+
"sqlmesh.projectPath": projectDir,
154+
};
155+
await fs.ensureDir(path.join(workspaceDir, '.vscode'));
156+
await fs.writeJson(path.join(workspaceDir, '.vscode', 'settings.json'), settings, { spaces: 2 });
157+
158+
try {
159+
const { window, close } = await startVSCode(workspaceDir);
160+
await testLineageWithProjectPath(window);
161+
await close();
162+
} finally {
163+
await fs.remove(tempFolder);
164+
}
165+
});

0 commit comments

Comments
 (0)