Skip to content

Commit 691dece

Browse files
authored
Support multiroot workspaces (#1798)
Simplifies a bunch of logic for keeping track of baml projects <!-- ELLIPSIS_HIDDEN --> ---- > [!IMPORTANT] > Add support for multi-root workspaces by managing multiple `baml_src` directories in the language server. > > - **Behavior**: > - Support for multi-root workspaces by managing multiple `baml_src` directories in `Session`. > - Removed restriction on single workspace in `server.rs`. > - Added `get_or_create_project()` in `Session` to handle project lookup and creation. > - **API**: > - Added `getBAMLFunctions` request in `api.rs` to list functions from all projects. > - Updated `DidChangeTextDocumentHandler`, `DidOpenTextDocumentHandler`, and other notification handlers to use `get_or_create_project()`. > - Updated `CodeLens`, `Completion`, `GotoDefinition`, `Hover`, and `Rename` request handlers to support multi-root workspaces. > - **Diagnostics**: > - Updated `publish_diagnostics()` and `project_diagnostics()` in `diagnostics.rs` to handle diagnostics for multiple projects. > - **Misc**: > - Added tests for `get_or_create_project()` in `session.rs`. > - Minor logging improvements in `server.rs` and `session.rs`. > > <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=BoundaryML%2Fbaml&utm_source=github&utm_medium=referral)<sup> for 967e24b. It will automatically update as commits are pushed.</sup> <!-- ELLIPSIS_HIDDEN -->
1 parent f5654c8 commit 691dece

18 files changed

Lines changed: 297 additions & 142 deletions

File tree

engine/language_server/src/baml_project/file_utils.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ use lsp_types::{TextDocumentItem, Url};
1919
/// or `None` otherwise.
2020
pub fn find_top_level_parent(file_path: &Path) -> Option<PathBuf> {
2121
let mut current = file_path;
22+
if let Some(file_name) = current.file_name() {
23+
if file_name == "baml_src" {
24+
return Some(current.to_path_buf());
25+
}
26+
}
2227
while let Some(parent) = current.parent() {
2328
if let Some(dir_name) = parent.file_name() {
2429
if dir_name == "baml_src" {

engine/language_server/src/baml_project/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ impl BamlProject {
9090
}
9191
}
9292

93+
pub fn list_functions(&mut self) -> Vec<BamlFunction> {
94+
let runtime = self.runtime(HashMap::new());
95+
if let Ok(runtime) = runtime {
96+
runtime.list_functions()
97+
} else {
98+
vec![]
99+
}
100+
}
101+
93102
pub fn check_version(
94103
&self,
95104
generator_config: &BamlGeneratorConfig,

engine/language_server/src/server.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
//! Scheduling, I/O, and API endpoints.
22
33
use log::info;
4+
use lsp_types::{
5+
WorkspaceClientCapabilities, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
6+
};
47
use std::num::NonZeroUsize;
58
// The new PanicInfoHook name requires MSRV >= 1.82
69
#[allow(deprecated)]
@@ -98,6 +101,10 @@ impl Server {
98101
});
99102
(url, settings)
100103
};
104+
tracing::info!(
105+
"--- workspace folders: {:?}",
106+
init_params.workspace_folders.clone()
107+
);
101108

102109
let workspaces = init_params
103110
.workspace_folders
@@ -123,10 +130,6 @@ impl Server {
123130
anyhow::anyhow!("Failed to get the current working directory while creating a default workspace.")
124131
})?;
125132

126-
if workspaces.len() > 1 {
127-
// TODO(dhruvmanila): Support multi-root workspaces
128-
anyhow::bail!("Multi-root workspaces are not supported yet");
129-
}
130133
// for some reason tracing logs are not available before this point
131134
tracing::info!("Starting server with {} worker threads", worker_threads);
132135

@@ -337,6 +340,13 @@ impl Server {
337340
..Default::default()
338341
},
339342
)),
343+
workspace: Some(WorkspaceServerCapabilities {
344+
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
345+
supported: Some(true),
346+
change_notifications: Some(lsp_types::OneOf::Left(true)),
347+
}),
348+
..Default::default()
349+
}),
340350
..Default::default()
341351
}
342352
}

engine/language_server/src/server/api.rs

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use lsp_types::{
99
DidChangeTextDocumentParams, DocumentDiagnosticReport, DocumentDiagnosticReportResult,
1010
FullDocumentDiagnosticReport, RelatedFullDocumentDiagnosticReport,
1111
};
12-
use serde::Deserialize;
12+
use serde::{Deserialize, Serialize};
1313
use url::Url;
1414

1515
mod diagnostics;
@@ -32,6 +32,19 @@ use super::{
3232
Result,
3333
};
3434

35+
#[derive(serde::Serialize, serde::Deserialize)]
36+
struct BamlFunctionSpan {
37+
file_path: String,
38+
start: usize,
39+
end: usize,
40+
}
41+
#[derive(serde::Serialize, serde::Deserialize)]
42+
struct BamlFunctionResult {
43+
name: String,
44+
span: BamlFunctionSpan,
45+
}
46+
47+
struct BamlFunctionArg {}
3548
// --- Add debounce duration constant ---
3649
const DID_CHANGE_DEBOUNCE_DURATION: Duration = Duration::from_millis(250);
3750

@@ -68,6 +81,51 @@ pub(super) fn request<'a>(req: lsp_server::Request) -> Task<'a> {
6881
BackgroundSchedule::LatencySensitive,
6982
)
7083
}
84+
"getBAMLFunctions" => {
85+
tracing::info!("getBAMLFunctions");
86+
return Task::local(move |session, notifier, requester, responder| {
87+
let result: anyhow::Result<(serde_json::Value,)> = (|| {
88+
let mut all_functions = Vec::new();
89+
let projects = session.baml_src_projects.lock().unwrap();
90+
91+
for (_, project) in projects.iter() {
92+
let functions = project
93+
.lock()
94+
.unwrap()
95+
.baml_project
96+
.list_functions()
97+
.iter()
98+
.map(|f| BamlFunctionResult {
99+
name: f.name.clone(),
100+
span: BamlFunctionSpan {
101+
file_path: f.span.file_path.clone(),
102+
start: f.span.start,
103+
end: f.span.end,
104+
},
105+
})
106+
.collect::<Vec<BamlFunctionResult>>();
107+
108+
all_functions.extend(functions);
109+
}
110+
111+
let result = serde_json::to_value(all_functions);
112+
if let Ok(result) = result {
113+
Ok((result,))
114+
} else {
115+
Err(anyhow::anyhow!(
116+
"Failed to serialize functions: {:?}",
117+
result
118+
))
119+
}
120+
})();
121+
if let Ok((result,)) = result {
122+
responder.respond(id, Ok(result)).unwrap();
123+
} else {
124+
// no action
125+
// responder.respond(id, Err(result.unwrap_err())).unwrap();
126+
}
127+
});
128+
}
71129
"requestDiagnostics" => {
72130
tracing::info!("---- requestDiagnostics");
73131
return Task::local(move |session, notifier, requester, responder| {
@@ -78,10 +136,8 @@ pub(super) fn request<'a>(req: lsp_server::Request) -> Task<'a> {
78136
.map_err(|e| anyhow::anyhow!("Failed to parse JSON: {e}"))?;
79137
let url = Url::parse(&params.project_id)
80138
.map_err(|e| anyhow::anyhow!("Failed to parse URL: {e}"))?;
81-
session.ensure_project_db_for_baml_file(&url)?;
82-
// tracing::info!("url: {:?}", url);
83139
let project = session
84-
.project_db_for_path(url.to_file_path().unwrap())
140+
.get_or_create_project(&url.to_file_path().unwrap())
85141
.expect("Already checked for project's existence");
86142
project.lock().unwrap().update_runtime(Some(notifier))?;
87143

@@ -228,9 +284,9 @@ fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>(
228284
};
229285
info!(
230286
"session.projects.len(): {:?}",
231-
session.projects_by_workspace_folder.lock().unwrap().len()
287+
session.baml_src_projects.lock().unwrap().len()
232288
);
233-
let _db = session.project_db_for_path(path).clone();
289+
let _db = session.get_or_create_project(&path).clone();
234290
if _db.is_none() {
235291
tracing::error!("Could not find project for path");
236292
return Box::new(|_, _| {});

engine/language_server/src/server/api/diagnostics.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,8 @@ pub fn publish_session_lsp_diagnostics(
8787
) -> Result<()> {
8888
// let keys = session.index().documents.keys();
8989
let path = file_url.to_file_path().unwrap_or(PathBuf::new());
90-
let _ = session
91-
.ensure_project_db_for_baml_file(file_url)
92-
.map_err(|e| {
93-
tracing::error!("Failed to ensure project db for baml file: {}", e);
94-
});
9590
let project = session
96-
.project_db_for_path_mut(path)
91+
.get_or_create_project(&path)
9792
.expect("We just ensured the session is valid");
9893

9994
let diagnostics = project_diagnostics(project.clone());

engine/language_server/src/server/api/notifications/did_change.rs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,24 @@ impl SyncNotificationHandler for DidChangeTextDocumentHandler {
2424
_requester: &mut Requester,
2525
params: DidChangeTextDocumentParams,
2626
) -> Result<()> {
27-
tracing::info!("------- DidChangeTextDocumentHandler");
27+
tracing::info!("DidChangeTextDocumentHandler");
2828
let start_time_total = Instant::now();
2929

3030
let url = params.text_document.uri;
3131
let path = url
3232
.to_file_path()
3333
.internal_error_msg("Could not convert URL to path")?;
34-
session
35-
.ensure_project_db_for_baml_file(&url)
36-
.internal_error()?;
3734

38-
let project = session
39-
.project_db_for_path_mut(path)
40-
.expect("We ensured above that the project exists");
35+
// Get or create the project using the unified method
36+
let project = session.get_or_create_project(&path);
37+
if project.is_none() {
38+
tracing::error!("Failed to get or create project for path: {:?}", path);
39+
show_err_msg!("Failed to get or create project for path: {:?}", path);
40+
}
41+
42+
let project = project.unwrap();
4143
let document_key =
42-
DocumentKey::from_url(project.lock().unwrap().root_path(), &url).internal_error()?;
44+
DocumentKey::from_url(&project.lock().unwrap().root_path(), &url).internal_error()?;
4345

4446
session
4547
.update_text_document(
@@ -52,11 +54,7 @@ impl SyncNotificationHandler for DidChangeTextDocumentHandler {
5254

5355
tracing::info!("publishing diagnostics");
5456

55-
publish_diagnostics(
56-
&notifier,
57-
project.clone(),
58-
Some(params.text_document.version),
59-
)?;
57+
publish_diagnostics(&notifier, project, Some(params.text_document.version))?;
6058

6159
let elapsed = start_time_total.elapsed();
6260
tracing::info!("didchange total took {:?}ms", elapsed.as_millis());

engine/language_server/src/server/api/notifications/did_change_configuration.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ impl super::SyncNotificationHandler for DidChangeConfiguration {
1717
_requester: &mut Requester,
1818
_params: types::DidChangeConfigurationParams,
1919
) -> Result<()> {
20-
// TODO(jane): get this wired up after the pre-release
2120
Ok(())
2221
}
2322
}

engine/language_server/src/server/api/notifications/did_close.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler {
3333
.to_file_path()
3434
.internal_error_msg("Could not convert URL to path")?;
3535

36-
match session.project_db_for_path(path) {
36+
match session.get_or_create_project(&path) {
3737
None => {}
3838
Some(project) => {
3939
let document_key = DocumentKey::from_url(

engine/language_server/src/server/api/notifications/did_open.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,14 @@ impl SyncNotificationHandler for DidOpenTextDocumentHandler {
2424
tracing::info!("DidOpenTextDocumentHandler");
2525

2626
let url = params.text_document.uri;
27-
session
28-
.ensure_project_db_for_baml_file(&url)
29-
.internal_error()?;
27+
let file_path = url.to_file_path().internal_error()?;
28+
29+
let project = session.get_or_create_project(&file_path);
30+
if project.is_none() {
31+
tracing::error!("Failed to get or create project for path: {:?}", file_path);
32+
show_err_msg!("Failed to get or create project for path: {:?}", file_path);
33+
}
34+
3035
session.reload(Some(notifier.clone())).internal_error()?;
3136

3237
publish_session_lsp_diagnostics(&notifier, session, &url)?;

engine/language_server/src/server/api/notifications/did_save_text_document.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,7 @@ impl super::SyncNotificationHandler for DidSaveTextDocument {
2828
session.reload(Some(notifier.clone())).internal_error()?;
2929
tracing::info!("About to run generator. URL path: {:?}", path);
3030
session
31-
.ensure_project_db_for_baml_file(&url)
32-
.internal_error()?;
33-
session
34-
.project_db_for_path_mut(path)
31+
.get_or_create_project(&path)
3532
.expect("Ensured that a project db exists")
3633
.lock()
3734
.unwrap()

0 commit comments

Comments
 (0)