From 17b3fbc5c0eca37eabf125efa807126d6c38e70e Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama Date: Sat, 12 Oct 2024 15:48:08 +0900 Subject: [PATCH] feat(els): impl document link --- crates/els/channels.rs | 24 ++- crates/els/doc_link.rs | 276 +++++++++++++++++++++++++++++ crates/els/lib.rs | 1 + crates/els/main.rs | 1 + crates/els/scheduler.rs | 18 +- crates/els/server.rs | 21 ++- crates/erg_compiler/context/mod.rs | 1 + crates/erg_parser/ast.rs | 4 + crates/erg_parser/token.rs | 4 + 9 files changed, 323 insertions(+), 27 deletions(-) create mode 100644 crates/els/doc_link.rs diff --git a/crates/els/channels.rs b/crates/els/channels.rs index 851423012..b90a3fcdf 100644 --- a/crates/els/channels.rs +++ b/crates/els/channels.rs @@ -6,19 +6,19 @@ use erg_compiler::erg_parser::parse::Parsable; use lsp_types::request::{ CallHierarchyIncomingCalls, CallHierarchyOutgoingCalls, CallHierarchyPrepare, CodeActionRequest, CodeActionResolveRequest, CodeLensRequest, Completion, - DocumentHighlightRequest, DocumentSymbolRequest, ExecuteCommand, FoldingRangeRequest, - GotoDefinition, GotoImplementation, GotoImplementationParams, GotoTypeDefinition, - GotoTypeDefinitionParams, HoverRequest, InlayHintRequest, InlayHintResolveRequest, References, - ResolveCompletionItem, SelectionRangeRequest, SemanticTokensFullRequest, SignatureHelpRequest, - WillRenameFiles, WorkspaceSymbol, + DocumentHighlightRequest, DocumentLinkRequest, DocumentSymbolRequest, ExecuteCommand, + FoldingRangeRequest, GotoDefinition, GotoImplementation, GotoImplementationParams, + GotoTypeDefinition, GotoTypeDefinitionParams, HoverRequest, InlayHintRequest, + InlayHintResolveRequest, References, ResolveCompletionItem, SelectionRangeRequest, + SemanticTokensFullRequest, SignatureHelpRequest, WillRenameFiles, WorkspaceSymbol, }; use lsp_types::{ CallHierarchyIncomingCallsParams, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams, CodeAction, CodeActionParams, CodeLensParams, CompletionItem, CompletionParams, - DocumentHighlightParams, DocumentSymbolParams, ExecuteCommandParams, FoldingRangeParams, - GotoDefinitionParams, HoverParams, InlayHint, InlayHintParams, ReferenceParams, - RenameFilesParams, SelectionRangeParams, SemanticTokensParams, SignatureHelpParams, - WorkspaceSymbolParams, + DocumentHighlightParams, DocumentLinkParams, DocumentSymbolParams, ExecuteCommandParams, + FoldingRangeParams, GotoDefinitionParams, HoverParams, InlayHint, InlayHintParams, + ReferenceParams, RenameFilesParams, SelectionRangeParams, SemanticTokensParams, + SignatureHelpParams, WorkspaceSymbolParams, }; use crate::server::Server; @@ -61,6 +61,7 @@ pub struct SendChannels { folding_range: mpsc::Sender>, selection_range: mpsc::Sender>, document_highlight: mpsc::Sender>, + document_link: mpsc::Sender>, pub(crate) health_check: mpsc::Sender>, } @@ -90,6 +91,7 @@ impl SendChannels { let (tx_folding_range, rx_folding_range) = mpsc::channel(); let (tx_selection_range, rx_selection_range) = mpsc::channel(); let (tx_document_highlight, rx_document_highlight) = mpsc::channel(); + let (tx_document_link, rx_document_link) = mpsc::channel(); let (tx_health_check, rx_health_check) = mpsc::channel(); ( Self { @@ -117,6 +119,7 @@ impl SendChannels { folding_range: tx_folding_range, selection_range: tx_selection_range, document_highlight: tx_document_highlight, + document_link: tx_document_link, health_check: tx_health_check, }, ReceiveChannels { @@ -144,6 +147,7 @@ impl SendChannels { folding_range: rx_folding_range, selection_range: rx_selection_range, document_highlight: rx_document_highlight, + document_link: rx_document_link, health_check: rx_health_check, }, ) @@ -206,6 +210,7 @@ pub struct ReceiveChannels { pub(crate) folding_range: mpsc::Receiver>, pub(crate) selection_range: mpsc::Receiver>, pub(crate) document_highlight: mpsc::Receiver>, + pub(crate) document_link: mpsc::Receiver>, pub(crate) health_check: mpsc::Receiver>, } @@ -292,3 +297,4 @@ impl_sendable!( DocumentHighlightParams, document_highlight ); +impl_sendable!(DocumentLinkRequest, DocumentLinkParams, document_link); diff --git a/crates/els/doc_link.rs b/crates/els/doc_link.rs new file mode 100644 index 000000000..9f54c774f --- /dev/null +++ b/crates/els/doc_link.rs @@ -0,0 +1,276 @@ +use std::path::Path; + +use erg_common::Str; +use erg_compiler::artifact::BuildRunnable; +use erg_compiler::erg_parser::ast::{ClassAttr, Expr, Literal}; +use erg_compiler::erg_parser::parse::Parsable; + +use erg_compiler::erg_parser::token::{Token, TokenKind}; +use erg_compiler::ty::Type; +use erg_compiler::varinfo::VarInfo; +use lsp_types::{DocumentLink, DocumentLinkParams, Position, Range, Url}; + +use crate::_log; +use crate::server::{ELSResult, RedirectableStdout, Server}; +use crate::util::NormalizedUrl; + +/// programming related words are not considered usual words (e.g. `if`, `while`) +fn is_usual_word(word: &str) -> bool { + matches!( + word, + "a" | "the" + | "an" + | "is" + | "are" + | "was" + | "were" + | "be" + | "been" + | "being" + | "am" + | "does" + | "did" + | "done" + | "doing" + | "have" + | "has" + | "had" + | "having" + | "will" + | "shall" + | "would" + | "should" + | "may" + | "might" + | "must" + | "can" + | "could" + | "need" + | "used" + | "to" + | "of" + | "in" + | "on" + | "at" + | "by" + | "with" + | "from" + | "about" + | "between" + | "among" + | "into" + | "onto" + | "upon" + | "over" + | "under" + | "above" + | "below" + | "behind" + | "beside" + | "before" + | "after" + | "during" + | "through" + | "across" + | "against" + | "towards" + | "around" + | "besides" + | "like" + | "near" + | "till" + | "until" + | "throughout" + | "within" + | "without" + | "according" + | "though" + | "whereas" + | "whether" + | "so" + | "although" + ) +} + +impl Server { + pub(crate) fn handle_document_link( + &mut self, + params: DocumentLinkParams, + ) -> ELSResult>> { + _log!(self, "document link requested: {params:?}"); + let uri = NormalizedUrl::new(params.text_document.uri); + let mut res = vec![]; + res.extend(self.get_document_link(&uri)); + Ok(Some(res)) + } + + fn get_document_link(&self, uri: &NormalizedUrl) -> Vec { + let mut res = vec![]; + if let Some(ast) = self.get_ast(uri) { + let mut comments = vec![]; + for chunk in ast.block().iter() { + comments.extend(self.get_doc_comments(chunk)); + } + for comment in comments { + res.extend(self.gen_doc_link(uri, comment)); + } + } else if let Ok(ast) = self.build_ast(uri) { + let mut comments = vec![]; + for chunk in ast.block().iter() { + comments.extend(self.get_doc_comments(chunk)); + } + for comment in comments { + res.extend(self.gen_doc_link(uri, comment)); + } + } + for comment in self.get_comments(uri) { + res.extend(self.gen_doc_link(uri, &comment)); + } + res + } + + fn gen_doc_link(&self, uri: &NormalizedUrl, comment: &Literal) -> Vec { + let mut res = vec![]; + let Some(mod_ctx) = self.get_mod_ctx(uri) else { + return vec![]; + }; + let mut line = comment.token.lineno.saturating_sub(1); + let mut col = comment.token.col_begin; + for li in comment.token.content.split('\n') { + let li = Str::rc(li); + let words = li.split_with(&[" ", "'", "\"", "`"]); + for word in words { + if word.trim().is_empty() || is_usual_word(word) { + col += word.len() as u32 + 1; + continue; + } + let range = Range { + start: Position { + line, + character: col, + }, + end: Position { + line, + character: col + word.len() as u32, + }, + }; + let typ = Type::Mono(Str::rc(word)); + if let Some(path) = self.cfg.input.resolve_path(Path::new(word), &self.cfg) { + let target = Url::from_file_path(path).ok(); + res.push(DocumentLink { + range, + target, + tooltip: Some(format!("module {word}")), + data: None, + }); + } else if let Some((_, vi)) = mod_ctx.context.get_type_info(&typ) { + res.push(self.gen_doc_link_from_vi(word, range, vi)); + } + col += word.len() as u32 + 1; + } + line += 1; + col = 0; + } + res + } + + fn gen_doc_link_from_vi(&self, name: &str, range: Range, vi: &VarInfo) -> DocumentLink { + let mut target = if let Some(path) = vi.t.module_path() { + Url::from_file_path(path).ok() + } else { + vi.def_loc + .module + .as_ref() + .and_then(|path| Url::from_file_path(path).ok()) + }; + if let Some(target) = target.as_mut() { + target.set_fragment( + vi.def_loc + .loc + .ln_begin() + .map(|l| format!("L{l}")) + .as_deref(), + ); + } + let tooltip = format!("{name}: {}", vi.t); + DocumentLink { + range, + target, + tooltip: Some(tooltip), + data: None, + } + } + + #[allow(clippy::only_used_in_recursion)] + fn get_doc_comments<'e>(&self, expr: &'e Expr) -> Vec<&'e Literal> { + match expr { + Expr::Literal(lit) if lit.is_doc_comment() => vec![lit], + Expr::Def(def) => { + let mut comments = vec![]; + for chunk in def.body.block.iter() { + comments.extend(self.get_doc_comments(chunk)); + } + comments + } + Expr::Methods(methods) => { + let mut comments = vec![]; + for chunk in methods.attrs.iter() { + match chunk { + ClassAttr::Def(def) => { + for chunk in def.body.block.iter() { + comments.extend(self.get_doc_comments(chunk)); + } + } + ClassAttr::Doc(lit) => { + comments.push(lit); + } + _ => {} + } + } + comments + } + Expr::Call(call) => { + let mut comments = vec![]; + for arg in call.args.pos_args() { + comments.extend(self.get_doc_comments(&arg.expr)); + } + comments + } + Expr::Lambda(lambda) => { + let mut comments = vec![]; + for chunk in lambda.body.iter() { + comments.extend(self.get_doc_comments(chunk)); + } + comments + } + Expr::Dummy(dummy) => { + let mut comments = vec![]; + for chunk in dummy.exprs.iter() { + comments.extend(self.get_doc_comments(chunk)); + } + comments + } + Expr::InlineModule(module) => { + let mut comments = vec![]; + for chunk in module.ast.module.block().iter() { + comments.extend(self.get_doc_comments(chunk)); + } + comments + } + _ => vec![], + } + } + + fn get_comments(&self, uri: &NormalizedUrl) -> Vec { + let mut lines = vec![]; + if let Ok(code) = self.file_cache.get_entire_code(uri) { + for (line, li) in code.lines().enumerate() { + if let Some(li) = li.strip_prefix("#") { + let token = Token::new(TokenKind::DocComment, Str::rc(li), line as u32 + 1, 1); + lines.push(Literal::new(token)); + } + } + } + lines + } +} diff --git a/crates/els/lib.rs b/crates/els/lib.rs index 9b45deaef..ecc0be835 100644 --- a/crates/els/lib.rs +++ b/crates/els/lib.rs @@ -8,6 +8,7 @@ mod definition; mod diagnostics; mod diff; mod doc_highlight; +mod doc_link; mod file_cache; mod folding_range; mod hir_visitor; diff --git a/crates/els/main.rs b/crates/els/main.rs index 262013c04..5841ca322 100644 --- a/crates/els/main.rs +++ b/crates/els/main.rs @@ -8,6 +8,7 @@ mod definition; mod diagnostics; mod diff; mod doc_highlight; +mod doc_link; mod file_cache; mod folding_range; mod hir_visitor; diff --git a/crates/els/scheduler.rs b/crates/els/scheduler.rs index d71168e44..8ac479590 100644 --- a/crates/els/scheduler.rs +++ b/crates/els/scheduler.rs @@ -173,20 +173,14 @@ impl Scheduler { /// Only pending tasks can be cancelled /// TODO: cancel executing tasks pub fn cancel(&self, id: TaskID) -> Option { - let idx = self - .pending - .borrow() - .iter() - .position(|task| task.id == id)?; - self.pending.borrow_mut().remove(idx) + let mut lock = self.pending.borrow_mut(); + let idx = lock.iter().position(|task| task.id == id)?; + lock.remove(idx) } pub fn finish(&self, id: TaskID) -> Option { - let idx = self - .executing - .borrow() - .iter() - .position(|task| task.id == id)?; - Some(self.executing.borrow_mut().remove(idx)) + let mut lock = self.executing.borrow_mut(); + let idx = lock.iter().position(|task| task.id == id)?; + Some(lock.remove(idx)) } } diff --git a/crates/els/server.rs b/crates/els/server.rs index e19797b96..83b3ef46b 100644 --- a/crates/els/server.rs +++ b/crates/els/server.rs @@ -37,9 +37,9 @@ use molc::{FakeClient, LangServer}; use lsp_types::request::{ CallHierarchyIncomingCalls, CallHierarchyOutgoingCalls, CallHierarchyPrepare, CodeActionRequest, CodeActionResolveRequest, CodeLensRequest, Completion, - DocumentHighlightRequest, DocumentSymbolRequest, ExecuteCommand, FoldingRangeRequest, - GotoDefinition, GotoImplementation, GotoTypeDefinition, HoverRequest, InlayHintRequest, - InlayHintResolveRequest, References, Rename, Request, ResolveCompletionItem, + DocumentHighlightRequest, DocumentLinkRequest, DocumentSymbolRequest, ExecuteCommand, + FoldingRangeRequest, GotoDefinition, GotoImplementation, GotoTypeDefinition, HoverRequest, + InlayHintRequest, InlayHintResolveRequest, References, Rename, Request, ResolveCompletionItem, SelectionRangeRequest, SemanticTokensFullRequest, SignatureHelpRequest, WillRenameFiles, WorkspaceSymbol, }; @@ -47,9 +47,9 @@ use lsp_types::{ CallHierarchyServerCapability, CodeActionKind, CodeActionOptions, CodeActionProviderCapability, CodeLensOptions, CompletionOptions, ConfigurationItem, ConfigurationParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, - ExecuteCommandOptions, FoldingRangeProviderCapability, HoverProviderCapability, - ImplementationProviderCapability, InitializeParams, InitializeResult, InlayHintOptions, - InlayHintServerCapabilities, NumberOrString, OneOf, Position, ProgressParams, + DocumentLinkOptions, ExecuteCommandOptions, FoldingRangeProviderCapability, + HoverProviderCapability, ImplementationProviderCapability, InitializeParams, InitializeResult, + InlayHintOptions, InlayHintServerCapabilities, NumberOrString, OneOf, Position, ProgressParams, ProgressParamsValue, SelectionRangeProviderCapability, SemanticTokenType, SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, SemanticTokensServerCapabilities, ServerCapabilities, SignatureHelpOptions, @@ -535,6 +535,10 @@ impl Server { }); capabilities.workspace_symbol_provider = Some(OneOf::Left(true)); capabilities.document_symbol_provider = Some(OneOf::Left(true)); + capabilities.document_link_provider = Some(DocumentLinkOptions { + resolve_provider: Some(false), + work_done_progress_options: Default::default(), + }); capabilities.call_hierarchy_provider = Some(CallHierarchyServerCapability::Simple(true)); capabilities.folding_range_provider = Some(FoldingRangeProviderCapability::Simple(true)); capabilities.selection_range_provider = @@ -639,6 +643,10 @@ impl Server { receivers.document_highlight, Self::handle_document_highlight, ); + self.start_service::( + receivers.document_link, + Self::handle_document_link, + ); self.start_client_health_checker(receivers.health_check); } @@ -897,6 +905,7 @@ impl Server { DocumentHighlightRequest::METHOD => { self.parse_send::(id, msg) } + DocumentLinkRequest::METHOD => self.parse_send::(id, msg), other => self.send_error(Some(id), -32600, format!("{other} is not supported")), } } diff --git a/crates/erg_compiler/context/mod.rs b/crates/erg_compiler/context/mod.rs index c32b74cf6..31985b357 100644 --- a/crates/erg_compiler/context/mod.rs +++ b/crates/erg_compiler/context/mod.rs @@ -739,6 +739,7 @@ impl Context { ContextProvider::has(self, name) } + /// NOTE: Actually, this can be used for searching variables, attributes, etc. pub fn get_type_info(&self, typ: &Type) -> Option<(&VarName, &VarInfo)> { let namespace = typ.namespace(); let ctx = self.get_namespace(&namespace)?; diff --git a/crates/erg_parser/ast.rs b/crates/erg_parser/ast.rs index b8c5192b7..3a9e6bc03 100644 --- a/crates/erg_parser/ast.rs +++ b/crates/erg_parser/ast.rs @@ -262,6 +262,10 @@ impl Literal { self.token.is_number() } + pub fn is_str(&self) -> bool { + self.token.is_str() + } + #[inline] pub fn is_doc_comment(&self) -> bool { self.token.is(TokenKind::DocComment) diff --git a/crates/erg_parser/token.rs b/crates/erg_parser/token.rs index faebfc492..ff12e31ba 100644 --- a/crates/erg_parser/token.rs +++ b/crates/erg_parser/token.rs @@ -541,6 +541,10 @@ impl Token { ) } + pub fn is_str(&self) -> bool { + matches!(self.kind, StrLit | DocComment) + } + pub const fn is_block_op(&self) -> bool { self.category().is_block_op() }