diff --git a/crates/stef-lsp/src/handlers/hover.rs b/crates/stef-lsp/src/handlers/hover.rs new file mode 100644 index 0000000..4d522d1 --- /dev/null +++ b/crates/stef-lsp/src/handlers/hover.rs @@ -0,0 +1,184 @@ +use std::{fmt::Write, ops::Range}; + +use anyhow::{Context, Result}; +use line_index::{LineIndex, TextSize, WideLineCol}; +use lsp_types::{Position, Range as LspRange}; +use stef_parser::{ + Comment, Const, Definition, Enum, Fields, Module, NamedField, Schema, Span, Spanned, Struct, + TypeAlias, Variant, +}; + +pub fn visit_schema( + index: &LineIndex, + item: &Schema<'_>, + position: Position, +) -> Result> { + let position = index + .offset( + index + .to_utf8( + line_index::WideEncoding::Utf16, + WideLineCol { + line: position.line, + col: position.character, + }, + ) + .context("missing utf-16 position")?, + ) + .context("missing offset position")? + .into(); + + item.definitions + .iter() + .find_map(|def| visit_definition(def, position)) + .map(|(text, span)| Ok((text, get_range(index, span)?))) + .transpose() +} + +fn visit_definition(item: &Definition<'_>, position: usize) -> Option<(String, Span)> { + match item { + Definition::Module(m) => visit_module(m, position), + Definition::Struct(s) => visit_struct(s, position), + Definition::Enum(e) => visit_enum(e, position), + Definition::TypeAlias(a) => visit_alias(a, position), + Definition::Const(c) => visit_const(c, position), + Definition::Import(_) => None, + } +} + +fn visit_module(item: &Module<'_>, position: usize) -> Option<(String, Span)> { + (Range::from(item.name.span()).contains(&position)) + .then(|| (fold_comment(&item.comment), item.name.span())) + .or_else(|| { + item.definitions + .iter() + .find_map(|def| visit_definition(def, position)) + }) +} + +fn visit_struct(item: &Struct<'_>, position: usize) -> Option<(String, Span)> { + (Range::from(item.name.span()).contains(&position)) + .then(|| { + let mut text = fold_comment(&item.comment); + + if let Some(next_id) = next_field_id(&item.fields) { + let _ = writeln!(&mut text, "- next ID: `{next_id}`"); + } + + (text, item.name.span()) + }) + .or_else(|| visit_fields(&item.fields, position)) +} + +fn visit_enum(item: &Enum<'_>, position: usize) -> Option<(String, Span)> { + (Range::from(item.name.span()).contains(&position)) + .then(|| { + let mut text = fold_comment(&item.comment); + + let _ = writeln!( + &mut text, + "- next ID: `{}`", + next_variant_id(&item.variants) + ); + + (text, item.name.span()) + }) + .or_else(|| { + item.variants + .iter() + .find_map(|variant| visit_variant(variant, position)) + }) +} + +fn visit_variant(item: &Variant<'_>, position: usize) -> Option<(String, Span)> { + (Range::from(item.name.span()).contains(&position)) + .then(|| { + let mut text = fold_comment(&item.comment); + + if let Some(next_id) = next_field_id(&item.fields) { + let _ = writeln!(&mut text, "- next ID: `{next_id}`"); + } + + (text, item.name.span()) + }) + .or_else(|| visit_fields(&item.fields, position)) +} + +fn visit_fields(item: &Fields<'_>, position: usize) -> Option<(String, Span)> { + if let Fields::Named(named) = item { + named + .iter() + .find_map(|field| visit_named_field(field, position)) + } else { + None + } +} + +fn visit_named_field(item: &NamedField<'_>, position: usize) -> Option<(String, Span)> { + (Range::from(item.name.span()).contains(&position)) + .then(|| (fold_comment(&item.comment), item.name.span())) +} + +fn visit_alias(item: &TypeAlias<'_>, position: usize) -> Option<(String, Span)> { + (Range::from(item.name.span()).contains(&position)) + .then(|| (fold_comment(&item.comment), item.name.span())) +} + +fn visit_const(item: &Const<'_>, position: usize) -> Option<(String, Span)> { + (Range::from(item.name.span()).contains(&position)) + .then(|| (fold_comment(&item.comment), item.name.span())) +} + +fn fold_comment(comment: &Comment<'_>) -> String { + comment.0.iter().fold(String::new(), |mut acc, line| { + acc.push_str(line.value); + acc.push('\n'); + acc + }) +} + +fn next_variant_id(variants: &[Variant<'_>]) -> u32 { + variants + .iter() + .map(|variant| variant.id.get()) + .max() + .unwrap_or(0) + + 1 +} + +fn next_field_id(fields: &Fields<'_>) -> Option { + match fields { + Fields::Named(named) => { + Some(named.iter().map(|field| field.id.get()).max().unwrap_or(0) + 1) + } + Fields::Unnamed(unnamed) => Some( + unnamed + .iter() + .map(|field| field.id.get()) + .max() + .unwrap_or(0) + + 1, + ), + Fields::Unit => None, + } +} + +#[allow(clippy::cast_possible_truncation)] +fn get_range(index: &LineIndex, span: Span) -> Result { + let range = Range::from(span); + let (start, end) = index + .to_wide( + line_index::WideEncoding::Utf16, + index.line_col(TextSize::new(range.start as u32)), + ) + .zip(index.to_wide( + line_index::WideEncoding::Utf16, + index.line_col(TextSize::new(range.end as u32)), + )) + .context("missing utf-16 positions")?; + + Ok(LspRange::new( + Position::new(start.line, start.col), + Position::new(end.line, end.col), + )) +} diff --git a/crates/stef-lsp/src/handlers/mod.rs b/crates/stef-lsp/src/handlers/mod.rs index 509dc9b..9de3b64 100644 --- a/crates/stef-lsp/src/handlers/mod.rs +++ b/crates/stef-lsp/src/handlers/mod.rs @@ -5,12 +5,12 @@ use line_index::{LineIndex, TextRange}; use log::{as_debug, as_display, debug, error, warn}; use lsp_types::{ DidChangeConfigurationParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, - DidOpenTextDocumentParams, DocumentSymbolOptions, DocumentSymbolParams, DocumentSymbolResponse, - InitializeParams, InitializeResult, InitializedParams, PositionEncodingKind, Registration, - SemanticTokens, SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, - SemanticTokensParams, SemanticTokensResult, SemanticTokensServerCapabilities, - ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, - WorkDoneProgressOptions, + DidOpenTextDocumentParams, DocumentSymbolParams, DocumentSymbolResponse, Hover, HoverContents, + HoverParams, HoverProviderCapability, InitializeParams, InitializeResult, InitializedParams, + MarkupContent, MarkupKind, OneOf, PositionEncodingKind, Registration, SemanticTokens, + SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, SemanticTokensParams, + SemanticTokensResult, SemanticTokensServerCapabilities, ServerCapabilities, ServerInfo, + TextDocumentSyncCapability, TextDocumentSyncKind, WorkDoneProgressOptions, }; use ropey::Rope; @@ -18,6 +18,7 @@ use crate::{state::FileBuilder, GlobalState}; mod compile; mod document_symbols; +mod hover; mod semantic_tokens; pub fn initialize( @@ -34,12 +35,8 @@ pub fn initialize( text_document_sync: Some(TextDocumentSyncCapability::Kind( TextDocumentSyncKind::INCREMENTAL, )), - document_symbol_provider: Some(lsp_types::OneOf::Right(DocumentSymbolOptions { - label: None, - work_done_progress_options: WorkDoneProgressOptions { - work_done_progress: Some(false), - }, - })), + hover_provider: Some(HoverProviderCapability::Simple(true)), + document_symbol_provider: Some(OneOf::Left(true)), semantic_tokens_provider: Some( SemanticTokensServerCapabilities::SemanticTokensOptions(SemanticTokensOptions { work_done_progress_options: WorkDoneProgressOptions { @@ -179,6 +176,35 @@ pub fn did_close(state: &mut GlobalState<'_>, params: DidCloseTextDocumentParams state.files.remove(¶ms.text_document.uri); } +pub fn hover(state: &mut GlobalState<'_>, params: HoverParams) -> Result> { + let uri = params.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + + debug!( + uri = as_display!(uri); + "requested hover info", + ); + + if let Some((schema, index)) = state.files.get_mut(&uri).and_then(|file| { + file.borrow_schema() + .as_ref() + .ok() + .zip(Some(file.borrow_index())) + }) { + Ok( + hover::visit_schema(index, schema, position)?.map(|(value, range)| Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value, + }), + range: Some(range), + }), + ) + } else { + Ok(None) + } +} + pub fn document_symbol( state: &mut GlobalState<'_>, params: DocumentSymbolParams, diff --git a/crates/stef-lsp/src/logging.rs b/crates/stef-lsp/src/logging.rs index 80f13cc..bcc70f1 100644 --- a/crates/stef-lsp/src/logging.rs +++ b/crates/stef-lsp/src/logging.rs @@ -238,8 +238,7 @@ impl log::Log for CombinedLogger { && metadata.level() <= Level::Trace) || (metadata.target().starts_with("stef_compiler") && metadata.level() <= Level::Trace) || (metadata.target().starts_with("stef_parser") && metadata.level() <= Level::Trace) - || (metadata.target().starts_with("lsp_server::msg") - && metadata.level() <= Level::Debug) + || (metadata.target().starts_with("lsp_server") && metadata.level() <= Level::Info) } fn log(&self, record: &Record<'_>) { diff --git a/crates/stef-lsp/src/main.rs b/crates/stef-lsp/src/main.rs index bcac12b..666457b 100644 --- a/crates/stef-lsp/src/main.rs +++ b/crates/stef-lsp/src/main.rs @@ -1,7 +1,7 @@ #![warn(clippy::expect_used, clippy::unwrap_used)] #![allow(missing_docs)] -use std::{collections::HashMap, net::Ipv4Addr}; +use std::{collections::HashMap, net::Ipv4Addr, time::Instant}; use anyhow::{bail, Result}; use log::{as_debug, debug, error, info, warn}; @@ -11,7 +11,10 @@ use lsp_types::{ DidChangeConfiguration, DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Initialized, Notification as LspNotification, }, - request::{DocumentSymbolRequest, Request as LspRequest, SemanticTokensFullRequest, Shutdown}, + request::{ + DocumentSymbolRequest, HoverRequest, Request as LspRequest, SemanticTokensFullRequest, + Shutdown, + }, DocumentSymbol, InitializeParams, SemanticTokens, }; @@ -78,6 +81,15 @@ fn main_loop(conn: &Connection, mut state: GlobalState<'_>) -> Result<()> { Shutdown::METHOD => { warn!("should never reach this"); } + HoverRequest::METHOD => { + handle_request::( + conn, + &mut state, + req, + handlers::hover, + |value| value, + )?; + } DocumentSymbolRequest::METHOD => { handle_request::( conn, @@ -161,7 +173,11 @@ where } }; + let start = Instant::now(); let result = handler(state, params); + + debug!(duration = as_debug!(start.elapsed()); "handled request"); + conn.sender .send( match result {