Skip to content

Commit 4c0885b

Browse files
authored
faet(vscode): support multiple workspaces (#4634)
1 parent 28567cd commit 4c0885b

3 files changed

Lines changed: 128 additions & 11 deletions

File tree

sqlmesh/lsp/main.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def __init__(
5656
self.server = LanguageServer(server_name, version)
5757
self.context_class = context_class
5858
self.lsp_context: t.Optional[LSPContext] = None
59+
self.workspace_folders: t.List[Path] = []
5960

6061
self.client_supports_pull_diagnostics = False
6162
# Register LSP features (e.g., formatting, hover, etc.)
@@ -68,7 +69,7 @@ def _register_features(self) -> None:
6869
def initialize(ls: LanguageServer, params: types.InitializeParams) -> None:
6970
"""Initialize the server when the client connects."""
7071
try:
71-
# Check if client supports pull diagnostics
72+
# Check if the client supports pull diagnostics
7273
if params.capabilities and params.capabilities.text_document:
7374
diagnostics = getattr(params.capabilities.text_document, "diagnostic", None)
7475
if diagnostics:
@@ -81,9 +82,13 @@ def initialize(ls: LanguageServer, params: types.InitializeParams) -> None:
8182
self.client_supports_pull_diagnostics = False
8283

8384
if params.workspace_folders:
85+
# Store all workspace folders for later use
86+
self.workspace_folders = [
87+
Path(self._uri_to_path(folder.uri)) for folder in params.workspace_folders
88+
]
89+
8490
# Try to find a SQLMesh config file in any workspace folder (only at the root level)
85-
for folder in params.workspace_folders:
86-
folder_path = Path(self._uri_to_path(folder.uri))
91+
for folder_path in self.workspace_folders:
8792
# Only check for config files directly in the workspace directory
8893
for ext in ("py", "yml", "yaml"):
8994
config_path = folder_path / f"config.{ext}"
@@ -104,8 +109,8 @@ def initialize(ls: LanguageServer, params: types.InitializeParams) -> None:
104109

105110
@self.server.feature(ALL_MODELS_FEATURE)
106111
def all_models(ls: LanguageServer, params: AllModelsRequest) -> AllModelsResponse:
112+
uri = URI(params.textDocument.uri)
107113
try:
108-
uri = URI(params.textDocument.uri)
109114
context = self._context_get_or_load(uri)
110115
return get_sql_completions(context, uri)
111116
except Exception as e:
@@ -457,6 +462,8 @@ def _context_get_or_load(self, document_uri: URI) -> LSPContext:
457462
def _ensure_context_in_folder(self, folder_uri: Path) -> None:
458463
if self.lsp_context is not None:
459464
return
465+
466+
# First, check the provided folder
460467
for ext in ("py", "yml", "yaml"):
461468
config_path = folder_uri / f"config.{ext}"
462469
if config_path.exists():
@@ -468,6 +475,22 @@ def _ensure_context_in_folder(self, folder_uri: Path) -> None:
468475
except Exception as e:
469476
self.server.show_message(f"Error loading context: {e}", types.MessageType.Error)
470477

478+
# If not found in the provided folder, search through all workspace folders
479+
for workspace_folder in self.workspace_folders:
480+
for ext in ("py", "yml", "yaml"):
481+
config_path = workspace_folder / f"config.{ext}"
482+
if config_path.exists():
483+
try:
484+
created_context = self.context_class(paths=[workspace_folder])
485+
self.lsp_context = LSPContext(created_context)
486+
loaded_sqlmesh_message(self.server, workspace_folder)
487+
return
488+
except Exception as e:
489+
self.server.show_message(
490+
f"Error loading context from {config_path}: {e}",
491+
types.MessageType.Warning,
492+
)
493+
471494
def _ensure_context_for_document(
472495
self,
473496
document_uri: URI,
@@ -506,6 +529,23 @@ def _ensure_context_for_document(
506529
)
507530
path = path.parent
508531

532+
# If still no context found, try the workspace folders
533+
if not loaded:
534+
for workspace_folder in self.workspace_folders:
535+
for ext in ("py", "yml", "yaml"):
536+
config_path = workspace_folder / f"config.{ext}"
537+
if config_path.exists():
538+
try:
539+
created_context = self.context_class(paths=[workspace_folder])
540+
self.lsp_context = LSPContext(created_context)
541+
loaded_sqlmesh_message(self.server, workspace_folder)
542+
return
543+
except Exception as e:
544+
self.server.show_message(
545+
f"Error loading context from {config_path}: {e}",
546+
types.MessageType.Warning,
547+
)
548+
509549
return
510550

511551
@staticmethod

vscode/extension/src/lsp/lsp.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,11 @@ export class LSPClient implements Disposable {
3434
return sqlmesh
3535
}
3636
const workspaceFolders = getWorkspaceFolders()
37-
if (workspaceFolders.length !== 1) {
38-
traceError(
39-
`Invalid number of workspace folders: ${workspaceFolders.length}`,
40-
)
37+
if (workspaceFolders.length === 0) {
38+
traceError(`No workspace folders found`)
4139
return err({
4240
type: 'generic',
43-
message: 'Invalid number of workspace folders',
41+
message: 'No workspace folders found',
4442
})
4543
}
4644
const workspacePath = sqlmesh.value.workspacePath

vscode/extension/tests/lineage.spec.ts

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from 'path'
33
import fs from 'fs-extra'
44
import os from 'os'
55
import { startVSCode, SUSHI_SOURCE_PATH } from './utils'
6+
import { writeFileSync } from 'fs'
67

78
/**
89
* Helper function to launch VS Code and test lineage with given project path config
@@ -15,8 +16,8 @@ async function testLineageWithProjectPath(window: Page): Promise<void> {
1516
await window.keyboard.type('Lineage: Focus On View')
1617
await window.keyboard.press('Enter')
1718

18-
// Wait for "Loaded SQLmesh Context" text to appear
19-
const loadedContextText = window.locator('text=Loaded SQLMesh Context')
19+
// Wait for "Loaded SQLMesh context" text to appear
20+
const loadedContextText = window.locator('text=Loaded SQLMesh context')
2021
await expect(loadedContextText.first()).toBeVisible({ timeout: 10_000 })
2122
}
2223

@@ -141,3 +142,81 @@ test('Lineage panel renders correctly - absolute path project outside of workspa
141142
await fs.remove(tempFolder)
142143
}
143144
})
145+
146+
test('Lineage panel renders correctly - multiworkspace setup', async () => {
147+
const workspaceDir = await fs.mkdtemp(
148+
path.join(os.tmpdir(), 'vscode-test-workspace-'),
149+
)
150+
const projectDir1 = path.join(workspaceDir, 'projects', 'sushi1')
151+
const projectDir2 = path.join(workspaceDir, 'projects', 'sushi2')
152+
await fs.copy(SUSHI_SOURCE_PATH, projectDir1)
153+
await fs.ensureDir(projectDir2)
154+
155+
// Add a .code-workspace file with multiple projects
156+
const workspaceFilePath = path.join(
157+
workspaceDir,
158+
'multi-workspace.code-workspace',
159+
)
160+
writeFileSync(
161+
workspaceFilePath,
162+
JSON.stringify({
163+
folders: [
164+
{
165+
name: 'sushi1',
166+
path: 'projects/sushi1',
167+
},
168+
{
169+
name: 'sushi2',
170+
path: 'projects/sushi2',
171+
},
172+
],
173+
}),
174+
)
175+
176+
try {
177+
const { window, close } = await startVSCode(workspaceFilePath)
178+
await testLineageWithProjectPath(window)
179+
await close()
180+
} finally {
181+
await fs.remove(workspaceDir)
182+
}
183+
})
184+
185+
test('Lineage panel renders correctly - multiworkspace setup reversed', async () => {
186+
const workspaceDir = await fs.mkdtemp(
187+
path.join(os.tmpdir(), 'vscode-test-workspace-'),
188+
)
189+
const projectDir1 = path.join(workspaceDir, 'projects', 'sushi1')
190+
const projectDir2 = path.join(workspaceDir, 'projects', 'sushi2')
191+
await fs.copy(SUSHI_SOURCE_PATH, projectDir2)
192+
await fs.ensureDir(projectDir1)
193+
194+
// Add a .code-workspace file with multiple projects
195+
const workspaceFilePath = path.join(
196+
workspaceDir,
197+
'multi-workspace.code-workspace',
198+
)
199+
writeFileSync(
200+
workspaceFilePath,
201+
JSON.stringify({
202+
folders: [
203+
{
204+
name: 'sushi1',
205+
path: 'projects/sushi1',
206+
},
207+
{
208+
name: 'sushi2',
209+
path: 'projects/sushi2',
210+
},
211+
],
212+
}),
213+
)
214+
215+
try {
216+
const { window, close } = await startVSCode(workspaceFilePath)
217+
await testLineageWithProjectPath(window)
218+
await close()
219+
} finally {
220+
await fs.remove(workspaceDir)
221+
}
222+
})

0 commit comments

Comments
 (0)