From db0c603eff28150dce5dba552e77592b6833268e Mon Sep 17 00:00:00 2001 From: Dominik Nakamura Date: Fri, 15 Dec 2023 22:40:22 +0900 Subject: [PATCH] feat(lsp): provide hover information Generate hover information for any element that is hovered by the user. This currently only replies the documentation for the element (if present), and calculates the next available ID for struct fields, enum variants and enum variant fields. --- crates/stef-lsp/src/handlers/hover.rs | 184 ++++++++++++++++++++++++++ crates/stef-lsp/src/handlers/mod.rs | 50 +++++-- crates/stef-lsp/src/logging.rs | 3 +- crates/stef-lsp/src/main.rs | 20 ++- 4 files changed, 241 insertions(+), 16 deletions(-) create mode 100644 crates/stef-lsp/src/handlers/hover.rs 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 {