Experimenting with the LSP
This commit is contained in:
@@ -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
@@ -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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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;
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
¶ms.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(
|
||||
¶ms.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(¶ms.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,
|
||||
}]
|
||||
}
|
||||
Reference in New Issue
Block a user