Skip to content

Commit 55f30f8

Browse files
committed
feat: workspace discovery
1 parent 06c28c1 commit 55f30f8

File tree

11 files changed

+333
-12
lines changed

11 files changed

+333
-12
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
@@ -51,7 +51,7 @@ pub struct SpanLabel {
5151
/// A collection of `Span`s.
5252
///
5353
/// Spans have two orthogonal attributes:
54-
/// - They can be *primary spans*. In this case they are the locus of the error, and would be
54+
/// - They can be *primary spans*. In this case they are the focus of the error, and would be
5555
/// rendered with `^^^`.
5656
/// - They can have a *label*. In this case, the label is written next to the mark in the snippet
5757
/// 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)]
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: 23 additions & 4 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 {
@@ -122,7 +136,11 @@ impl GlobalState {
122136
}
123137

124138
fn snapshot(&self) -> GlobalStateSnapshot {
125-
GlobalStateSnapshot { client: self.client.clone(), vfs: self.vfs.clone() }
139+
GlobalStateSnapshot {
140+
client: self.client.clone(),
141+
vfs: self.vfs.clone(),
142+
config: self.config.clone(),
143+
}
126144
}
127145

128146
fn spawn_with_snapshot<T: Send + 'static>(
@@ -137,4 +155,5 @@ impl GlobalState {
137155
pub(crate) struct GlobalStateSnapshot {
138156
client: ClientSocket,
139157
vfs: Arc<RwLock<Vfs>>,
158+
config: Arc<Config>,
140159
}

crates/lsp/src/handlers/notifs.rs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
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
};
88
use tracing::error;
99

@@ -81,3 +81,24 @@ pub(crate) fn did_change_configuration(
8181
// For now this is just a stub.
8282
ControlFlow::Continue(())
8383
}
84+
85+
pub(crate) fn did_change_workspace_folders(
86+
state: &mut GlobalState,
87+
params: DidChangeWorkspaceFoldersParams,
88+
) -> NotifyResult {
89+
let config = Arc::make_mut(&mut state.config);
90+
91+
for workspace in params.event.removed {
92+
let Ok(path) = workspace.uri.to_file_path() else {
93+
continue;
94+
};
95+
config.remove_workspace(&path);
96+
}
97+
98+
let added = params.event.added.into_iter().filter_map(|it| it.uri.to_file_path().ok());
99+
config.add_workspaces(added);
100+
101+
// todo: rediscover workspaces & refetch configs
102+
103+
ControlFlow::Continue(())
104+
}

crates/lsp/src/lib.rs

Lines changed: 2 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

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+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use serde::Deserialize;
2+
use solar_config::{EvmVersion, ImportRemapping};
3+
4+
/// A subset of `foundry.toml` that the LSP will parse
5+
/// using `forge config --json`.
6+
///
7+
/// This will be merged into the main configuration.
8+
#[derive(Debug, Deserialize)]
9+
pub(crate) struct FoundryConfig {
10+
#[serde(with = "crate::serde::display_fromstr::vec")]
11+
remappings: Vec<ImportRemapping>,
12+
#[serde(with = "crate::serde::display_fromstr")]
13+
evm_version: EvmVersion,
14+
}

0 commit comments

Comments
 (0)