Experimenting with the LSP

This commit is contained in:
Mia
2026-03-08 14:34:12 +01:00
parent 2aababbbe1
commit 45fd421e19
16 changed files with 1682 additions and 228 deletions
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "leaf_lsp"
version = "0.1.0"
edition = "2024"
[dependencies]
leaf_parser = { path = "../parser" }
leaf_compiler = { path = "../compiler" }
leaf_assembly = { path = "../assembly" }
leaf_allocators = { path = "../allocators" }
tower-lsp-server = "0.23.0"
tokio = { version = "1.50.0", features = ["io-std", "macros", "rt-multi-thread", "sync"] }
scc = "3.6.9"
rust_search = "2.1.0"
rangemap = "1.7.1"
boxcar = "0.2.14"
+153
View File
@@ -0,0 +1,153 @@
use crate::utils::UriUtils;
use crate::workspace::{Workspace, start_workspace_thread};
use std::sync::Arc;
use tokio::sync::RwLock;
use tower_lsp_server::jsonrpc::Result;
use tower_lsp_server::ls_types::request::{GotoDeclarationParams, GotoDeclarationResponse};
use tower_lsp_server::{Client, LspService, Server};
use tower_lsp_server::{LanguageServer, ls_types::*};
mod utils;
mod workspace;
struct Backend {
client: Client,
workspaces: RwLock<Vec<Arc<Workspace>>>,
}
impl LanguageServer for Backend {
async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
let mut workspaces = self.workspaces.write().await;
for workspace in workspaces.drain(..) {
workspace.close().await;
}
for folder in params.workspace_folders.iter().flatten() {
workspaces
.push(start_workspace_thread(folder.uri.clone(), self.client.clone()).unwrap());
}
Ok(InitializeResult {
capabilities: ServerCapabilities {
hover_provider: Some(HoverProviderCapability::Simple(true)),
definition_provider: Some(OneOf::Left(true)),
declaration_provider: Some(DeclarationCapability::Simple(true)),
text_document_sync: Some(TextDocumentSyncCapability::Options(
TextDocumentSyncOptions {
save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
include_text: Some(false),
})),
..Default::default()
},
)),
semantic_tokens_provider: Some(
SemanticTokensServerCapabilities::SemanticTokensOptions(
SemanticTokensOptions {
legend: SemanticTokensLegend {
token_types: vec![
SemanticTokenType::TYPE,
SemanticTokenType::FUNCTION,
SemanticTokenType::NUMBER,
],
token_modifiers: vec![],
},
full: Some(SemanticTokensFullOptions::Bool(true)),
range: None,
..Default::default()
},
),
),
..Default::default()
},
..Default::default()
})
}
async fn initialized(&self, _: InitializedParams) {
self.client
.log_message(MessageType::INFO, "server initialized!")
.await;
}
async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
if let Some(w) = self
.find_workspace(&params.text_document_position_params.text_document.uri)
.await
{
return Ok(w.hover(params).await);
}
Ok(None)
}
async fn goto_declaration(
&self,
params: GotoDeclarationParams,
) -> Result<Option<GotoDeclarationResponse>> {
if let Some(w) = self
.find_workspace(&params.text_document_position_params.text_document.uri)
.await
{
return Ok(w.go_to_declaration(params).await);
}
Ok(None)
}
async fn goto_definition(
&self,
params: GotoDeclarationParams,
) -> Result<Option<GotoDeclarationResponse>> {
if let Some(w) = self
.find_workspace(&params.text_document_position_params.text_document.uri)
.await
{
return Ok(w.go_to_declaration(params).await);
}
Ok(None)
}
async fn did_save(&self, params: DidSaveTextDocumentParams) {
if let Some(w) = self.find_workspace(&params.text_document.uri).await {
w.reload().await;
}
}
async fn semantic_tokens_full(
&self,
params: SemanticTokensParams,
) -> Result<Option<SemanticTokensResult>> {
if let Some(w) = self.find_workspace(&params.text_document.uri).await {
return Ok(w.semantic_tokens(params).await);
}
Ok(None)
}
async fn shutdown(&self) -> Result<()> {
let mut workspaces = self.workspaces.write().await;
for workspace in workspaces.drain(..) {
workspace.close().await;
}
Ok(())
}
}
impl Backend {
pub async fn find_workspace(&self, file: &Uri) -> Option<Arc<Workspace>> {
let workspaces = self.workspaces.read().await;
let path = file.strip_header();
workspaces
.iter()
.find(|w| path.starts_with(&w.base))
.cloned()
}
}
#[tokio::main]
async fn main() {
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
let (service, socket) = LspService::new(|client| Backend {
client,
workspaces: RwLock::default(),
});
Server::new(stdin, stdout, socket).serve(service).await;
}
+35
View File
@@ -0,0 +1,35 @@
use leaf_compiler::metadata::CodePosition;
use tower_lsp_server::ls_types::{Position, Range, Uri};
pub trait UriUtils {
fn strip_header(&self) -> &str;
}
impl UriUtils for Uri {
fn strip_header(&self) -> &str {
let Some(("", str)) = self.as_str().split_once("file://") else {
unimplemented!()
};
str
}
}
pub trait CodePositionUtils {
fn lsp_range(&self) -> Range;
}
impl CodePositionUtils for CodePosition {
fn lsp_range(&self) -> Range {
let range = self.line_col();
Range {
start: Position {
line: range.start.line as u32,
character: range.start.column as u32,
},
end: Position {
line: range.end.line as u32,
character: range.end.column as u32,
},
}
}
}
+407
View File
@@ -0,0 +1,407 @@
use leaf_allocators::SyncArenaAllocator;
use leaf_assembly::{
assembly::{AssemblyIdentifier, Version},
types::{Type, derivations::PtrT},
values::{AnyConst, AnyValue, Value},
};
use leaf_compiler::{CompilationContext, events::Event, metadata::CodePosition};
use leaf_parser::{ArcStr, SourceCode};
use rangemap::RangeMap;
use rust_search::SearchBuilder;
use std::{
borrow::Cow,
collections::HashMap,
fmt::Write,
ops::Range,
path::PathBuf,
sync::{Arc, OnceLock},
};
use tokio::sync::{
Mutex, Notify, RwLock,
mpsc::{Sender, channel},
};
use tower_lsp_server::{
Client,
ls_types::{
Diagnostic, DiagnosticSeverity, Hover, HoverContents, HoverParams, Location, MarkedString,
NumberOrString, Position, Range as LspRange, SemanticToken, SemanticTokenType,
SemanticTokens, SemanticTokensParams, SemanticTokensResult, TextDocumentPositionParams,
Uri,
request::{GotoDeclarationParams, GotoDeclarationResponse},
},
};
use crate::utils::{CodePositionUtils, UriUtils};
pub struct Workspace {
pub base: String,
sender: Sender<Request>,
}
impl Workspace {
pub async fn close(&self) -> bool {
let notify = Arc::new(Notify::new());
self.sender
.send(Request::Close(notify.clone()))
.await
.unwrap();
notify.notified().await;
true
}
pub async fn hover(&self, params: HoverParams) -> Option<Hover> {
let result = Arc::<(OnceLock<Hover>, Notify)>::default();
self.sender
.send(Request::Hover(params, result.clone()))
.await
.unwrap();
result.1.notified().await;
result.0.get().cloned()
}
pub async fn go_to_declaration(
&self,
params: GotoDeclarationParams,
) -> Option<GotoDeclarationResponse> {
let result = Arc::<(OnceLock<GotoDeclarationResponse>, Notify)>::default();
self.sender
.send(Request::GoToDeclaration(params, result.clone()))
.await
.unwrap();
result.1.notified().await;
result.0.get().cloned()
}
pub async fn semantic_tokens(
&self,
params: SemanticTokensParams,
) -> Option<SemanticTokensResult> {
let result = Arc::<(OnceLock<SemanticTokensResult>, Notify)>::default();
self.sender
.send(Request::SemanticTokens(params, result.clone()))
.await
.unwrap();
result.1.notified().await;
result.0.get().cloned()
}
pub async fn reload(&self) {
self.sender.send(Request::Reload).await.unwrap();
}
}
enum Request {
Reload,
Close(Arc<Notify>),
Hover(HoverParams, Arc<(OnceLock<Hover>, Notify)>),
GoToDeclaration(
GotoDeclarationParams,
Arc<(OnceLock<GotoDeclarationResponse>, Notify)>,
),
SemanticTokens(
SemanticTokensParams,
Arc<(OnceLock<SemanticTokensResult>, Notify)>,
),
}
struct State<'l> {
context: CompilationContext<'l>,
files: Arc<RwLock<HashMap<String, FileInfo>>>,
}
#[derive(Default)]
struct FileInfo {
line_ranges: HashMap<u32, Range<usize>>,
symbol_ranges: RangeMap<usize, [u8; size_of::<AnyValue<'_>>()]>,
symbol_positions: HashMap<[u8; size_of::<AnyValue<'_>>()], CodePosition>,
symbol_definitions: HashMap<[u8; size_of::<AnyValue<'_>>()], CodePosition>,
}
impl<'l> State<'l> {
pub fn new(alloc: &'l SyncArenaAllocator) -> Self {
State {
context: CompilationContext::new(alloc),
files: Default::default(),
}
}
}
pub fn start_workspace_thread(url: Uri, client: Client) -> Result<Arc<Workspace>, String> {
let base = url.strip_header().to_string();
let path = base.clone();
let (sender, mut receiver) = channel(1);
let _handle = tokio::task::spawn(async move {
let mut alloc = SyncArenaAllocator::default();
'global: loop {
alloc.reset();
let mut state = State::new(&alloc);
let mut diagnostics = HashMap::<PathBuf, Vec<Diagnostic>>::new();
let files: Vec<_> = tokio::task::block_in_place(|| {
SearchBuilder::default()
.location(&path)
.ext("leaf")
.build()
.map(|f| {
let mut info = state.files.blocking_write();
let info = info.entry(f.clone()).or_default();
let text: ArcStr = std::fs::read_to_string(&f).unwrap().into();
info.line_ranges = calc_line_ranges(text.as_str());
diagnostics.entry(PathBuf::from(&f)).or_default();
Arc::new(SourceCode {
text,
file: f.into(),
})
})
.collect()
});
let diagnostics = Arc::new(Mutex::new(diagnostics));
{
let info = state.files.clone();
let diagnostics = diagnostics.clone();
state.context.add_event_callback(move |e| unsafe {
tokio::task::block_in_place(|| match e {
Event::Symbol { value, position } => {
let mut info = info.blocking_write();
let info = info
.entry(position.file.file.to_string_lossy().to_string())
.or_default();
info.symbol_ranges
.insert(position.range.clone(), std::mem::transmute(*value));
info.symbol_positions
.insert(std::mem::transmute(*value), position.clone());
}
Event::Definition { value, position } => {
let mut info = info.blocking_write();
let info = info
.entry(position.file.file.to_string_lossy().to_string())
.or_default();
info.symbol_definitions
.insert(std::mem::transmute(*value), position.clone());
info.symbol_ranges
.insert(position.range.clone(), std::mem::transmute(*value));
info.symbol_positions
.insert(std::mem::transmute(*value), position.clone());
}
Event::Diagnostic(diagnostic) => {
let mut diagnostics = diagnostics.blocking_lock();
diagnostics
.entry(diagnostic.position.file.file.clone())
.or_default()
.extend(make_diagnostics(diagnostic));
}
});
});
}
if let Err(err) = state.context.extend(
AssemblyIdentifier {
version: Version::default(),
name: Cow::Borrowed("Leaf lsp tmp"),
},
&files,
) {
let mut diagnostics = diagnostics.lock().await;
diagnostics
.entry(err.position.file.file.clone())
.or_default()
.extend(make_diagnostics(&err));
}
{
let mut diagnostics = diagnostics.lock().await;
for (file, diagnostics) in diagnostics.drain() {
client
.publish_diagnostics(Uri::from_file_path(file).unwrap(), diagnostics, None)
.await;
}
}
while let Some(event) = receiver.recv().await {
match event {
Request::Reload => {
alloc = SyncArenaAllocator::default();
continue 'global;
}
Request::Hover(params, result) => {
let value = state
.with_file_and_range(
&params.text_document_position_params,
|info, range| unsafe {
let Some(symbol) = info.symbol_ranges.get(
&(range.start
+ params
.text_document_position_params
.position
.character as usize),
) else {
return None;
};
// This is blasphemy but what can I do? :3
Some(std::mem::transmute::<_, AnyValue>(*symbol))
},
)
.await;
if let Some(Some(value)) = value {
let mut message = String::new();
let _ = writeln!(
message,
"Type: {}",
match value.is_lvalue() {
false => value.ty(),
true => match value.ty() {
Type::Ptr(PtrT { base, .. }) => *base,
_ => unreachable!(),
},
}
);
result
.0
.set(Hover {
contents: HoverContents::Scalar(MarkedString::String(message)),
range: None,
})
.unwrap();
}
result.1.notify_one();
}
Request::GoToDeclaration(params, result) => {
let declaration = state
.with_file_and_range(
&params.text_document_position_params,
|info, range| {
let Some(symbol) = info.symbol_ranges.get(
&(range.start
+ params
.text_document_position_params
.position
.character as usize),
) else {
return None;
};
let Some(position) = info.symbol_definitions.get(symbol) else {
return None;
};
Some(position.clone())
},
)
.await;
if let Some(Some(decl)) = declaration {
result
.0
.set(GotoDeclarationResponse::Scalar(Location {
uri: Uri::from_file_path(&decl.file.file).unwrap(),
range: decl.lsp_range(),
}))
.unwrap();
}
result.1.notify_one();
}
Request::SemanticTokens(params, result) => {
let info = state.files.read().await;
let Some(file) = info.get(params.text_document.uri.strip_header()) else {
result.1.notify_one();
continue;
};
let mut tokens = SemanticTokens::default();
for (symbol, position) in file.symbol_positions.iter() {
let symbol: AnyValue = unsafe { std::mem::transmute(*symbol) };
let line_col = position.line_col();
tokens.data.push(SemanticToken {
delta_line: line_col.start.line as u32,
delta_start: line_col.start.column as u32,
length: position.range.len() as u32,
token_type: match symbol {
AnyValue::Constant(AnyConst::Type(_)) => 0,
AnyValue::Constant(AnyConst::Function(_)) => 1,
AnyValue::Constant(AnyConst::Int(_) | AnyConst::Float(_)) => 2,
_ => continue,
},
token_modifiers_bitset: 0,
});
}
let mut previous_line = 0;
let mut previous_start = 0;
tokens.data.sort_by_key(|v| (v.delta_line, v.delta_start));
for SemanticToken {
delta_line,
delta_start,
..
} in tokens.data.iter_mut()
{
let line = *delta_line;
let start = *delta_start;
*delta_start = match line == previous_line {
false => start,
true => start - previous_start,
};
*delta_line = line - previous_line;
previous_line = line;
previous_start = start;
}
result.0.set(SemanticTokensResult::Tokens(tokens)).unwrap();
result.1.notify_one();
}
Request::Close(notify) => {
notify.notify_waiters();
break 'global;
}
}
}
}
});
Ok(Arc::new(Workspace { base, sender }))
}
fn calc_line_ranges(text: &str) -> HashMap<u32, Range<usize>> {
let mut map = HashMap::new();
for line in text.split('\n') {
let start = line.as_ptr() as usize - text.as_ptr() as usize;
map.insert(map.len() as u32, start..start + line.len());
}
map
}
impl State<'_> {
async fn with_file_and_range<T>(
&self,
params: &TextDocumentPositionParams,
action: impl FnOnce(&FileInfo, Range<usize>) -> T,
) -> Option<T> {
let info = self.files.read().await;
let Some(info) = info.get(params.text_document.uri.strip_header()) else {
return None;
};
let Some(range) = info.line_ranges.get(&params.position.line) else {
return None;
};
Some(action(info, range.clone()))
}
}
fn make_diagnostics(diag: &leaf_compiler::diagnostics::Diagnostic) -> Vec<Diagnostic> {
vec![Diagnostic {
range: diag.position.lsp_range(),
severity: Some(match diag.kind {
leaf_compiler::diagnostics::Kind::Info => DiagnosticSeverity::INFORMATION,
leaf_compiler::diagnostics::Kind::Warning => DiagnosticSeverity::WARNING,
leaf_compiler::diagnostics::Kind::Error => DiagnosticSeverity::ERROR,
}),
code: Some(NumberOrString::Number(diag.code as i32)),
code_description: None,
source: Some("Leaf compiler".into()),
message: diag.message.clone(),
related_information: None,
tags: None,
data: None,
}]
}