Skip to content

Commit 6b3b622

Browse files
committed
feat: workspace discovery
1 parent 5adf4bc commit 6b3b622

File tree

13 files changed

+374
-21
lines changed

13 files changed

+374
-21
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/interface/src/diagnostics/message.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ pub struct SpanLabel {
6060
/// A collection of `Span`s.
6161
///
6262
/// Spans have two orthogonal attributes:
63-
/// - They can be *primary spans*. In this case they are the locus of the error, and would be
63+
/// - They can be *primary spans*. In this case they are the focus of the error, and would be
6464
/// rendered with `^^^`.
6565
/// - They can have a *label*. In this case, the label is written next to the mark in the snippet
6666
/// when we render.

crates/lsp/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ async-lsp = { workspace = true, features = ["omni-trait"] }
3232
crop = { workspace = true, features = ["utf16-metric"] }
3333
lsp-types.workspace = true
3434
normalize-path.workspace = true
35+
serde.workspace = true
36+
serde_json.workspace = true
3537
tower.workspace = true
3638
tokio = { workspace = true, features = ["rt"] }
3739
tracing.workspace = true

crates/lsp/src/config.rs

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,79 @@
1+
use std::{
2+
env,
3+
path::{Path, PathBuf},
4+
};
5+
16
use lsp_types::{
27
InitializeParams, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
38
TextDocumentSyncOptions,
49
};
10+
use tracing::{error, info};
11+
12+
use crate::workspace::manifest::ProjectManifest;
13+
14+
/// The LSP config.
15+
///
16+
/// This struct is internal only and should not be serialized or deserialized. Instead, values in
17+
/// this struct are the full view of all merged config sources, such as `initialization_opts`,
18+
/// on-disk config files (e.g. `foundry.toml`).
19+
#[derive(Default, Clone, Debug)]
20+
pub(crate) struct Config {
21+
root_path: PathBuf,
22+
workspace_roots: Vec<PathBuf>,
23+
discovered_projects: Vec<ProjectManifest>,
24+
}
25+
26+
impl Config {
27+
pub(crate) fn new(root_path: PathBuf, workspace_roots: Vec<PathBuf>) -> Self {
28+
Config { root_path, workspace_roots, discovered_projects: Default::default() }
29+
}
30+
31+
pub(crate) fn rediscover_workspaces(&mut self) {
32+
let discovered = ProjectManifest::discover_all(&self.workspace_roots);
33+
info!("discovered projects: {:?}", discovered);
34+
if discovered.is_empty() {
35+
error!("failed to find any projects in {:?}", &self.workspace_roots);
36+
}
37+
self.discovered_projects = discovered;
38+
}
39+
40+
pub(crate) fn remove_workspace(&mut self, path: &PathBuf) {
41+
if let Some(pos) = self.workspace_roots.iter().position(|it| it == path) {
42+
self.workspace_roots.remove(pos);
43+
}
44+
}
45+
46+
pub(crate) fn add_workspaces(&mut self, paths: impl Iterator<Item = PathBuf>) {
47+
self.workspace_roots.extend(paths);
48+
}
49+
50+
pub(crate) fn root_path(&self) -> &Path {
51+
self.root_path.as_path()
52+
}
53+
}
54+
55+
pub(crate) fn negotiate_capabilities(params: InitializeParams) -> (ServerCapabilities, Config) {
56+
// todo: make this absolute guaranteed
57+
#[allow(deprecated)]
58+
let root_path = match params.root_uri.and_then(|it| it.to_file_path().ok()) {
59+
Some(it) => it,
60+
None => {
61+
// todo: unwrap
62+
env::current_dir().unwrap()
63+
}
64+
};
565

6-
#[derive(Default)]
7-
pub(crate) struct Config {}
66+
// todo: make this absolute guaranteed
67+
// The latest LSP spec mandates clients report `workspace_folders`, but some might still report
68+
// `root_uri`.
69+
let workspace_roots = params
70+
.workspace_folders
71+
.map(|workspaces| {
72+
workspaces.into_iter().filter_map(|it| it.uri.to_file_path().ok()).collect::<Vec<_>>()
73+
})
74+
.filter(|workspaces| !workspaces.is_empty())
75+
.unwrap_or_else(|| vec![root_path.clone()]);
876

9-
pub(crate) fn negotiate_capabilities(_: InitializeParams) -> (ServerCapabilities, Config) {
1077
(
1178
ServerCapabilities {
1279
text_document_sync: Some(TextDocumentSyncCapability::Options(
@@ -20,6 +87,6 @@ pub(crate) fn negotiate_capabilities(_: InitializeParams) -> (ServerCapabilities
2087
)),
2188
..Default::default()
2289
},
23-
Config::default(),
90+
Config::new(root_path, workspace_roots),
2491
)
2592
}

crates/lsp/src/global_state.rs

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,39 @@ use solar_interface::{
1515
use solar_sema::Compiler;
1616
use tokio::task::JoinHandle;
1717

18-
use crate::{NotifyResult, config::negotiate_capabilities, proto, vfs::Vfs};
18+
use crate::{
19+
NotifyResult,
20+
config::{Config, negotiate_capabilities},
21+
proto,
22+
vfs::Vfs,
23+
};
1924

2025
pub(crate) struct GlobalState {
2126
client: ClientSocket,
2227
pub(crate) vfs: Arc<RwLock<Vfs>>,
28+
pub(crate) config: Arc<Config>,
2329
analysis_version: usize,
2430
}
2531

2632
impl GlobalState {
2733
pub(crate) fn new(client: ClientSocket) -> Self {
28-
Self { client, vfs: Arc::new(Default::default()), analysis_version: 0 }
34+
Self {
35+
client,
36+
vfs: Arc::new(Default::default()),
37+
analysis_version: 0,
38+
config: Arc::new(Default::default()),
39+
}
2940
}
3041

3142
pub(crate) fn on_initialize(
3243
&mut self,
3344
params: InitializeParams,
3445
) -> impl Future<Output = Result<InitializeResult, ResponseError>> + use<> {
35-
let (capabilities, _config) = negotiate_capabilities(params);
46+
let (capabilities, mut config) = negotiate_capabilities(params);
47+
48+
config.rediscover_workspaces();
3649

50+
self.config = Arc::new(config);
3751
std::future::ready(Ok(InitializeResult {
3852
capabilities,
3953
server_info: Some(ServerInfo {
@@ -98,16 +112,36 @@ impl GlobalState {
98112
let _ = compiler.lower_asts();
99113
let _ = compiler.analysis();
100114

101-
// todo handle diagnostic clearing
102115
// todo clean this mess up boya
103-
let diagnostics: HashMap<_, Vec<_>> = diag_buffer
116+
let mut diagnostics: HashMap<lsp_types::Url, Vec<lsp_types::Diagnostic>> =
117+
HashMap::new();
118+
for (path, diagnostic) in diag_buffer
104119
.read()
105120
.iter()
106121
.filter_map(|diag| proto::diagnostic(compiler.sess().source_map(), diag))
107-
.fold(HashMap::new(), |mut diags, (path, diag)| {
108-
diags.entry(path).or_default().push(diag);
109-
diags
110-
});
122+
{
123+
diagnostics.entry(path).or_default().push(diagnostic);
124+
}
125+
126+
// For any other file that was parsed, we additionally load it into the VFS for
127+
// later, and set an empty diagnostics set. This is to clear the existing
128+
// diagnostics for files that went from an errored to an ok state, but tracking this
129+
// separately is more efficient given most of these are wasted allocations.
130+
for url in compiler
131+
.gcx()
132+
.sources
133+
.sources
134+
.iter()
135+
.filter_map(|source| source.file.name.as_real())
136+
.filter_map(|path| lsp_types::Url::from_file_path(path).ok())
137+
{
138+
// todo: all of this can be a `HashMap::try_insert` (<https://github.com/rust-lang/rust/issues/82766>)
139+
if diagnostics.contains_key(&url) {
140+
continue;
141+
}
142+
diagnostics.insert(url, Vec::new());
143+
}
144+
111145
for (uri, diagnostics) in diagnostics.into_iter() {
112146
let _ = snapshot.client.publish_diagnostics(PublishDiagnosticsParams::new(
113147
uri,
@@ -122,7 +156,11 @@ impl GlobalState {
122156
}
123157

124158
fn snapshot(&self) -> GlobalStateSnapshot {
125-
GlobalStateSnapshot { client: self.client.clone(), vfs: self.vfs.clone() }
159+
GlobalStateSnapshot {
160+
client: self.client.clone(),
161+
vfs: self.vfs.clone(),
162+
config: self.config.clone(),
163+
}
126164
}
127165

128166
fn spawn_with_snapshot<T: Send + 'static>(
@@ -137,4 +175,5 @@ impl GlobalState {
137175
pub(crate) struct GlobalStateSnapshot {
138176
client: ClientSocket,
139177
vfs: Arc<RwLock<Vfs>>,
178+
config: Arc<Config>,
140179
}

crates/lsp/src/handlers/notifs.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
use std::ops::ControlFlow;
1+
use std::{ops::ControlFlow, sync::Arc};
22

33
use crop::Rope;
44
use lsp_types::{
5-
DidChangeConfigurationParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams,
6-
DidOpenTextDocumentParams,
5+
DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWorkspaceFoldersParams,
6+
DidCloseTextDocumentParams, DidOpenTextDocumentParams,
77
};
8-
use tracing::error;
8+
use tracing::{error, info};
99

1010
use crate::{NotifyResult, global_state::GlobalState, proto, utils::apply_document_changes};
1111

1212
pub(crate) fn did_open_text_document(
1313
state: &mut GlobalState,
1414
params: DidOpenTextDocumentParams,
1515
) -> NotifyResult {
16+
info!("config: {:?}", state.config);
1617
if let Some(path) = proto::vfs_path(&params.text_document.uri) {
1718
let already_exists = state.vfs.read().exists(&path);
1819
if already_exists {
@@ -81,3 +82,24 @@ pub(crate) fn did_change_configuration(
8182
// For now this is just a stub.
8283
ControlFlow::Continue(())
8384
}
85+
86+
pub(crate) fn did_change_workspace_folders(
87+
state: &mut GlobalState,
88+
params: DidChangeWorkspaceFoldersParams,
89+
) -> NotifyResult {
90+
let config = Arc::make_mut(&mut state.config);
91+
92+
for workspace in params.event.removed {
93+
let Ok(path) = workspace.uri.to_file_path() else {
94+
continue;
95+
};
96+
config.remove_workspace(&path);
97+
}
98+
99+
let added = params.event.added.into_iter().filter_map(|it| it.uri.to_file_path().ok());
100+
config.add_workspaces(added);
101+
102+
// todo: rediscover workspaces & refetch configs
103+
104+
ControlFlow::Continue(())
105+
}

crates/lsp/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ mod config;
2020
mod global_state;
2121
mod handlers;
2222
mod proto;
23+
mod serde;
2324
mod utils;
2425
mod vfs;
26+
mod workspace;
2527

2628
pub(crate) type NotifyResult = ControlFlow<async_lsp::Result<()>>;
2729

@@ -36,6 +38,9 @@ fn new_router(client: ClientSocket) -> Router<GlobalState> {
3638
.request::<req::Shutdown, _>(|_, _| std::future::ready(Ok(())))
3739
.notification::<notif::Exit>(|_, _| ControlFlow::Break(Ok(())));
3840

41+
// Workspace management
42+
router.notification::<notif::DidChangeWorkspaceFolders>(handlers::did_change_workspace_folders);
43+
3944
// Notifications
4045
router
4146
.notification::<notif::DidOpenTextDocument>(handlers::did_open_text_document)

crates/lsp/src/serde.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//! Serde serializers and deserializers.
2+
3+
pub(crate) mod display_fromstr {
4+
use std::{fmt::Display, str::FromStr};
5+
6+
use serde::{Deserialize, Deserializer, Serializer, de};
7+
8+
pub(crate) fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
9+
where
10+
T: Display,
11+
S: Serializer,
12+
{
13+
serializer.collect_str(value)
14+
}
15+
16+
pub(crate) fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
17+
where
18+
T: FromStr,
19+
T::Err: Display,
20+
D: Deserializer<'de>,
21+
{
22+
String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
23+
}
24+
25+
pub(crate) mod vec {
26+
use std::{
27+
fmt::{self, Display},
28+
marker::PhantomData,
29+
str::FromStr,
30+
};
31+
32+
use serde::{
33+
Deserializer, Serializer,
34+
de::{SeqAccess, Visitor},
35+
ser::SerializeSeq,
36+
};
37+
38+
pub(crate) fn serialize<T, S>(value: &[T], serializer: S) -> Result<S::Ok, S::Error>
39+
where
40+
T: Display,
41+
S: Serializer,
42+
{
43+
let mut seq = serializer.serialize_seq(Some(value.len()))?;
44+
for val in value {
45+
seq.serialize_element(&val.to_string())?;
46+
}
47+
seq.end()
48+
}
49+
50+
pub(crate) fn deserialize<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
51+
where
52+
T: FromStr,
53+
T::Err: fmt::Display,
54+
D: Deserializer<'de>,
55+
{
56+
struct VecVisitor<T> {
57+
marker: PhantomData<T>,
58+
}
59+
60+
impl<'de, T> Visitor<'de> for VecVisitor<T>
61+
where
62+
T: FromStr,
63+
T::Err: fmt::Display,
64+
{
65+
type Value = Vec<T>;
66+
67+
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68+
formatter.write_str("a sequence")
69+
}
70+
71+
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
72+
where
73+
A: SeqAccess<'de>,
74+
{
75+
let mut values = Vec::<T>::with_capacity(seq.size_hint().unwrap_or(0));
76+
while let Some(value) = seq.next_element::<&str>()? {
77+
values.push(T::from_str(value).map_err(serde::de::Error::custom)?);
78+
}
79+
Ok(values)
80+
}
81+
}
82+
83+
let visitor = VecVisitor { marker: PhantomData };
84+
deserializer.deserialize_seq(visitor)
85+
}
86+
}
87+
}

crates/lsp/src/vfs/fs.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,8 @@ impl Vfs {
7171
pub(crate) fn iter(&self) -> impl Iterator<Item = (&VfsPath, &Rope)> {
7272
self.data.iter()
7373
}
74+
75+
pub(crate) fn is_empty(&self) -> bool {
76+
self.data.is_empty()
77+
}
7478
}

0 commit comments

Comments
 (0)