Skip to content

Commit 0efd93e

Browse files
committed
feat: lsp config and cli
1 parent 6c3c02c commit 0efd93e

File tree

16 files changed

+290
-59
lines changed

16 files changed

+290
-59
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ solar-cli = { version = "=0.1.6", path = "crates/cli", default-features = false
9292
solar-config = { version = "=0.1.6", path = "crates/config", default-features = false }
9393
solar-data-structures = { version = "=0.1.6", path = "crates/data-structures", default-features = false }
9494
solar-interface = { version = "=0.1.6", path = "crates/interface", default-features = false }
95+
solar-lsp = { version = "=0.1.6", path = "crates/lsp", default-features = false }
9596
solar-macros = { version = "=0.1.6", path = "crates/macros", default-features = false }
9697
solar-parse = { version = "=0.1.6", path = "crates/parse", default-features = false }
9798
solar-sema = { version = "=0.1.6", path = "crates/sema", default-features = false }

crates/cli/src/args.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use clap::{Parser, Subcommand};
2+
use solar_config::Opts;
3+
4+
/// Blazingly fast Solidity compiler.
5+
#[derive(Clone, Debug, Default, Parser)]
6+
#[command(
7+
name = "solar",
8+
version = crate::version::SHORT_VERSION,
9+
long_version = crate::version::LONG_VERSION,
10+
arg_required_else_help = true,
11+
)]
12+
#[allow(clippy::manual_non_exhaustive)]
13+
pub struct Args {
14+
#[command(subcommand)]
15+
pub commands: Option<Subcommands>,
16+
#[command(flatten)]
17+
pub default_compile: Opts,
18+
}
19+
20+
#[derive(Debug, Clone, Subcommand)]
21+
pub enum Subcommands {
22+
/// Start the language server.
23+
Lsp,
24+
}

crates/cli/src/lib.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ pub mod utils;
1717
#[cfg(all(unix, any(target_env = "gnu", target_os = "macos")))]
1818
pub mod signal_handler;
1919

20+
mod args;
21+
pub use args::{Args, Subcommands};
22+
2023
/// Signal handler to extract a backtrace from stack overflow.
2124
///
2225
/// This is a no-op because this platform doesn't support our signal handler's requirements.
@@ -34,13 +37,14 @@ use alloy_primitives as _;
3437

3538
use tracing as _;
3639

37-
pub fn parse_args<I, T>(itr: I) -> Result<Opts, clap::Error>
40+
pub fn parse_args<I, T>(itr: I) -> Result<Args, clap::Error>
3841
where
3942
I: IntoIterator<Item = T>,
4043
T: Into<std::ffi::OsString> + Clone,
4144
{
42-
let mut opts = Opts::try_parse_from(itr)?;
43-
opts.finish()?;
45+
let mut opts = Args::try_parse_from(itr)?;
46+
opts.default_compile.finish()?;
47+
4448
Ok(opts)
4549
}
4650

crates/config/src/opts.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,8 @@ use clap::{Parser, ValueHint};
1111

1212
// TODO: implement `allow_paths`.
1313

14-
/// Blazingly fast Solidity compiler.
1514
#[derive(Clone, Debug, Default)]
1615
#[cfg_attr(feature = "clap", derive(Parser))]
17-
#[cfg_attr(feature = "clap", command(
18-
name = "solar",
19-
version = crate::version::SHORT_VERSION,
20-
long_version = crate::version::LONG_VERSION,
21-
arg_required_else_help = true,
22-
))]
2316
#[allow(clippy::manual_non_exhaustive)]
2417
pub struct Opts {
2518
/// Files to compile, or import remappings.

crates/interface/src/source_map/file.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ pub struct SourceFile {
190190
pub source_len: RelativeBytePos,
191191
/// Locations of lines beginnings in the source code.
192192
#[debug(skip)]
193-
pub lines: Vec<RelativeBytePos>,
193+
lines: Vec<RelativeBytePos>,
194194
/// Locations of multi-byte characters in the source code.
195195
#[debug(skip)]
196196
pub multibyte_chars: Vec<MultiByteChar>,

crates/lsp/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,20 @@ workspace = true
2626
[dependencies]
2727
solar-config = { workspace = true, features = ["version"] }
2828
solar-interface.workspace = true
29+
solar-sema.workspace = true
2930

30-
async-lsp.workspace = true
31+
async-lsp = { workspace = true, features = ["omni-trait"] }
3132
crop = { workspace = true, features = ["utf16-metric"] }
3233
lsp-types.workspace = true
3334
normalize-path.workspace = true
3435
tower.workspace = true
36+
tokio = { workspace = true, features = ["rt"] }
3537
tracing.workspace = true
3638

3739
# This is needed because Windows does not support truly asynchronous pipes.
3840
[target.'cfg(not(unix))'.dependencies]
3941
tokio-util = { workspace = true, features = ["compat"] }
40-
tokio = { workspace = true, features = ["io-std"] }
42+
tokio = { workspace = true, features = ["rt", "io-std"] }
4143

4244
[features]
4345
nightly = ["solar-config/nightly"]

crates/lsp/src/config.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use lsp_types::{
2+
InitializeParams, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
3+
TextDocumentSyncOptions,
4+
};
5+
6+
#[derive(Default)]
7+
pub(crate) struct Config {}
8+
9+
pub(crate) fn negotiate_capabilities(_: InitializeParams) -> (ServerCapabilities, Config) {
10+
(
11+
ServerCapabilities {
12+
text_document_sync: Some(TextDocumentSyncCapability::Options(
13+
TextDocumentSyncOptions {
14+
open_close: Some(true),
15+
change: Some(TextDocumentSyncKind::INCREMENTAL),
16+
will_save: None,
17+
will_save_wait_until: None,
18+
..Default::default()
19+
},
20+
)),
21+
..Default::default()
22+
},
23+
Config::default(),
24+
)
25+
}

crates/lsp/src/global_state.rs

Lines changed: 102 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,41 @@
1-
use std::{ops::ControlFlow, sync::Arc};
1+
use std::{collections::HashMap, ops::ControlFlow, sync::Arc};
22

3-
use async_lsp::{ClientSocket, ResponseError};
3+
use async_lsp::{ClientSocket, LanguageClient, ResponseError};
44
use lsp_types::{
55
InitializeParams, InitializeResult, InitializedParams, LogMessageParams, MessageType,
6-
ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind,
7-
TextDocumentSyncOptions, notification as notif,
6+
PublishDiagnosticsParams, ServerInfo,
87
};
98
use solar_config::version::SHORT_VERSION;
10-
use solar_interface::data_structures::sync::RwLock;
9+
use solar_interface::{
10+
Session,
11+
data_structures::sync::RwLock,
12+
diagnostics::{DiagCtxt, InMemoryEmitter},
13+
source_map::FileName,
14+
};
15+
use solar_sema::Compiler;
16+
use tokio::task::JoinHandle;
1117

12-
use crate::{NotifyResult, vfs::Vfs};
18+
use crate::{NotifyResult, config::negotiate_capabilities, proto, vfs::Vfs};
1319

1420
pub(crate) struct GlobalState {
1521
client: ClientSocket,
1622
pub(crate) vfs: Arc<RwLock<Vfs>>,
23+
analysis_version: usize,
1724
}
1825

1926
impl GlobalState {
2027
pub(crate) fn new(client: ClientSocket) -> Self {
21-
Self { client, vfs: Arc::new(Default::default()) }
28+
Self { client, vfs: Arc::new(Default::default()), analysis_version: 0 }
2229
}
2330

2431
pub(crate) fn on_initialize(
2532
&mut self,
26-
_: InitializeParams,
33+
params: InitializeParams,
2734
) -> impl Future<Output = Result<InitializeResult, ResponseError>> + use<> {
35+
let (capabilities, _config) = negotiate_capabilities(params);
36+
2837
std::future::ready(Ok(InitializeResult {
29-
capabilities: ServerCapabilities {
30-
text_document_sync: Some(TextDocumentSyncCapability::Options(
31-
TextDocumentSyncOptions {
32-
open_close: Some(true),
33-
change: Some(TextDocumentSyncKind::INCREMENTAL),
34-
will_save: None,
35-
will_save_wait_until: None,
36-
..Default::default()
37-
},
38-
)),
39-
..Default::default()
40-
},
38+
capabilities,
4139
server_info: Some(ServerInfo {
4240
name: "solar".into(),
4341
version: Some(SHORT_VERSION.into()),
@@ -46,32 +44,97 @@ impl GlobalState {
4644
}
4745

4846
pub(crate) fn on_initialized(&mut self, _: InitializedParams) -> NotifyResult {
49-
self.info_msg("solar initialized");
47+
let _ = self.client.log_message(LogMessageParams {
48+
typ: MessageType::INFO,
49+
message: "solar initialized".into(),
50+
});
5051
ControlFlow::Continue(())
5152
}
52-
}
5353

54-
impl GlobalState {
55-
#[expect(unused)]
56-
fn warn_msg(&self, message: impl Into<String>) {
57-
let _ = self.client.notify::<notif::LogMessage>(LogMessageParams {
58-
typ: MessageType::WARNING,
59-
message: message.into(),
54+
/// Parses, lowers, and performs analysis on project files, including in-memory only files.
55+
///
56+
/// Each time analysis is triggered, a version is assigned to the analysis. A snapshot is then
57+
/// taken of the global state ([`GlobalStateSnapshot`]) and analysis is performed on
58+
/// the entire project in a separate thread.
59+
///
60+
/// Currently, Solar is sufficiently fast at parsing and lowering even large Solidity projects,
61+
/// so while analysing the entire project is relatively expensive compared to incremental
62+
/// analysis, it is still fast enough for most workloads. A potential improvement would be to
63+
/// enable incremental parsing and analysis in Solar using e.g. [`salsa`].
64+
///
65+
/// [`salsa`]: https://docs.rs/salsa/latest/salsa/
66+
pub(crate) fn recompute(&mut self) {
67+
self.analysis_version += 1;
68+
let version = self.analysis_version;
69+
self.spawn_with_snapshot(move |mut snapshot| {
70+
// todo: if this errors, we should notify the user
71+
// todo: set base path to project root
72+
// todo: remappings
73+
let (emitter, diag_buffer) = InMemoryEmitter::new();
74+
let sess = Session::builder().dcx(DiagCtxt::new(Box::new(emitter))).build();
75+
76+
let mut compiler = Compiler::new(sess);
77+
let _ = compiler.enter_mut(move |compiler| -> solar_interface::Result<_> {
78+
// Parse the files.
79+
let mut parsing_context = compiler.parse();
80+
// todo: unwraps
81+
parsing_context.add_files(snapshot.vfs.read().iter().map(|(path, contents)| {
82+
compiler
83+
.sess()
84+
.source_map()
85+
.new_source_file(
86+
FileName::real(path.as_path().unwrap()),
87+
contents.to_string(),
88+
)
89+
.unwrap()
90+
}));
91+
92+
parsing_context.parse();
93+
94+
// Perform lowering and analysis.
95+
// We should never encounter `ControlFlow::Break` because we do not stop after
96+
// parsing, so we ignore the return.
97+
// todo: handle errors (currently this always errors?)
98+
let _ = compiler.lower_asts();
99+
let _ = compiler.analysis();
100+
101+
// todo handle diagnostic clearing
102+
// todo clean this mess up boya
103+
let diagnostics: HashMap<_, Vec<_>> = diag_buffer
104+
.read()
105+
.iter()
106+
.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+
});
111+
for (uri, diagnostics) in diagnostics.into_iter() {
112+
let _ = snapshot.client.publish_diagnostics(PublishDiagnosticsParams::new(
113+
uri,
114+
diagnostics,
115+
None,
116+
));
117+
}
118+
119+
Ok(())
120+
});
60121
});
61122
}
62123

63-
#[expect(unused)]
64-
fn error_msg(&self, message: impl Into<String>) {
65-
let _ = self.client.notify::<notif::LogMessage>(LogMessageParams {
66-
typ: MessageType::ERROR,
67-
message: message.into(),
68-
});
124+
fn snapshot(&self) -> GlobalStateSnapshot {
125+
GlobalStateSnapshot { client: self.client.clone(), vfs: self.vfs.clone() }
69126
}
70127

71-
fn info_msg(&self, message: impl Into<String>) {
72-
let _ = self.client.notify::<notif::LogMessage>(LogMessageParams {
73-
typ: MessageType::INFO,
74-
message: message.into(),
75-
});
128+
fn spawn_with_snapshot<T: Send + 'static>(
129+
&self,
130+
f: impl FnOnce(GlobalStateSnapshot) -> T + Send + 'static,
131+
) -> JoinHandle<T> {
132+
let snapshot = self.snapshot();
133+
tokio::task::spawn_blocking(move || f(snapshot))
76134
}
77135
}
136+
137+
pub(crate) struct GlobalStateSnapshot {
138+
client: ClientSocket,
139+
vfs: Arc<RwLock<Vfs>>,
140+
}

crates/lsp/src/handlers/notifs.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ use std::ops::ControlFlow;
22

33
use crop::Rope;
44
use lsp_types::{
5-
DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
5+
DidChangeConfigurationParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams,
6+
DidOpenTextDocumentParams,
67
};
78
use tracing::error;
89

@@ -18,7 +19,12 @@ pub(crate) fn did_open_text_document(
1819
error!(?path, "duplicate DidOpenTextDocument");
1920
}
2021

21-
state.vfs.write().set_file_contents(path, Some(Rope::from(params.text_document.text)));
22+
let mut vfs = state.vfs.write();
23+
vfs.set_file_contents(path, Some(Rope::from(params.text_document.text)));
24+
if vfs.mark_clean() {
25+
drop(vfs);
26+
state.recompute();
27+
}
2228
}
2329

2430
ControlFlow::Continue(())
@@ -42,6 +48,7 @@ pub(crate) fn did_change_text_document(
4248

4349
if changed {
4450
state.vfs.write().set_file_contents(path, Some(new_contents));
51+
state.recompute();
4552
}
4653
}
4754

@@ -58,7 +65,19 @@ pub(crate) fn did_close_text_document(
5865
}
5966

6067
state.vfs.write().set_file_contents(path, None);
68+
state.recompute();
6169
}
6270

6371
ControlFlow::Continue(())
6472
}
73+
74+
pub(crate) fn did_change_configuration(
75+
_: &mut GlobalState,
76+
_: DidChangeConfigurationParams,
77+
) -> NotifyResult {
78+
// As stated in https://github.com/microsoft/language-server-protocol/issues/676,
79+
// this notification's parameters should be ignored and the actual config queried separately.
80+
//
81+
// For now this is just a stub.
82+
ControlFlow::Continue(())
83+
}

0 commit comments

Comments
 (0)