mod code_actions;
mod completion;
mod document_symbols;
mod index_queries;
mod inlay_hints;
mod locals_nav;
mod position;
mod project;
mod publish;
mod signature_help;
mod structure;
mod symbols;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
use tower_lsp::jsonrpc::Result as JsonRpcResult;
use tower_lsp::lsp_types::request::{
GotoImplementationParams, GotoImplementationResponse, GotoTypeDefinitionParams,
GotoTypeDefinitionResponse,
};
use tower_lsp::lsp_types::*;
use tower_lsp::{Client, LanguageServer, LspService, Server};
use crate::project::ProjectConfig;
const SERVER_NAME: &str = "bynkc-lsp";
const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone)]
struct DocumentState {
text: String,
version: i32,
}
#[derive(Debug)]
struct Analysis {
src_root: PathBuf,
index: bynk_check::index::ProjectIndex,
snapshots: std::collections::HashMap<PathBuf, String>,
versions: std::collections::HashMap<PathBuf, i32>,
diagnostics: std::collections::HashMap<PathBuf, Vec<bynk_ide::Diagnostic>>,
hints: bynk_check::hints::FileHints,
requirements: bynk_check::requirements::FileRequirements,
locals: bynk_check::locals::FileLocals,
expr_types: bynk_check::expr_types::FileExprTypes,
unit_sources: std::collections::HashMap<String, Vec<PathBuf>>,
}
impl Analysis {
fn diag_categories(&self) -> Vec<(PathBuf, String)> {
self.diagnostics
.iter()
.flat_map(|(path, diags)| {
diags
.iter()
.map(|d| (path.clone(), d.error.category.to_string()))
})
.collect()
}
}
#[derive(Debug, Default)]
struct State {
project_root: Option<PathBuf>,
config: ProjectConfig,
docs: std::collections::HashMap<Url, DocumentState>,
published: std::collections::HashSet<Url>,
analysis_generation: u64,
analysis: Option<Arc<Analysis>>,
}
#[derive(Clone)]
struct Backend {
client: Client,
state: Arc<RwLock<State>>,
}
impl Backend {
fn new(client: Client) -> Self {
Self {
client,
state: Arc::new(RwLock::new(State::default())),
}
}
fn find_project_root(start: &std::path::Path) -> Option<PathBuf> {
let mut current = if start.is_file() {
start.parent()?.to_path_buf()
} else {
start.to_path_buf()
};
loop {
let candidate = current.join("bynk.toml");
if candidate.is_file() {
return Some(current);
}
current = current.parent()?.to_path_buf();
}
}
async fn recompile_and_publish(&self, uri: &Url) {
if self.state.read().await.project_root.is_some() {
self.schedule_project_diagnostics().await;
return;
}
let text = {
let state = self.state.read().await;
state.docs.get(uri).map(|d| d.text.clone())
};
let Some(text) = text else { return };
let diagnostics = bynk_ide::diagnose(&text);
let lsp_diags: Vec<Diagnostic> = diagnostics
.into_iter()
.map(|d| make_diagnostic(&d, &text, uri))
.collect();
let version = {
let state = self.state.read().await;
state.docs.get(uri).map(|d| d.version)
};
self.client
.publish_diagnostics(uri.clone(), lsp_diags, version)
.await;
}
async fn schedule_project_diagnostics(&self) {
let generation = {
let mut state = self.state.write().await;
state.analysis_generation += 1;
state.analysis_generation
};
let this = self.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
if this.state.read().await.analysis_generation != generation {
return;
}
this.run_project_diagnostics().await;
});
}
async fn run_project_diagnostics(&self) {
let (root, src_root, overlay, versions, previously_dirty) = {
let state = self.state.read().await;
let Some(root) = state.project_root.clone() else {
return;
};
let src_root = root.join(&state.config.src_dir);
let canonical_src_root = src_root.canonicalize().unwrap_or_else(|_| src_root.clone());
let mut overlay = std::collections::HashMap::new();
let mut versions = std::collections::HashMap::new();
for (uri, doc) in &state.docs {
if let Ok(p) = uri.to_file_path() {
let canonical = p.canonicalize().unwrap_or(p);
if let Ok(rel) = canonical.strip_prefix(&canonical_src_root) {
versions.insert(rel.to_path_buf(), doc.version);
}
overlay.insert(canonical, doc.text.clone());
}
}
(root, src_root, overlay, versions, state.published.clone())
};
let analysis_root = src_root.clone();
let Ok(result) = tokio::task::spawn_blocking(move || {
bynk_ide::diagnose_project(&analysis_root, &overlay)
})
.await
else {
return;
};
let mut new_by_uri: std::collections::HashMap<Url, Vec<Diagnostic>> =
std::collections::HashMap::new();
let mut snapshots = std::collections::HashMap::new();
let mut diagnostics: std::collections::HashMap<PathBuf, Vec<bynk_ide::Diagnostic>> =
std::collections::HashMap::new();
for file in &result.files {
let abs = src_root.join(&file.source_path);
let abs = abs.canonicalize().unwrap_or(abs);
let Ok(uri) = Url::from_file_path(&abs) else {
continue;
};
let diags: Vec<Diagnostic> = file
.diagnostics
.iter()
.map(|d| make_diagnostic(d, &file.text, &uri))
.collect();
new_by_uri.insert(uri, diags);
diagnostics.insert(file.source_path.clone(), file.diagnostics.clone());
snapshots.insert(file.source_path.clone(), file.text.clone());
}
{
let analysis = Arc::new(Analysis {
src_root: src_root.canonicalize().unwrap_or_else(|_| src_root.clone()),
index: result.index.clone(),
snapshots,
versions,
diagnostics,
hints: result.hints,
requirements: result.requirements,
locals: result.locals,
expr_types: result.expr_types,
unit_sources: result.unit_sources,
});
self.state.write().await.analysis = Some(analysis);
}
if !result.unattributed.is_empty()
&& let Ok(toml_uri) = Url::from_file_path(root.join("bynk.toml"))
{
let entry = new_by_uri.entry(toml_uri).or_default();
for d in &result.unattributed {
entry.push(Diagnostic {
range: Default::default(),
severity: Some(match d.severity {
bynk_syntax::Severity::Error => DiagnosticSeverity::ERROR,
bynk_syntax::Severity::Warning => DiagnosticSeverity::WARNING,
}),
code: Some(tower_lsp::lsp_types::NumberOrString::String(
d.error.category.to_string(),
)),
message: d.error.message.clone(),
..Default::default()
});
}
}
let (publishes, dirty) = publish::publish_plan(&previously_dirty, new_by_uri);
for (uri, diags) in publishes {
self.client.publish_diagnostics(uri, diags, None).await;
}
self.state.write().await.published = dirty;
}
async fn project_src_root(&self) -> Option<PathBuf> {
let state = self.state.read().await;
let root = state.project_root.as_ref()?;
Some(root.join(&state.config.src_dir))
}
fn local_sites(
&self,
analysis: &Analysis,
rel: &std::path::Path,
offset: usize,
) -> Option<Vec<bynk_syntax::span::Span>> {
let text = analysis.snapshots.get(rel)?;
let locals = analysis.locals.get(rel)?;
crate::locals_nav::local_sites_at(locals, text, offset)
}
async fn locals_completions(&self, uri: &Url, pos: Position) -> Vec<CompletionItem> {
let analysis = self.state.read().await.analysis.clone();
let Some(analysis) = analysis else {
return Vec::new();
};
let Some(rel) = Self::uri_to_rel(&analysis, uri) else {
return Vec::new();
};
let (Some(text), Some(locals)) = (analysis.snapshots.get(&rel), analysis.locals.get(&rel))
else {
return Vec::new();
};
let Some(offset) = crate::position::position_to_offset(text, pos) else {
return Vec::new();
};
bynk_check::locals::locals_at(locals, offset)
.into_iter()
.map(|b| CompletionItem {
label: b.name.clone(),
kind: Some(CompletionItemKind::VARIABLE),
detail: Some(b.ty.clone()),
..Default::default()
})
.collect()
}
fn local_locations(
&self,
analysis: &Analysis,
rel: &std::path::Path,
spans: &[bynk_syntax::span::Span],
) -> Vec<Location> {
let Some(text) = analysis.snapshots.get(rel) else {
return Vec::new();
};
let Ok(uri) = Url::from_file_path(analysis.src_root.join(rel)) else {
return Vec::new();
};
spans
.iter()
.map(|s| Location {
uri: uri.clone(),
range: crate::position::span_to_range(text, *s),
})
.collect()
}
async fn value_member_completions(
&self,
uri: &Url,
text: &str,
offset: usize,
) -> Vec<CompletionItem> {
let Some((rewritten, recv_offset)) = completion::value_receiver_rewrite(text, offset)
else {
return Vec::new();
};
let Some(ty) = self.type_receiver(uri, rewritten, recv_offset).await else {
return Vec::new();
};
let src_root = self.project_src_root().await;
completion::value_member_candidates(&ty, text, src_root.as_deref())
.into_iter()
.map(to_completion_item)
.collect()
}
async fn type_receiver(
&self,
uri: &Url,
rewritten: String,
recv_offset: usize,
) -> Option<bynk_check::checker::Ty> {
let src_root = self.project_src_root().await?;
let canonical_src_root = src_root.canonicalize().unwrap_or_else(|_| src_root.clone());
let cur = uri.to_file_path().ok()?;
let cur = cur.canonicalize().unwrap_or(cur);
let rel = cur.strip_prefix(&canonical_src_root).ok()?.to_path_buf();
let overlay = {
let state = self.state.read().await;
let mut ov = std::collections::HashMap::new();
for (u, doc) in &state.docs {
if let Ok(p) = u.to_file_path() {
let canonical = p.canonicalize().unwrap_or(p);
let t = if u == uri {
rewritten.clone()
} else {
doc.text.clone()
};
ov.insert(canonical, t);
}
}
ov
};
let result =
tokio::task::spawn_blocking(move || bynk_ide::diagnose_project(&src_root, &overlay))
.await
.ok()?;
let (_, entries) = result.expr_types.iter().find(|(p, _)| **p == rel)?;
bynk_check::expr_types::type_at_offset(entries, recv_offset).cloned()
}
async fn ensure_analysis(&self) -> Option<Arc<Analysis>> {
if let Some(a) = self.state.read().await.analysis.clone() {
return Some(a);
}
self.run_project_diagnostics().await;
self.state.read().await.analysis.clone()
}
async fn fresh_analysis(&self) -> Option<Arc<Analysis>> {
self.run_project_diagnostics().await;
self.state.read().await.analysis.clone()
}
fn uri_to_rel(analysis: &Analysis, uri: &Url) -> Option<PathBuf> {
let p = uri.to_file_path().ok()?;
let canonical = p.canonicalize().unwrap_or(p);
canonical
.strip_prefix(&analysis.src_root)
.ok()
.map(|r| r.to_path_buf())
}
async fn unit_reference_definition(&self, uri: &Url, pos: Position) -> Option<Location> {
let (text, analysis) = {
let s = self.state.read().await;
(s.docs.get(uri).map(|d| d.text.clone()), s.analysis.clone())
};
let (text, analysis) = (text?, analysis?);
let offset = cursor_byte_offset(&text, pos);
for (unit, span) in crate::symbols::unit_reference_spans(&text) {
if span.start <= offset && offset <= span.end {
let rel = analysis.unit_sources.get(&unit)?.first()?;
let target = Url::from_file_path(analysis.src_root.join(rel)).ok()?;
return Some(Location {
uri: target,
range: Range::default(),
});
}
}
None
}
fn site_to_location(
analysis: &Analysis,
site: &bynk_check::index::SiteRef,
) -> Option<Location> {
let text = analysis.snapshots.get(&site.path)?;
let abs = analysis.src_root.join(&site.path);
let uri = Url::from_file_path(abs).ok()?;
Some(Location {
uri,
range: crate::position::span_to_range(text, site.span),
})
}
fn call_hierarchy_item(
analysis: &Analysis,
key: &bynk_check::index::SymbolKey,
def: &bynk_check::index::SiteRef,
) -> Option<CallHierarchyItem> {
let location = Self::site_to_location(analysis, def)?;
Some(CallHierarchyItem {
name: key.name.clone(),
kind: lsp_symbol_kind(key.kind),
tags: None,
detail: Some(key.unit.clone()),
uri: location.uri,
range: location.range,
selection_range: location.range,
data: serde_json::to_value(SerKey::from(key)).ok(),
})
}
fn call_ranges(analysis: &Analysis, sites: &[&bynk_check::index::SiteRef]) -> Vec<Range> {
sites
.iter()
.filter_map(|s| {
let text = analysis.snapshots.get(&s.path)?;
Some(crate::position::span_to_range(text, s.span))
})
.collect()
}
async fn semantic_tokens_for(&self, uri: &Url, range: Option<Range>) -> Vec<SemanticToken> {
let analysis = { self.state.read().await.analysis.clone() };
let Some(analysis) = analysis else {
return Vec::new();
};
let Some(rel) = Self::uri_to_rel(&analysis, uri) else {
return Vec::new();
};
let Some(text) = analysis.snapshots.get(&rel) else {
return Vec::new();
};
let span = match range {
None => None,
Some(r) => {
let (Some(start), Some(end)) = (
crate::position::position_to_offset(text, r.start),
crate::position::position_to_offset(text, r.end),
) else {
return Vec::new();
};
Some(bynk_syntax::span::Span::new(start, end))
}
};
let lt = analysis
.locals
.get(&rel)
.map(|l| crate::locals_nav::local_token_sites(l, text))
.unwrap_or_default();
crate::index_queries::semantic_tokens(&analysis.index, <, &rel, text, span)
}
async fn index_position(
&self,
uri: &Url,
position: Position,
fresh: bool,
) -> Option<(Arc<Analysis>, PathBuf, usize)> {
let analysis = if fresh {
self.fresh_analysis().await?
} else {
self.ensure_analysis().await?
};
let rel = Self::uri_to_rel(&analysis, uri)?;
let text = analysis.snapshots.get(&rel)?;
let offset = crate::position::position_to_offset(text, position)?;
Some((analysis, rel, offset))
}
async fn identifier_at(
&self,
uri: &Url,
position: Position,
) -> Option<(String, bynk_syntax::span::Span, String)> {
let text = {
let state = self.state.read().await;
state.docs.get(uri)?.text.clone()
};
let offset = crate::position::position_to_offset(&text, position)?;
let tokens = bynk_syntax::lexer::tokenize(&text).ok()?;
for t in &tokens {
if t.span.start <= offset
&& offset < t.span.end
&& matches!(
t.kind,
bynk_syntax::lexer::TokenKind::Ident
| bynk_syntax::lexer::TokenKind::Int
| bynk_syntax::lexer::TokenKind::String
| bynk_syntax::lexer::TokenKind::Bool
| bynk_syntax::lexer::TokenKind::Float
| bynk_syntax::lexer::TokenKind::Result
| bynk_syntax::lexer::TokenKind::Option
| bynk_syntax::lexer::TokenKind::Effect
)
{
let name = text[t.span.start..t.span.end].to_string();
return Some((name, t.span, text));
}
}
None
}
}
#[tower_lsp::async_trait]
impl LanguageServer for Backend {
async fn initialize(&self, params: InitializeParams) -> JsonRpcResult<InitializeResult> {
if let Some(folders) = ¶ms.workspace_folders
&& let Some(first) = folders.first()
&& let Ok(path) = first.uri.to_file_path()
{
let mut state = self.state.write().await;
if let Some(root) = Self::find_project_root(&path) {
state.config = project::load_config(&root).unwrap_or_default();
state.project_root = Some(root);
}
}
Ok(InitializeResult {
capabilities: server_capabilities(),
server_info: Some(ServerInfo {
name: SERVER_NAME.into(),
version: Some(SERVER_VERSION.into()),
}),
})
}
async fn initialized(&self, _: InitializedParams) {
let root = { self.state.read().await.project_root.clone() };
match root {
Some(root) => {
self.client
.log_message(
MessageType::INFO,
format!("bynkc-lsp: project root at {}", root.display()),
)
.await;
}
None => {
self.client
.log_message(
MessageType::INFO,
"bynkc-lsp: no bynk.toml found; single-file mode",
)
.await;
}
}
}
async fn shutdown(&self) -> JsonRpcResult<()> {
Ok(())
}
async fn did_open(&self, params: DidOpenTextDocumentParams) {
let uri = params.text_document.uri.clone();
{
let mut state = self.state.write().await;
if state.project_root.is_none()
&& let Ok(path) = uri.to_file_path()
&& let Some(root) = Self::find_project_root(&path)
{
state.config = project::load_config(&root).unwrap_or_default();
state.project_root = Some(root);
}
state.docs.insert(
uri.clone(),
DocumentState {
text: params.text_document.text,
version: params.text_document.version,
},
);
}
self.recompile_and_publish(&uri).await;
}
async fn did_change(&self, params: DidChangeTextDocumentParams) {
let uri = params.text_document.uri.clone();
{
let mut state = self.state.write().await;
if let Some(doc) = state.docs.get_mut(&uri)
&& let Some(change) = params.content_changes.into_iter().next_back()
{
doc.text = change.text;
doc.version = params.text_document.version;
}
}
let debounce_ms = {
let s = self.state.read().await;
s.config.diagnostics_debounce_ms
};
let backend = self.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(debounce_ms)).await;
backend.recompile_and_publish(&uri).await;
});
}
async fn did_close(&self, params: DidCloseTextDocumentParams) {
let uri = params.text_document.uri;
let mut state = self.state.write().await;
state.docs.remove(&uri);
}
async fn hover(&self, params: HoverParams) -> JsonRpcResult<Option<Hover>> {
let uri = params.text_document_position_params.text_document.uri;
let pos = params.text_document_position_params.position;
if let Some((analysis, rel, offset)) = self.index_position(&uri, pos, false).await
&& let Some((key, def)) =
crate::index_queries::definition_at(&analysis.index, &rel, offset)
&& let Some(def_text) = analysis.snapshots.get(&def.path)
&& let Some(content) = crate::symbols::describe_symbol(def_text, &key.name)
{
return Ok(Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: content,
}),
range: None,
}));
}
let Some((name, _span, text)) = self.identifier_at(&uri, pos).await else {
return Ok(None);
};
let content = match crate::symbols::describe_symbol(&text, &name) {
Some(local) => local,
None => {
let src_root = self.project_src_root().await;
match src_root
.and_then(|root| crate::symbols::describe_symbol_cross_file(&root, &uri, &name))
.map(|(_other_uri, desc)| desc)
.or_else(|| crate::symbols::describe_firstparty_symbol(&name))
{
Some(desc) => desc,
None => return Ok(None),
}
}
};
Ok(Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: content,
}),
range: None,
}))
}
async fn signature_help(
&self,
params: SignatureHelpParams,
) -> JsonRpcResult<Option<SignatureHelp>> {
let uri = params.text_document_position_params.text_document.uri;
let pos = params.text_document_position_params.position;
let text = {
let s = self.state.read().await;
s.docs.get(&uri).map(|d| d.text.clone())
};
let Some(text) = text else { return Ok(None) };
let offset = cursor_byte_offset(&text, pos);
let Some(ctx) = crate::signature_help::call_context(&text, offset) else {
return Ok(None);
};
let src_root = self.project_src_root().await;
let label =
match crate::signature_help::resolve_label(&ctx.callee, &text, src_root.as_deref()) {
Some(l) => Some(l),
None => match crate::signature_help::value_receiver_method(&ctx.callee) {
Some((_, method)) => {
if let Some((rewritten, recv_offset)) =
crate::signature_help::value_receiver_rewrite(
&text,
&ctx.callee,
ctx.open_paren,
offset,
)
&& let Some(ty) = self.type_receiver(&uri, rewritten, recv_offset).await
{
crate::signature_help::kernel_method_signature(&ty, method)
} else {
None
}
}
None => None,
},
};
let Some(label) = label else { return Ok(None) };
let active = ctx.active_param as u32;
let parameters: Vec<ParameterInformation> = crate::signature_help::param_ranges(&label)
.into_iter()
.map(|(s, e)| ParameterInformation {
label: ParameterLabel::LabelOffsets([s as u32, e as u32]),
documentation: None,
})
.collect();
Ok(Some(SignatureHelp {
signatures: vec![SignatureInformation {
label,
documentation: None,
parameters: Some(parameters),
active_parameter: Some(active),
}],
active_signature: Some(0),
active_parameter: Some(active),
}))
}
async fn code_lens(&self, params: CodeLensParams) -> JsonRpcResult<Option<Vec<CodeLens>>> {
let uri = params.text_document.uri;
let analysis = { self.state.read().await.analysis.clone() };
let Some(analysis) = analysis else {
return Ok(Some(Vec::new()));
};
let Some(rel) = Self::uri_to_rel(&analysis, &uri) else {
return Ok(Some(Vec::new()));
};
let Some(text) = analysis.snapshots.get(&rel) else {
return Ok(Some(Vec::new()));
};
let lenses: Vec<CodeLens> = crate::index_queries::code_lenses(&analysis.index, &rel)
.into_iter()
.map(|(def, refs)| {
let range = crate::position::span_to_range(text, def.span);
let locations: Vec<Location> = refs
.iter()
.filter_map(|r| Self::site_to_location(&analysis, r))
.collect();
let n = refs.len();
CodeLens {
range,
command: Some(Command {
title: format!("{n} reference{}", if n == 1 { "" } else { "s" }),
command: "editor.action.showReferences".to_string(),
arguments: Some(vec![
serde_json::to_value(&uri).unwrap_or_default(),
serde_json::to_value(range.start).unwrap_or_default(),
serde_json::to_value(&locations).unwrap_or_default(),
]),
}),
data: None,
}
})
.collect();
Ok(Some(lenses))
}
async fn prepare_call_hierarchy(
&self,
params: CallHierarchyPrepareParams,
) -> JsonRpcResult<Option<Vec<CallHierarchyItem>>> {
let uri = params.text_document_position_params.text_document.uri;
let pos = params.text_document_position_params.position;
let Some((analysis, rel, offset)) = self.index_position(&uri, pos, false).await else {
return Ok(None);
};
let Some((key, def)) =
crate::index_queries::prepare_call_hierarchy(&analysis.index, &rel, offset)
else {
return Ok(None);
};
Ok(Self::call_hierarchy_item(&analysis, key, def).map(|item| vec![item]))
}
async fn incoming_calls(
&self,
params: CallHierarchyIncomingCallsParams,
) -> JsonRpcResult<Option<Vec<CallHierarchyIncomingCall>>> {
let analysis = { self.state.read().await.analysis.clone() };
let Some(analysis) = analysis else {
return Ok(Some(Vec::new()));
};
let Some(key) = SerKey::read(¶ms.item.data) else {
return Ok(Some(Vec::new()));
};
let calls = crate::index_queries::incoming_calls(&analysis.index, &key)
.into_iter()
.filter_map(|rel| {
let from = Self::call_hierarchy_item(&analysis, rel.key, rel.def)?;
let from_ranges = Self::call_ranges(&analysis, &rel.sites);
Some(CallHierarchyIncomingCall { from, from_ranges })
})
.collect();
Ok(Some(calls))
}
async fn outgoing_calls(
&self,
params: CallHierarchyOutgoingCallsParams,
) -> JsonRpcResult<Option<Vec<CallHierarchyOutgoingCall>>> {
let analysis = { self.state.read().await.analysis.clone() };
let Some(analysis) = analysis else {
return Ok(Some(Vec::new()));
};
let Some(key) = SerKey::read(¶ms.item.data) else {
return Ok(Some(Vec::new()));
};
let calls = crate::index_queries::outgoing_calls(&analysis.index, &key)
.into_iter()
.filter_map(|rel| {
let to = Self::call_hierarchy_item(&analysis, rel.key, rel.def)?;
let from_ranges = Self::call_ranges(&analysis, &rel.sites);
Some(CallHierarchyOutgoingCall { to, from_ranges })
})
.collect();
Ok(Some(calls))
}
async fn goto_implementation(
&self,
params: GotoImplementationParams,
) -> JsonRpcResult<Option<GotoImplementationResponse>> {
let uri = params.text_document_position_params.text_document.uri;
let pos = params.text_document_position_params.position;
let Some((analysis, rel, offset)) = self.index_position(&uri, pos, false).await else {
return Ok(None);
};
let Some((key, _)) = analysis.index.symbol_at(&rel, offset) else {
return Ok(None);
};
if key.kind != bynk_check::index::SymbolKind::Capability {
return Ok(None);
}
let locations: Vec<Location> = crate::index_queries::implementations(&analysis.index, key)
.into_iter()
.filter_map(|d| Self::site_to_location(&analysis, d))
.collect();
if locations.is_empty() {
return Ok(None);
}
Ok(Some(GotoDefinitionResponse::Array(locations)))
}
async fn goto_type_definition(
&self,
params: GotoTypeDefinitionParams,
) -> JsonRpcResult<Option<GotoTypeDefinitionResponse>> {
let uri = params.text_document_position_params.text_document.uri;
let pos = params.text_document_position_params.position;
let Some((analysis, rel, offset)) = self.index_position(&uri, pos, false).await else {
return Ok(None);
};
let Some(entries) = analysis.expr_types.get(&rel) else {
return Ok(None);
};
let Some(ty) = bynk_check::expr_types::type_at_offset(entries, offset) else {
return Ok(None);
};
let Some(name) = crate::index_queries::named_type_target(ty) else {
return Ok(None);
};
let locations: Vec<Location> =
crate::index_queries::type_definitions_named(&analysis.index, name)
.into_iter()
.filter_map(|d| Self::site_to_location(&analysis, d))
.collect();
if locations.is_empty() {
return Ok(None);
}
Ok(Some(GotoDefinitionResponse::Array(locations)))
}
async fn document_link(
&self,
params: DocumentLinkParams,
) -> JsonRpcResult<Option<Vec<DocumentLink>>> {
let uri = params.text_document.uri;
let (text, analysis) = {
let s = self.state.read().await;
(s.docs.get(&uri).map(|d| d.text.clone()), s.analysis.clone())
};
let (Some(text), Some(analysis)) = (text, analysis) else {
return Ok(None);
};
let links: Vec<DocumentLink> = crate::symbols::unit_reference_spans(&text)
.into_iter()
.filter_map(|(unit, span)| {
let rel = analysis.unit_sources.get(&unit)?.first()?;
let target = Url::from_file_path(analysis.src_root.join(rel)).ok()?;
Some(DocumentLink {
range: crate::position::span_to_range(&text, span),
target: Some(target),
tooltip: Some(format!("Open unit `{unit}`")),
data: None,
})
})
.collect();
Ok((!links.is_empty()).then_some(links))
}
async fn completion(
&self,
params: CompletionParams,
) -> JsonRpcResult<Option<CompletionResponse>> {
let uri = params.text_document_position.text_document.uri;
let pos = params.text_document_position.position;
let text = {
let s = self.state.read().await;
s.docs.get(&uri).map(|d| d.text.clone())
};
let Some(text) = text else { return Ok(None) };
let line_prefix = text
.lines()
.nth(pos.line as usize)
.map(|l| {
let end = (pos.character as usize).min(l.len());
l.get(..end).unwrap_or(l)
})
.unwrap_or("")
.to_string();
let src_root = self.project_src_root().await;
let candidates = completion::complete(&line_prefix, &text, src_root.as_deref());
let mut items: Vec<CompletionItem> =
candidates.into_iter().map(to_completion_item).collect();
if completion::is_keyword_position(&line_prefix)
|| completion::is_expression_position(&line_prefix)
{
items.extend(self.locals_completions(&uri, pos).await);
}
if items.is_empty() {
let offset = cursor_byte_offset(&text, pos);
let value_items = self.value_member_completions(&uri, &text, offset).await;
return Ok((!value_items.is_empty()).then_some(CompletionResponse::Array(value_items)));
}
stamp_resolve_data(&mut items, &uri);
Ok(Some(CompletionResponse::Array(items)))
}
async fn completion_resolve(&self, mut item: CompletionItem) -> JsonRpcResult<CompletionItem> {
if item.documentation.is_some() {
return Ok(item);
}
let Some(uri) = item
.data
.as_ref()
.and_then(|d| d.get("uri"))
.and_then(serde_json::Value::as_str)
.and_then(|s| Url::parse(s).ok())
else {
return Ok(item);
};
let local = {
let s = self.state.read().await;
s.docs.get(&uri).map(|d| d.text.clone())
};
let doc = match local
.as_deref()
.and_then(|t| crate::symbols::describe_symbol(t, &item.label))
{
Some(md) => Some(md),
None => self
.project_src_root()
.await
.and_then(|root| {
crate::symbols::describe_symbol_cross_file(&root, &uri, &item.label)
})
.map(|(_uri, md)| md)
.or_else(|| crate::symbols::describe_firstparty_symbol(&item.label)),
};
if let Some(md) = doc {
item.documentation = Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: md,
}));
}
Ok(item)
}
async fn goto_definition(
&self,
params: GotoDefinitionParams,
) -> JsonRpcResult<Option<GotoDefinitionResponse>> {
let uri = params
.text_document_position_params
.text_document
.uri
.clone();
let pos = params.text_document_position_params.position;
if let Some((analysis, rel, offset)) = self.index_position(&uri, pos, false).await {
if let Some((_, def)) =
crate::index_queries::definition_at(&analysis.index, &rel, offset)
&& let Some(location) = Self::site_to_location(&analysis, def)
{
return Ok(Some(GotoDefinitionResponse::Scalar(location)));
}
if let Some(text) = analysis.snapshots.get(&rel)
&& let Some(locals) = analysis.locals.get(&rel)
&& let Some(def) = crate::locals_nav::local_definition_at(locals, text, offset)
&& let Some(location) = self
.local_locations(&analysis, &rel, &[def])
.into_iter()
.next()
{
return Ok(Some(GotoDefinitionResponse::Scalar(location)));
}
}
if let Some(location) = self.unit_reference_definition(&uri, pos).await {
return Ok(Some(GotoDefinitionResponse::Scalar(location)));
}
let Some((name, _span, text)) = self.identifier_at(&uri, pos).await else {
return Ok(None);
};
if let Some(decl_span) = crate::symbols::find_declaration_span(&text, &name) {
let range = crate::position::span_to_range(&text, decl_span);
return Ok(Some(GotoDefinitionResponse::Scalar(Location {
uri,
range,
})));
}
if let Some(root) = self.project_src_root().await
&& let Some(found) = crate::symbols::find_declaration_cross_file(&root, &uri, &name)
{
let range = crate::position::span_to_range(&found.source, found.span);
return Ok(Some(GotoDefinitionResponse::Scalar(Location {
uri: found.uri,
range,
})));
}
Ok(None)
}
async fn formatting(
&self,
params: DocumentFormattingParams,
) -> JsonRpcResult<Option<Vec<TextEdit>>> {
let uri = params.text_document.uri;
let text = {
let s = self.state.read().await;
s.docs.get(&uri).map(|d| d.text.clone())
};
let Some(text) = text else { return Ok(None) };
let opts = {
let s = self.state.read().await;
s.config.format_options()
};
match bynk_fmt::format_source(&text, &opts) {
Ok(formatted) => {
if formatted == text {
Ok(Some(Vec::new()))
} else {
let end_pos = crate::position::end_position(&text);
Ok(Some(vec![TextEdit {
range: Range {
start: Position::new(0, 0),
end: end_pos,
},
new_text: formatted,
}]))
}
}
Err(_) => {
Ok(Some(Vec::new()))
}
}
}
async fn range_formatting(
&self,
params: DocumentRangeFormattingParams,
) -> JsonRpcResult<Option<Vec<TextEdit>>> {
self.formatting(DocumentFormattingParams {
text_document: params.text_document,
options: params.options,
work_done_progress_params: params.work_done_progress_params,
})
.await
}
async fn document_symbol(
&self,
params: DocumentSymbolParams,
) -> JsonRpcResult<Option<DocumentSymbolResponse>> {
let uri = params.text_document.uri;
let text = {
let s = self.state.read().await;
s.docs.get(&uri).map(|d| d.text.clone())
};
let Some(text) = text else { return Ok(None) };
let syms = crate::document_symbols::outline(&text);
if syms.is_empty() {
return Ok(None);
}
Ok(Some(DocumentSymbolResponse::Nested(syms)))
}
async fn folding_range(
&self,
params: FoldingRangeParams,
) -> JsonRpcResult<Option<Vec<FoldingRange>>> {
let uri = params.text_document.uri;
let text = {
let s = self.state.read().await;
s.docs.get(&uri).map(|d| d.text.clone())
};
let Some(text) = text else { return Ok(None) };
Ok(Some(crate::structure::folding_ranges(&text)))
}
async fn selection_range(
&self,
params: SelectionRangeParams,
) -> JsonRpcResult<Option<Vec<SelectionRange>>> {
let uri = params.text_document.uri;
let text = {
let s = self.state.read().await;
s.docs.get(&uri).map(|d| d.text.clone())
};
let Some(text) = text else { return Ok(None) };
Ok(Some(crate::structure::selection_ranges(
&text,
¶ms.positions,
)))
}
async fn references(&self, params: ReferenceParams) -> JsonRpcResult<Option<Vec<Location>>> {
let uri = params.text_document_position.text_document.uri;
let pos = params.text_document_position.position;
let Some((analysis, rel, offset)) = self.index_position(&uri, pos, false).await else {
return Ok(None);
};
let include_decl = params.context.include_declaration;
if let Some(sites) =
crate::index_queries::sites_for(&analysis.index, &rel, offset, include_decl)
{
let locations: Vec<Location> = sites
.into_iter()
.filter_map(|site| Self::site_to_location(&analysis, site))
.collect();
return Ok(Some(locations));
}
if let Some(spans) = self.local_sites(&analysis, &rel, offset) {
let spans = if include_decl {
&spans[..]
} else {
&spans[1..]
}; let locations = self.local_locations(&analysis, &rel, spans);
return Ok(Some(locations));
}
Ok(None)
}
async fn code_action(
&self,
params: CodeActionParams,
) -> JsonRpcResult<Option<CodeActionResponse>> {
let uri = params.text_document.uri;
let analysis = { self.state.read().await.analysis.clone() };
let Some(analysis) = analysis else {
return Ok(Some(Vec::new()));
};
let Some(rel) = Self::uri_to_rel(&analysis, &uri) else {
return Ok(Some(Vec::new()));
};
let (Some(text), Some(diags)) =
(analysis.snapshots.get(&rel), analysis.diagnostics.get(&rel))
else {
return Ok(Some(Vec::new()));
};
let (Some(start), Some(end)) = (
crate::position::position_to_offset(text, params.range.start),
crate::position::position_to_offset(text, params.range.end),
) else {
return Ok(Some(Vec::new()));
};
let actions = crate::code_actions::quick_fixes(
text,
diags,
bynk_syntax::span::Span::new(start, end),
&uri,
analysis.versions.get(&rel).copied(),
);
Ok(Some(actions))
}
async fn inlay_hint(&self, params: InlayHintParams) -> JsonRpcResult<Option<Vec<InlayHint>>> {
let uri = params.text_document.uri;
let analysis = { self.state.read().await.analysis.clone() };
let Some(analysis) = analysis else {
return Ok(Some(Vec::new()));
};
let Some(rel) = Self::uri_to_rel(&analysis, &uri) else {
return Ok(Some(Vec::new()));
};
let Some(text) = analysis.snapshots.get(&rel) else {
return Ok(Some(Vec::new()));
};
let (Some(start), Some(end)) = (
crate::position::position_to_offset(text, params.range.start),
crate::position::position_to_offset(text, params.range.end),
) else {
return Ok(Some(Vec::new()));
};
let visible = bynk_syntax::span::Span::new(start, end);
let mut hints = analysis
.hints
.get(&rel)
.map(|h| crate::inlay_hints::inlay_hints(text, h, visible))
.unwrap_or_default();
if let Some(reqs) = analysis.requirements.get(&rel) {
hints.extend(crate::inlay_hints::given_hints(text, reqs, visible));
}
Ok(Some(hints))
}
async fn semantic_tokens_full(
&self,
params: SemanticTokensParams,
) -> JsonRpcResult<Option<SemanticTokensResult>> {
let data = self
.semantic_tokens_for(¶ms.text_document.uri, None)
.await;
Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
result_id: None,
data,
})))
}
async fn semantic_tokens_range(
&self,
params: SemanticTokensRangeParams,
) -> JsonRpcResult<Option<SemanticTokensRangeResult>> {
let data = self
.semantic_tokens_for(¶ms.text_document.uri, Some(params.range))
.await;
Ok(Some(SemanticTokensRangeResult::Tokens(SemanticTokens {
result_id: None,
data,
})))
}
async fn symbol(
&self,
params: WorkspaceSymbolParams,
) -> JsonRpcResult<Option<Vec<SymbolInformation>>> {
let Some(analysis) = self.ensure_analysis().await else {
return Ok(None);
};
let matches = crate::index_queries::workspace_symbols(&analysis.index, ¶ms.query);
let symbols: Vec<SymbolInformation> = matches
.into_iter()
.filter_map(|(key, def)| {
let location = Self::site_to_location(&analysis, def)?;
#[allow(deprecated)]
Some(SymbolInformation {
name: key.name.clone(),
kind: lsp_symbol_kind(key.kind),
tags: None,
deprecated: None,
location,
container_name: Some(key.unit.clone()),
})
})
.collect();
Ok(Some(symbols))
}
async fn document_highlight(
&self,
params: DocumentHighlightParams,
) -> JsonRpcResult<Option<Vec<DocumentHighlight>>> {
let uri = params.text_document_position_params.text_document.uri;
let pos = params.text_document_position_params.position;
let Some((analysis, rel, offset)) = self.index_position(&uri, pos, false).await else {
return Ok(None);
};
let Some(text) = analysis.snapshots.get(&rel) else {
return Ok(None);
};
if let Some(sites) =
crate::index_queries::document_highlights(&analysis.index, &rel, offset)
{
let highlights: Vec<DocumentHighlight> = sites
.into_iter()
.map(|s| DocumentHighlight {
range: crate::position::span_to_range(text, s.span),
kind: None,
})
.collect();
return Ok(Some(highlights));
}
if let Some(spans) = self.local_sites(&analysis, &rel, offset) {
let highlights = spans
.iter()
.map(|s| DocumentHighlight {
range: crate::position::span_to_range(text, *s),
kind: None,
})
.collect();
return Ok(Some(highlights));
}
Ok(None)
}
async fn prepare_rename(
&self,
params: TextDocumentPositionParams,
) -> JsonRpcResult<Option<PrepareRenameResponse>> {
let uri = params.text_document.uri;
let pos = params.position;
let Some((analysis, rel, offset)) = self.index_position(&uri, pos, false).await else {
return Ok(None);
};
let Some((key, site)) = crate::index_queries::prepare_rename(&analysis.index, &rel, offset)
else {
return Ok(None);
};
let Some(text) = analysis.snapshots.get(&rel) else {
return Ok(None);
};
Ok(Some(PrepareRenameResponse::RangeWithPlaceholder {
range: crate::position::span_to_range(text, site.span),
placeholder: key.name.clone(),
}))
}
async fn rename(&self, params: RenameParams) -> JsonRpcResult<Option<WorkspaceEdit>> {
let uri = params.text_document_position.text_document.uri;
let pos = params.text_document_position.position;
let new_name = params.new_name;
let refused = |msg: String| tower_lsp::jsonrpc::Error {
code: tower_lsp::jsonrpc::ErrorCode::InvalidParams,
message: msg.into(),
data: None,
};
let Some((analysis, rel, offset)) = self.index_position(&uri, pos, true).await else {
return Err(refused("rename requires a project (bynk.toml)".into()));
};
let plan = crate::index_queries::plan_rename(&analysis.index, &rel, offset, &new_name)
.map_err(refused)?;
let mut overlay = std::collections::HashMap::new();
for (rel_path, text) in &analysis.snapshots {
let edited = match plan.edits.get(rel_path) {
Some(spans) => crate::index_queries::apply_edits(text, spans, &plan.new_name),
None => text.clone(),
};
let abs = analysis.src_root.join(rel_path);
let abs = abs.canonicalize().unwrap_or(abs);
overlay.insert(abs, edited);
}
let analysis_root = analysis.src_root.clone();
let Ok(post) = tokio::task::spawn_blocking(move || {
bynk_ide::diagnose_project(&analysis_root, &overlay)
})
.await
else {
return Err(refused("rename validation failed to run".into()));
};
let post_diags: Vec<(PathBuf, String)> = post
.files
.iter()
.flat_map(|f| {
f.diagnostics
.iter()
.map(|d| (f.source_path.clone(), d.error.category.to_string()))
})
.collect();
crate::index_queries::no_new_diagnostics(&analysis.diag_categories(), &post_diags)
.map_err(refused)?;
if !crate::index_queries::index_unchanged_modulo_rename(&analysis.index, &post.index, &plan)
{
return Err(refused(format!(
"renaming `{}` to `{new_name}` would silently re-bind another name — refused",
plan.key.name
)));
}
let mut document_edits: Vec<TextDocumentEdit> = Vec::new();
for (rel_path, spans) in &plan.edits {
let Some(text) = analysis.snapshots.get(rel_path) else {
continue;
};
let abs = analysis.src_root.join(rel_path);
let Ok(file_uri) = Url::from_file_path(&abs) else {
continue;
};
let edits: Vec<OneOf<TextEdit, AnnotatedTextEdit>> = spans
.iter()
.map(|span| {
OneOf::Left(TextEdit {
range: crate::position::span_to_range(text, *span),
new_text: plan.new_name.clone(),
})
})
.collect();
document_edits.push(TextDocumentEdit {
text_document: OptionalVersionedTextDocumentIdentifier {
uri: file_uri,
version: analysis.versions.get(rel_path).copied(),
},
edits,
});
}
Ok(Some(WorkspaceEdit {
changes: None,
document_changes: Some(DocumentChanges::Edits(document_edits)),
change_annotations: None,
}))
}
async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
let mut uris_to_refresh = Vec::new();
{
let state = self.state.read().await;
for ev in ¶ms.changes {
if state.docs.contains_key(&ev.uri) {
uris_to_refresh.push(ev.uri.clone());
}
}
}
for uri in uris_to_refresh {
self.recompile_and_publish(&uri).await;
}
}
}
fn server_capabilities() -> ServerCapabilities {
ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
definition_provider: Some(OneOf::Left(true)),
completion_provider: Some(CompletionOptions {
trigger_characters: Some(vec![
" ".to_string(),
"{".to_string(),
",".to_string(),
".".to_string(),
]),
resolve_provider: Some(true),
..Default::default()
}),
signature_help_provider: Some(SignatureHelpOptions {
trigger_characters: Some(vec!["(".to_string(), ",".to_string()]),
retrigger_characters: Some(vec![",".to_string()]),
..Default::default()
}),
code_lens_provider: Some(CodeLensOptions {
resolve_provider: Some(false),
}),
call_hierarchy_provider: Some(CallHierarchyServerCapability::Simple(true)),
implementation_provider: Some(ImplementationProviderCapability::Simple(true)),
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
document_link_provider: Some(DocumentLinkOptions {
resolve_provider: Some(false),
work_done_progress_options: Default::default(),
}),
document_formatting_provider: Some(OneOf::Left(true)),
document_range_formatting_provider: Some(OneOf::Left(true)),
document_symbol_provider: Some(OneOf::Left(true)),
folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)),
references_provider: Some(OneOf::Left(true)),
rename_provider: Some(OneOf::Right(RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: Default::default(),
})),
code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),
..Default::default()
})),
inlay_hint_provider: Some(OneOf::Left(true)),
semantic_tokens_provider: Some(SemanticTokensServerCapabilities::SemanticTokensOptions(
SemanticTokensOptions {
legend: crate::index_queries::semantic_tokens_legend(),
full: Some(SemanticTokensFullOptions::Bool(true)),
range: Some(true),
..Default::default()
},
)),
workspace_symbol_provider: Some(OneOf::Left(true)),
document_highlight_provider: Some(OneOf::Left(true)),
workspace: Some(WorkspaceServerCapabilities {
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
supported: Some(true),
change_notifications: Some(OneOf::Left(true)),
}),
file_operations: None,
}),
..Default::default()
}
}
fn stamp_resolve_data(items: &mut [CompletionItem], uri: &Url) {
let data = serde_json::json!({ "uri": uri.to_string() });
for item in items.iter_mut() {
item.data = Some(data.clone());
}
}
fn to_completion_item(c: completion::Completion) -> CompletionItem {
CompletionItem {
kind: Some(match c.kind {
completion::CompletionKind::Unit => CompletionItemKind::MODULE,
completion::CompletionKind::Capability => CompletionItemKind::INTERFACE,
completion::CompletionKind::Type => CompletionItemKind::STRUCT,
completion::CompletionKind::Keyword => CompletionItemKind::KEYWORD,
completion::CompletionKind::Snippet => CompletionItemKind::SNIPPET,
completion::CompletionKind::Variant => CompletionItemKind::ENUM_MEMBER,
completion::CompletionKind::Member => CompletionItemKind::METHOD,
completion::CompletionKind::Field => CompletionItemKind::FIELD,
completion::CompletionKind::Constructor => CompletionItemKind::CONSTRUCTOR,
completion::CompletionKind::Function => CompletionItemKind::FUNCTION,
}),
insert_text_format: c.insert_text.as_ref().map(|_| InsertTextFormat::SNIPPET),
insert_text: c.insert_text,
label: c.label,
detail: c.detail,
..Default::default()
}
}
fn cursor_byte_offset(text: &str, pos: Position) -> usize {
let mut offset = 0;
for (i, line) in text.split_inclusive('\n').enumerate() {
if i == pos.line as usize {
let bare = line.strip_suffix('\n').unwrap_or(line);
return offset + (pos.character as usize).min(bare.len());
}
offset += line.len();
}
offset.min(text.len())
}
#[derive(serde::Serialize, serde::Deserialize)]
struct SerKey {
unit: String,
kind: String,
name: String,
}
impl From<&bynk_check::index::SymbolKey> for SerKey {
fn from(k: &bynk_check::index::SymbolKey) -> Self {
SerKey {
unit: k.unit.clone(),
kind: k.kind.display().to_string(),
name: k.name.clone(),
}
}
}
impl SerKey {
fn read(data: &Option<serde_json::Value>) -> Option<bynk_check::index::SymbolKey> {
let sk: SerKey = serde_json::from_value(data.as_ref()?.clone()).ok()?;
let kind = match sk.kind.as_str() {
"type" => bynk_check::index::SymbolKind::Type,
"fn" => bynk_check::index::SymbolKind::Fn,
"capability" => bynk_check::index::SymbolKind::Capability,
"service" => bynk_check::index::SymbolKind::Service,
"agent" => bynk_check::index::SymbolKind::Agent,
"provider" => bynk_check::index::SymbolKind::Provider,
_ => return None,
};
Some(bynk_check::index::SymbolKey {
unit: sk.unit,
kind,
name: sk.name,
})
}
}
fn lsp_symbol_kind(kind: bynk_check::index::SymbolKind) -> SymbolKind {
match kind {
bynk_check::index::SymbolKind::Type => SymbolKind::STRUCT,
bynk_check::index::SymbolKind::Fn => SymbolKind::FUNCTION,
bynk_check::index::SymbolKind::Capability => SymbolKind::INTERFACE,
bynk_check::index::SymbolKind::Service | bynk_check::index::SymbolKind::Agent => {
SymbolKind::CLASS
}
bynk_check::index::SymbolKind::Provider => SymbolKind::OBJECT,
bynk_check::index::SymbolKind::Method => SymbolKind::METHOD,
bynk_check::index::SymbolKind::CapabilityOp => SymbolKind::METHOD,
bynk_check::index::SymbolKind::Field => SymbolKind::FIELD,
bynk_check::index::SymbolKind::Actor => SymbolKind::INTERFACE,
}
}
fn make_diagnostic(d: &bynk_ide::Diagnostic, text: &str, uri: &Url) -> Diagnostic {
let range = crate::position::span_to_range(text, d.error.span);
let severity = match d.severity {
bynk_syntax::Severity::Error => DiagnosticSeverity::ERROR,
bynk_syntax::Severity::Warning => DiagnosticSeverity::WARNING,
};
let related_information: Vec<DiagnosticRelatedInformation> = d
.error
.labels
.iter()
.map(|(span, msg)| DiagnosticRelatedInformation {
location: Location {
uri: uri.clone(),
range: crate::position::span_to_range(text, *span),
},
message: msg.clone(),
})
.collect();
let mut message = d.error.message.clone();
for note in &d.error.notes {
message.push_str("\n\n");
message.push_str("note: ");
message.push_str(note);
}
Diagnostic {
range,
severity: Some(severity),
code: Some(NumberOrString::String(d.error.category.to_string())),
code_description: None,
source: Some(SERVER_NAME.to_string()),
message,
related_information: if related_information.is_empty() {
None
} else {
Some(related_information)
},
tags: None,
data: None,
}
}
#[tokio::main]
async fn main() {
if std::env::args()
.skip(1)
.any(|a| a == "--version" || a == "-V")
{
println!("{SERVER_NAME} {SERVER_VERSION}");
return;
}
if let Some(home) = std::env::var_os("HOME") {
let path: PathBuf = PathBuf::from(home).join(".bynk-lsp.log");
if let Ok(file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
{
use tracing_subscriber::prelude::*;
let env_filter = tracing_subscriber::EnvFilter::try_from_env("BYNK_LSP_LOG")
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn"));
let file_layer = tracing_subscriber::fmt::layer()
.with_writer(std::sync::Mutex::new(file))
.with_ansi(false);
tracing_subscriber::registry()
.with(env_filter)
.with(file_layer)
.try_init()
.ok();
}
}
tracing::info!("bynkc-lsp v{} starting", SERVER_VERSION);
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
let (service, socket) = LspService::new(Backend::new);
Server::new(stdin, stdout, socket).serve(service).await;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn advertises_code_actions_and_the_index_riders() {
let caps = server_capabilities();
let Some(CodeActionProviderCapability::Options(opts)) = caps.code_action_provider else {
panic!("codeActionProvider not advertised with options");
};
assert_eq!(opts.code_action_kinds, Some(vec![CodeActionKind::QUICKFIX]));
assert!(matches!(
caps.workspace_symbol_provider,
Some(OneOf::Left(true))
));
assert!(matches!(
caps.document_highlight_provider,
Some(OneOf::Left(true))
));
}
#[test]
fn advertises_inlay_hints() {
let caps = server_capabilities();
assert!(matches!(caps.inlay_hint_provider, Some(OneOf::Left(true))));
}
#[test]
fn advertises_type_definition() {
let caps = server_capabilities();
assert!(matches!(
caps.type_definition_provider,
Some(TypeDefinitionProviderCapability::Simple(true))
));
}
#[test]
fn advertises_document_links() {
let caps = server_capabilities();
assert!(caps.document_link_provider.is_some());
}
#[test]
fn advertises_completion_with_dot_trigger_and_resolve() {
let caps = server_capabilities();
let opts = caps.completion_provider.expect("completion advertised");
assert_eq!(opts.resolve_provider, Some(true), "resolve_provider");
assert!(
opts.trigger_characters
.as_deref()
.is_some_and(|t| t.iter().any(|c| c == ".")),
"`.` trigger char"
);
}
#[test]
fn advertises_semantic_tokens() {
let caps = server_capabilities();
let Some(SemanticTokensServerCapabilities::SemanticTokensOptions(opts)) =
caps.semantic_tokens_provider
else {
panic!("semanticTokensProvider not advertised with options");
};
assert_eq!(opts.full, Some(SemanticTokensFullOptions::Bool(true)));
assert_eq!(opts.range, Some(true));
assert_eq!(opts.legend, crate::index_queries::semantic_tokens_legend());
}
}