Skip to content

Commit 63ae5f5

Browse files
committed
feat: workspace discovery
1 parent 06c28c1 commit 63ae5f5

File tree

9 files changed

+275
-5
lines changed

9 files changed

+275
-5
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: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,68 @@
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+
pub(crate) struct Config {
20+
root_path: PathBuf,
21+
workspace_roots: Vec<PathBuf>,
22+
discovered_projects: Vec<ProjectManifest>,
23+
}
24+
25+
impl Config {
26+
pub(crate) fn new(root_path: PathBuf, workspace_roots: Vec<PathBuf>) -> Self {
27+
Config { root_path, workspace_roots, discovered_projects: Default::default() }
28+
}
29+
30+
pub(crate) fn rediscover_workspaces(&mut self) {
31+
let discovered = ProjectManifest::discover_all(&self.workspace_roots);
32+
info!("discovered projects: {:?}", discovered);
33+
if discovered.is_empty() {
34+
error!("failed to find any projects in {:?}", &self.workspace_roots);
35+
}
36+
self.discovered_projects = discovered;
37+
}
38+
39+
pub(crate) fn root_path(&self) -> &Path {
40+
self.root_path.as_path()
41+
}
42+
}
43+
44+
pub(crate) fn negotiate_capabilities(params: InitializeParams) -> (ServerCapabilities, Config) {
45+
// todo: make this absolute guaranteed
46+
#[allow(deprecated)]
47+
let root_path = match params.root_uri.and_then(|it| it.to_file_path().ok()) {
48+
Some(it) => it,
49+
None => {
50+
// todo: unwrap
51+
env::current_dir().unwrap()
52+
}
53+
};
554

6-
#[derive(Default)]
7-
pub(crate) struct Config {}
55+
// todo: make this absolute guaranteed
56+
// The latest LSP spec mandates clients report `workspace_folders`, but some might still report
57+
// `root_uri`.
58+
let workspace_roots = params
59+
.workspace_folders
60+
.map(|workspaces| {
61+
workspaces.into_iter().filter_map(|it| it.uri.to_file_path().ok()).collect::<Vec<_>>()
62+
})
63+
.filter(|workspaces| !workspaces.is_empty())
64+
.unwrap_or_else(|| vec![root_path.clone()]);
865

9-
pub(crate) fn negotiate_capabilities(_: InitializeParams) -> (ServerCapabilities, Config) {
1066
(
1167
ServerCapabilities {
1268
text_document_sync: Some(TextDocumentSyncCapability::Options(
@@ -20,6 +76,6 @@ pub(crate) fn negotiate_capabilities(_: InitializeParams) -> (ServerCapabilities
2076
)),
2177
..Default::default()
2278
},
23-
Config::default(),
79+
Config::new(root_path, workspace_roots),
2480
)
2581
}

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+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use std::{
2+
fs::{ReadDir, read_dir},
3+
path::{Path, PathBuf},
4+
};
5+
6+
use solar_interface::data_structures::map::rustc_hash::FxHashSet;
7+
use tokio::io;
8+
9+
#[derive(Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
10+
pub(crate) enum ProjectManifest {
11+
// todo: guarantee this to be absolute
12+
Foundry(PathBuf),
13+
}
14+
15+
impl ProjectManifest {
16+
fn discover(path: &Path) -> io::Result<Vec<Self>> {
17+
return find_foundry_toml(path)
18+
.map(|paths| paths.into_iter().map(ProjectManifest::Foundry).collect());
19+
20+
fn find_foundry_toml(path: &Path) -> io::Result<Vec<PathBuf>> {
21+
match find_in_parent_dirs(path, "foundry.toml") {
22+
Some(it) => Ok(vec![it]),
23+
None => Ok(find_foundry_toml_in_child_dir(read_dir(path)?)),
24+
}
25+
}
26+
27+
fn find_in_parent_dirs(path: &Path, target_file_name: &str) -> Option<PathBuf> {
28+
if path.file_name().unwrap_or_default() == target_file_name {
29+
return Some(path.to_path_buf());
30+
}
31+
32+
let mut curr = Some(path);
33+
34+
while let Some(path) = curr {
35+
let candidate = path.join(target_file_name);
36+
if std::fs::metadata(&candidate).is_ok() {
37+
return Some(candidate);
38+
}
39+
40+
curr = path.parent();
41+
}
42+
43+
None
44+
}
45+
46+
fn find_foundry_toml_in_child_dir(entities: ReadDir) -> Vec<PathBuf> {
47+
entities
48+
.filter_map(Result::ok)
49+
.map(|it| it.path().join("foundry.toml"))
50+
.filter(|it| it.exists())
51+
.collect()
52+
}
53+
}
54+
55+
/// Discover all project manifests at the given paths.
56+
///
57+
/// Returns a `Vec` of discovered [`ProjectManifest`]s, which is guaranteed to be unique and
58+
/// sorted.
59+
pub(crate) fn discover_all(paths: &[PathBuf]) -> Vec<Self> {
60+
let mut res = paths
61+
.iter()
62+
.filter_map(|it| Self::discover(it.as_ref()).ok())
63+
.flatten()
64+
.collect::<FxHashSet<_>>()
65+
.into_iter()
66+
.collect::<Vec<_>>();
67+
res.sort();
68+
res
69+
}
70+
}

crates/lsp/src/workspace/mod.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//! Workspace models.
2+
//!
3+
//! Solar LSP supports multiple workspace models that are configured in different ways.
4+
//!
5+
//! This module contains a generic workspace concept, as well as implementations of different
6+
//! project models (e.g. Foundry projects), and a project discovery algorithm to try and determine
7+
//! what kind of project the LSP is dealing with based on different heuristics.
8+
//!
9+
//! Once a project type is identified, the configuration for that project model is merged into the
10+
//! overall LSP config.
11+
use crate::workspace::foundry::FoundryConfig;
12+
13+
mod foundry;
14+
pub(crate) mod manifest;
15+
16+
#[derive(Debug)]
17+
pub(crate) struct Workspace {
18+
pub(crate) kind: WorkspaceKind,
19+
}
20+
21+
#[derive(Debug)]
22+
pub(crate) enum WorkspaceKind {
23+
Foundry {
24+
foundry: FoundryConfig,
25+
},
26+
/// A naked workspace is a workspace with no specific configuration.
27+
///
28+
/// Naked workspaces have no remappings or toolchain-style dependencies, so all imports are
29+
/// assumed to be relative to the file being parsed.
30+
Naked,
31+
}
32+
33+
impl Workspace {
34+
pub(crate) fn load() -> Self {
35+
todo!()
36+
}
37+
}

0 commit comments

Comments
 (0)