feat: correctly parse and show hover doc (#105)

* feat: seperate content on hover tips

* dev: half hover

* fix: ensure extracting docs correctly
This commit is contained in:
Myriad-Dreamin 2024-03-27 10:19:34 +08:00 committed by GitHub
parent de2504b15f
commit 3344eebe3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 355 additions and 131 deletions

View file

@ -10,7 +10,6 @@ pub use global::*;
#[cfg(test)]
mod module_tests {
use ecow::EcoVec;
use reflexo::path::unix_slash;
use serde_json::json;
@ -63,6 +62,56 @@ mod module_tests {
}
}
#[cfg(test)]
mod matcher_tests {
use typst::syntax::LinkedNode;
use crate::{syntax::get_def_target, tests::*};
#[test]
fn test() {
snapshot_testing("match_def", &|ctx, path| {
let source = ctx.source_by_path(&path).unwrap();
let pos = ctx
.to_typst_pos(find_test_position(&source), &source)
.unwrap();
let root = LinkedNode::new(source.root());
let node = root.leaf_at(pos).unwrap();
let result = get_def_target(node).map(|e| format!("{:?}", e.node().range()));
let result = result.as_deref().unwrap_or("<nil>");
assert_snapshot!(result);
});
}
}
#[cfg(test)]
mod document_tests {
use crate::syntax::find_document_before;
use crate::tests::*;
#[test]
fn test() {
snapshot_testing("docs", &|ctx, path| {
let source = ctx.source_by_path(&path).unwrap();
let pos = ctx
.to_typst_pos(find_test_position(&source), &source)
.unwrap();
let result = find_document_before(&source, pos);
let result = result.as_deref().unwrap_or("<nil>");
assert_snapshot!(result);
});
}
}
#[cfg(test)]
mod lexical_hierarchy_tests {
use def_use::DefUseSnapshot;

View file

@ -4,16 +4,16 @@ expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/def_use/base.typ
---
{
"x@5..6@s0.typ": {
"x@33..34@s0.typ": {
"def": {
"kind": {
"Var": "Variable"
},
"name": "x",
"range": "5:6"
"range": "33:34"
},
"refs": [
"x@13..14"
"x@41..42"
]
}
}

View file

@ -0,0 +1,2 @@
// This is X.
#let x /* ident */ = 1;

View file

@ -0,0 +1,2 @@
/* This is X */
#let x /* ident */ = 1;

View file

@ -0,0 +1,3 @@
/* This is X
Note: This is not Y */
#let x /* ident */ = 1;

View file

@ -0,0 +1,3 @@
// This is X.
// Note: this is not Y.
#let x /* ident */ = 1;

View file

@ -0,0 +1,2 @@
#let x /* some comment */ = 1;
#let x /* ident */ = 1;

View file

@ -0,0 +1,4 @@
/* This is X
Note: This is not Y */
#let x /* ident */ = 1;

View file

@ -0,0 +1,3 @@
// This is X
#let x /* ident */ = 1;

View file

@ -0,0 +1,4 @@
// Docs for f.
#let f(/* ident after */ a) = {
show it: it => it;
};

View file

@ -0,0 +1,4 @@
// Docs for f.
#let f(a) = {
show it: it => /* ident after */ it;
};

View file

@ -0,0 +1,6 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: result
input_file: crates/tinymist-query/src/fixtures/docs/base.typ
---
This is X.

View file

@ -0,0 +1,6 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: result
input_file: crates/tinymist-query/src/fixtures/docs/blocky.typ
---
This is X

View file

@ -0,0 +1,7 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: result
input_file: crates/tinymist-query/src/fixtures/docs/blocky2.typ
---
This is X
Note: This is not Y

View file

@ -0,0 +1,7 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: result
input_file: crates/tinymist-query/src/fixtures/docs/multiple_line.typ
---
This is X.
Note: this is not Y.

View file

@ -0,0 +1,6 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: "result.as_deref().unwrap_or(\"<nil>\")"
input_file: crates/tinymist-query/src/fixtures/document/no_comment.typ
---
<nil>

View file

@ -0,0 +1,6 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: result
input_file: crates/tinymist-query/src/fixtures/docs/not_attach.typ
---
<nil>

View file

@ -0,0 +1,6 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: result
input_file: crates/tinymist-query/src/fixtures/docs/not_attach2.typ
---
<nil>

View file

@ -0,0 +1,6 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: result
input_file: crates/tinymist-query/src/fixtures/docs/param.typ
---
<nil>

View file

@ -0,0 +1,6 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: result
input_file: crates/tinymist-query/src/fixtures/docs/param_in_init.typ
---
<nil>

View file

@ -4,16 +4,16 @@ expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
input_file: crates/tinymist-query/src/fixtures/lexical_hierarchy/base.typ
---
{
"x@5..6@s0.typ": {
"x@33..34@s0.typ": {
"def": {
"kind": {
"Var": "Variable"
},
"name": "x",
"range": "5:6"
"range": "33:34"
},
"refs": [
"x@13..14"
"x@41..42"
]
}
}

View file

@ -9,13 +9,13 @@ input_file: crates/tinymist-query/src/fixtures/lexical_hierarchy/base.typ
"Var": "Variable"
},
"name": "x",
"range": "5:6"
"range": "33:34"
},
{
"kind": {
"Var": "ValRef"
},
"name": "x",
"range": "13:14"
"range": "41:42"
}
]

View file

@ -0,0 +1 @@
#let /* ident after */ f(a) = a;

View file

@ -0,0 +1 @@
#let f(a ) = /* ident after */a;

View file

@ -0,0 +1,3 @@
#let f(a) = {
/* ident after */ a
};

View file

@ -0,0 +1 @@
#let f(a /* ident */) = a;

View file

@ -0,0 +1,3 @@
#let f(a) = {
show it: /* ident after */ it => it;
};

View file

@ -0,0 +1,3 @@
#let f(a) = {
set text(/* ident after */ fill: a);
};

View file

@ -0,0 +1,6 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: result
input_file: crates/tinymist-query/src/fixtures/match_def/base.typ
---
1..31

View file

@ -0,0 +1,6 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: result
input_file: crates/tinymist-query/src/fixtures/match_def/ident_in_init.typ
---
<nil>

View file

@ -0,0 +1,6 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: result
input_file: crates/tinymist-query/src/fixtures/match_def/ident_in_init2.typ
---
<nil>

View file

@ -0,0 +1,6 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: result
input_file: crates/tinymist-query/src/fixtures/match_def/param.typ
---
<nil>

View file

@ -0,0 +1,6 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: result
input_file: crates/tinymist-query/src/fixtures/match_def/param_in_init.typ
---
<nil>

View file

@ -0,0 +1,6 @@
---
source: crates/tinymist-query/src/analysis.rs
expression: result
input_file: crates/tinymist-query/src/fixtures/match_def/param_in_init2.typ
---
<nil>

View file

@ -3,7 +3,7 @@ use core::fmt;
use crate::{
analyze_signature, find_definition,
prelude::*,
syntax::{get_def_target, get_deref_target, LexicalVarKind},
syntax::{find_document_before, get_deref_target, LexicalKind, LexicalVarKind},
upstream::{expr_tooltip, tooltip, Tooltip},
DefinitionLink, LspHoverContents, StatefulRequest,
};
@ -68,42 +68,58 @@ fn def_tooltip(
let lnk = find_definition(ctx, source.clone(), deref_target.clone())?;
let mut results = vec![];
match lnk.kind {
crate::syntax::LexicalKind::Mod(_)
| crate::syntax::LexicalKind::Var(LexicalVarKind::Label)
| crate::syntax::LexicalKind::Var(LexicalVarKind::LabelRef)
| crate::syntax::LexicalKind::Var(LexicalVarKind::ValRef)
| crate::syntax::LexicalKind::Block
| crate::syntax::LexicalKind::Heading(..) => None,
crate::syntax::LexicalKind::Var(LexicalVarKind::Function) => Some(
LspHoverContents::Scalar(lsp_types::MarkedString::String(format!(
r#"```typc
let {name}({params});
```{doc}"#,
name = lnk.name,
params = ParamTooltip(&lnk),
doc = DocTooltip::get(ctx, &lnk).unwrap_or_default(),
))),
),
crate::syntax::LexicalKind::Var(LexicalVarKind::Variable) => {
LexicalKind::Mod(_)
| LexicalKind::Var(LexicalVarKind::Label)
| LexicalKind::Var(LexicalVarKind::LabelRef)
| LexicalKind::Var(LexicalVarKind::ValRef)
| LexicalKind::Block
| LexicalKind::Heading(..) => None,
LexicalKind::Var(LexicalVarKind::Function) => {
results.push(MarkedString::LanguageString(LanguageString {
language: "typc".to_owned(),
value: format!(
"let {name}({params});",
name = lnk.name,
params = ParamTooltip(&lnk)
),
}));
if let Some(doc) = DocTooltip::get(ctx, &lnk) {
results.push(MarkedString::String(doc));
}
Some(LspHoverContents::Array(results))
}
LexicalKind::Var(LexicalVarKind::Variable) => {
let deref_node = deref_target.node();
// todo: check sensible length, value highlighting
let values = expr_tooltip(ctx.world(), deref_node)
.map(|t| match t {
Tooltip::Text(s) => format!("// Values: {s}"),
Tooltip::Code(s) => format!("// Values: {s}"),
})
.unwrap_or_default();
Some(LspHoverContents::Scalar(lsp_types::MarkedString::String(
format!(
r#"```typc
{values}
let {name};
```{doc}"#,
name = lnk.name,
doc = DocTooltip::get(ctx, &lnk).unwrap_or_default(),
),
)))
if let Some(values) = expr_tooltip(ctx.world(), deref_node) {
match values {
Tooltip::Text(values) => {
results.push(MarkedString::String(format!("Values: {values}")));
}
Tooltip::Code(values) => {
results.push(MarkedString::LanguageString(LanguageString {
language: "typc".to_owned(),
value: format!("// Values\n{values}"),
}));
}
}
}
results.push(MarkedString::LanguageString(LanguageString {
language: "typc".to_owned(),
value: format!("let {name};", name = lnk.name),
}));
if let Some(doc) = DocTooltip::get(ctx, &lnk) {
results.push(MarkedString::String(doc));
}
Some(LspHoverContents::Array(results))
}
}
}
@ -170,81 +186,18 @@ impl DocTooltip {
}
fn get_inner(ctx: &mut AnalysisContext, lnk: &DefinitionLink) -> Option<String> {
// let doc = find_document_before(ctx, &lnk);
if matches!(lnk.value, Some(Value::Func(..))) {
if let Some(builtin) = Self::builtin_func_tooltip(lnk) {
return Some(builtin.to_owned());
}
};
Self::find_document_before(ctx, lnk)
let src = ctx.source_by_id(lnk.fid).ok()?;
find_document_before(&src, lnk.def_range.start)
}
}
impl DocTooltip {
fn find_document_before(ctx: &mut AnalysisContext, lnk: &DefinitionLink) -> Option<String> {
log::debug!("finding comment at: {:?}, {}", lnk.fid, lnk.def_range.start);
let src = ctx.source_by_id(lnk.fid).ok()?;
let root = LinkedNode::new(src.root());
let leaf = root.leaf_at(lnk.def_range.start + 1)?;
let def_target = get_def_target(leaf.clone())?;
log::info!("found comment target: {:?}", def_target.node().kind());
// collect all comments before the definition
let mut comments = vec![];
// todo: import
let target = def_target.node().clone();
let mut node = def_target.node().clone();
while let Some(prev) = node.prev_sibling() {
node = prev;
if node.kind() == SyntaxKind::Hash {
continue;
}
let start = node.range().end;
let end = target.range().start;
if end <= start {
break;
}
let nodes = node.parent()?.children();
for n in nodes {
let offset = n.offset();
if offset > end || offset < start {
continue;
}
if n.kind() == SyntaxKind::Hash {
continue;
}
if n.kind() == SyntaxKind::LineComment {
// comments.push(n.text().strip_prefix("//")?.trim().to_owned());
// strip all slash prefix
let text = n.text().trim_start_matches('/');
comments.push(text.trim().to_owned());
continue;
}
if n.kind() == SyntaxKind::BlockComment {
let text = n.text();
let mut text = text.strip_prefix("/*")?.strip_suffix("*/")?.trim();
// trip start star
if text.starts_with('*') {
text = text.strip_prefix('*')?.trim();
}
comments.push(text.to_owned());
}
}
break;
}
if comments.is_empty() {
return None;
}
Some(comments.join("\n"))
}
fn builtin_func_tooltip(lnk: &DefinitionLink) -> Option<&'_ str> {
let Some(Value::Func(func)) = &lnk.value else {
return None;

View file

@ -5,15 +5,16 @@ pub use std::{
sync::Arc,
};
pub use ecow::EcoVec;
pub use itertools::{Format, Itertools};
pub use log::{error, trace};
pub use lsp_types::{
request::GotoDeclarationResponse, CodeLens, CompletionResponse, DiagnosticRelatedInformation,
DocumentSymbol, DocumentSymbolResponse, Documentation, FoldingRange, GotoDefinitionResponse,
Hover, InlayHint, Location as LspLocation, LocationLink, MarkupContent, MarkupKind,
Position as LspPosition, PrepareRenameResponse, SelectionRange, SemanticTokens,
SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult, SignatureHelp,
SignatureInformation, SymbolInformation, Url, WorkspaceEdit,
Hover, InlayHint, LanguageString, Location as LspLocation, LocationLink, MarkedString,
MarkupContent, MarkupKind, Position as LspPosition, PrepareRenameResponse, SelectionRange,
SemanticTokens, SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult,
SignatureHelp, SignatureInformation, SymbolInformation, Url, WorkspaceEdit,
};
pub use reflexo::vector::ir::DefId;
pub use serde_json::Value as JsonValue;
@ -22,7 +23,8 @@ pub use typst::foundations::{Func, ParamInfo, Value};
pub use typst::syntax::FileId as TypstFileId;
pub use typst::syntax::{
ast::{self, AstNode},
LinkedNode, Source, Spanned, SyntaxKind,
package::{PackageManifest, PackageSpec},
LinkedNode, Source, Spanned, SyntaxKind, VirtualPath,
};
pub use typst::World;

View file

@ -0,0 +1,98 @@
use std::ops::Range;
use crate::prelude::*;
use crate::syntax::get_def_target;
fn extract_document_between(node: &LinkedNode, rng: Range<usize>) -> Option<String> {
// collect all comments before the definition
let mut comments = vec![];
let mut newline_count = 0;
let nodes = node.parent()?.children();
for n in nodes {
let offset = n.offset();
if !rng.contains(&offset) {
continue;
}
log::debug!("found comment for docs: {:?}: {:?}", n.kind(), n.text());
match n.kind() {
SyntaxKind::Hash => {
newline_count = 0;
}
SyntaxKind::Space => {
if n.text().contains('\n') {
newline_count += 1;
}
if newline_count > 1 {
comments.clear();
}
}
SyntaxKind::Parbreak => {
newline_count = 2;
comments.clear();
}
SyntaxKind::LineComment => {
newline_count = 0;
// comments.push(n.text().strip_prefix("//")?.trim().to_owned());
// strip all slash prefix
let text = n.text().trim_start_matches('/');
comments.push(text.trim().to_owned());
continue;
}
SyntaxKind::BlockComment => {
newline_count = 0;
let text = n.text();
let mut text = text.strip_prefix("/*")?.strip_suffix("*/")?.trim();
// trip start star
if text.starts_with('*') {
text = text.strip_prefix('*')?.trim();
}
comments.push(text.to_owned());
}
_ => {
newline_count = 0;
}
}
}
if comments.is_empty() {
return None;
}
Some(comments.join("\n"))
}
pub fn find_document_before(src: &Source, cursor: usize) -> Option<String> {
log::debug!("finding docs at: {id:?}, {cursor}", id = src.id());
let root = LinkedNode::new(src.root());
let leaf = root.leaf_at(cursor)?;
let def_target = get_def_target(leaf.clone())?;
log::info!("found docs target: {:?}", def_target.node().kind());
// todo: import
let target = def_target.node().clone();
let mut node = target.clone();
while let Some(prev) = node.prev_sibling() {
node = prev;
if node.kind() == SyntaxKind::Hash {
continue;
}
let start = node.range().end;
let end = target.range().start;
if end <= start {
return None;
}
return extract_document_between(&node, start..end);
}
if node.parent()?.range() == root.range() && node.prev_sibling().is_none() {
return extract_document_between(&node, root.offset()..node.range().start);
}
None
}

View file

@ -1,12 +1,3 @@
use std::path::Path;
use ecow::EcoVec;
use typst::syntax::{
ast,
package::{PackageManifest, PackageSpec},
FileId as TypstFileId, LinkedNode, Source, SyntaxKind, VirtualPath,
};
use crate::prelude::*;
fn resolve_id_by_path(

View file

@ -13,6 +13,8 @@ pub(crate) mod matcher;
pub use matcher::*;
pub(crate) mod module;
pub use module::*;
pub(crate) mod comment;
pub use comment::*;
use core::fmt;
use std::ops::Range;

View file

@ -1,11 +1,7 @@
use std::{collections::HashMap, path::Path, sync::Once};
use ecow::EcoVec;
use typst::syntax::{FileId as TypstFileId, VirtualPath};
use crate::prelude::AnalysisContext;
use std::{collections::HashMap, sync::Once};
use super::find_imports;
use crate::prelude::*;
/// The dependency information of a module (file).
pub struct ModuleDependency {

View file

@ -106,10 +106,12 @@ pub fn run_with_sources<T>(source: &str, f: impl FnOnce(&mut TypstSystemWorld, P
if source.starts_with("//") {
let first_line = source.lines().next().unwrap();
source = source.strip_prefix(first_line).unwrap().trim();
let content = first_line.strip_prefix("//").unwrap().trim();
path = content.strip_prefix("path:").map(|e| e.trim().to_owned())
if let Some(path_attr) = content.strip_prefix("path:") {
source = source.strip_prefix(first_line).unwrap().trim();
path = Some(path_attr.trim().to_owned())
}
};
let path = path.unwrap_or_else(|| format!("/s{i}.typ"));