1
1
mirror of https://github.com/oxalica/nil.git synced 2024-11-23 12:03:30 +03:00

Impl hover for names

This commit is contained in:
oxalica 2022-09-22 20:09:14 +08:00
parent fbe5d4d974
commit f94b668d25
7 changed files with 227 additions and 15 deletions

187
crates/ide/src/ide/hover.rs Normal file
View File

@ -0,0 +1,187 @@
use crate::def::{AstPtr, Expr, ResolveResult};
use crate::{DefDatabase, FilePos, NameKind};
use builtin::ALL_BUILTINS;
use rowan::ast::AstNode;
use rowan::TextRange;
use std::fmt::Write;
use syntax::{ast, best_token_at_offset, match_ast};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HoverResult {
pub range: TextRange,
pub markup: String,
}
pub(crate) fn hover(
db: &dyn DefDatabase,
FilePos { file_id, pos }: FilePos,
) -> Option<HoverResult> {
let parse = db.parse(file_id);
let tok = best_token_at_offset(&parse.syntax_node(), pos)?;
let ptr = tok.parent_ancestors().find_map(|node| {
match_ast! {
match node {
ast::Ref(n) => Some(AstPtr::new(n.syntax())),
ast::Name(n) => Some(AstPtr::new(n.syntax())),
ast::Literal(n) => Some(AstPtr::new(n.syntax())),
_ => None,
}
}
})?;
let range = ptr.text_range();
let src = db.file_content(file_id);
let module = db.module(file_id);
let source_map = db.source_map(file_id);
let nameres = db.name_resolution(file_id);
let mut name = None;
if let Some(expr) = source_map.expr_for_node(ptr.clone()) {
match nameres.get(expr) {
None => {}
Some(ResolveResult::Builtin(name)) => {
let b = &ALL_BUILTINS[*name];
let mut markup = String::new();
match b.summary {
None => writeln!(markup, "`builtins.{name}`").unwrap(),
Some(summary) => writeln!(markup, "`{summary}`").unwrap(),
}
if let Some(doc) = b.doc {
writeln!(markup, "\n{doc}").unwrap();
}
return Some(HoverResult { range, markup });
}
Some(ResolveResult::WithExprs(withs)) => {
let text = match &module[expr] {
Expr::Reference(text) => text,
_ => return None,
};
let mut markup = format!("Attribute `{}` from:", text);
for (&expr, i) in withs.iter().zip(1..) {
let ptr = source_map.node_for_expr(expr)?;
let with_node = ast::With::cast(ptr.to_node(&parse.syntax_node()))?;
let env_text = with_node
.environment()
.map_or("?", |env_node| &src[env_node.syntax().text_range()]);
write!(markup, "\n{i}. `with {env_text};`").unwrap();
}
return Some(HoverResult { range, markup });
}
Some(ResolveResult::Definition(def)) => {
name = Some(*def);
}
}
}
if let Some(name) = name.or_else(|| source_map.name_for_node(ptr.clone())) {
let text = &module[name].text;
let kind = match module[name].kind {
NameKind::LetIn => "Let binding",
NameKind::PlainAttrset => "Attrset attribute",
NameKind::RecAttrset => "Rec-attrset attribute",
NameKind::Param => "Parameter",
NameKind::PatField => "Field parameter",
};
return Some(HoverResult {
range,
markup: format!("{kind} `{text}`"),
});
}
None
}
#[cfg(test)]
mod tests {
use crate::base::SourceDatabase;
use crate::tests::TestDB;
use expect_test::{expect, Expect};
#[track_caller]
fn check(fixture: &str, full: &str, expect: Expect) {
let (db, f) = TestDB::from_fixture(fixture).unwrap();
assert_eq!(f.markers().len(), 1);
let ret = super::hover(&db, f[0]).expect("No hover");
let src = db.file_content(f[0].file_id);
assert_eq!(full, &src[ret.range]);
let mut got = ret.markup.trim().to_string();
if got.contains('\n') {
got += "\n";
}
expect.assert_eq(&got);
}
#[test]
fn definition() {
check("let $0a = 1; in a", "a", expect!["Let binding `a`"]);
check("let a.$0a = 1; in a", "a", expect!["Attrset attribute `a`"]);
check("{ $0a = 1; }", "a", expect!["Attrset attribute `a`"]);
check(
"rec { $0a = 1; }",
"a",
expect!["Rec-attrset attribute `a`"],
);
check("$0a: a", "a", expect!["Parameter `a`"]);
check("{$0a}: a", "a", expect!["Field parameter `a`"]);
}
#[test]
fn reference() {
check("let a = 1; in $0a", "a", expect!["Let binding `a`"]);
check(
"let a = 1; in { inherit $0a; }",
"a",
expect!["Let binding `a`"],
);
check(
"let a = 1; in rec { inherit $0a; }",
"a",
expect!["Let binding `a`"],
);
check("a: $0a", "a", expect!["Parameter `a`"]);
check("{a}: $0a", "a", expect!["Field parameter `a`"]);
}
#[test]
fn with() {
check(
"with 1; $0a",
"a",
expect![[r#"
Attribute `a` from:
1. `with 1;`
"#]],
);
check(
"with 1; with 2; $0a",
"a",
expect![[r#"
Attribute `a` from:
1. `with 2;`
2. `with 1;`
"#]],
);
}
#[test]
fn builtin() {
check("$0true", "true", expect!["`builtins.true`"]);
check(
"$0map",
"map",
expect![[r#"
`builtins.map f list`
Apply the function *f* to each element in the list *list*. For
example,
```nix
map (x: "foo" + x) [ "bar" "bla" "abc" ]
```
evaluates to `[ "foobar" "foobla" "fooabc" ]`.
"#]],
);
}
}

View File

@ -2,6 +2,7 @@ mod completion;
mod diagnostics;
mod expand_selection;
mod goto_definition;
mod hover;
mod references;
mod rename;
mod syntax_highlighting;
@ -15,6 +16,7 @@ use smol_str::SmolStr;
use std::fmt;
pub use completion::{CompletionItem, CompletionItemKind};
pub use hover::HoverResult;
pub use syntax_highlighting::{HlKeyword, HlOperator, HlPunct, HlRange, HlTag};
#[derive(Debug, Clone, PartialEq, Eq)]
@ -129,4 +131,8 @@ impl Analysis {
) -> Cancellable<RenameResult<WorkspaceEdit>> {
self.with_db(|db| rename::rename(db, fpos, new_name))
}
pub fn hover(&self, fpos: FilePos) -> Cancellable<Option<HoverResult>> {
self.with_db(|db| hover::hover(db, fpos))
}
}

View File

@ -9,7 +9,7 @@ mod tests;
pub use self::ide::{
Analysis, AnalysisHost, Cancelled, CompletionItem, CompletionItemKind, HlKeyword, HlOperator,
HlPunct, HlRange, HlTag, NavigationTarget, RootDatabase,
HlPunct, HlRange, HlTag, HoverResult, NavigationTarget, RootDatabase,
};
pub use base::{
Change, FileId, FilePos, FileRange, FileSet, InFile, SourceDatabase, SourceRoot, SourceRootId,

View File

@ -1,9 +1,10 @@
use crate::semantic_tokens::{SEMANTIC_TOKEN_MODIFIERS, SEMANTIC_TOKEN_TYPES};
use lsp_types::{
CompletionOptions, OneOf, RenameOptions, SelectionRangeProviderCapability,
SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions,
SemanticTokensServerCapabilities, ServerCapabilities, TextDocumentSyncCapability,
TextDocumentSyncKind, TextDocumentSyncOptions, WorkDoneProgressOptions,
CompletionOptions, HoverProviderCapability, OneOf, RenameOptions,
SelectionRangeProviderCapability, SemanticTokensFullOptions, SemanticTokensLegend,
SemanticTokensOptions, SemanticTokensServerCapabilities, ServerCapabilities,
TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
WorkDoneProgressOptions,
};
pub(crate) fn server_capabilities() -> ServerCapabilities {
@ -39,6 +40,7 @@ pub(crate) fn server_capabilities() -> ServerCapabilities {
full: Some(SemanticTokensFullOptions::Delta { delta: Some(false) }),
},
)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
..Default::default()
}
}

View File

@ -1,16 +1,15 @@
use std::sync::Arc;
use crate::{semantic_tokens, LineMap, LspError, Result, Vfs};
use ide::{
CompletionItem, CompletionItemKind, Diagnostic, FileId, FilePos, FileRange, HlRange, Severity,
TextEdit, WorkspaceEdit,
CompletionItem, CompletionItemKind, Diagnostic, FileId, FilePos, FileRange, HlRange,
HoverResult, Severity, TextEdit, WorkspaceEdit,
};
use lsp_server::ErrorCode;
use lsp_types::{
self as lsp, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Documentation,
Location, MarkupContent, MarkupKind, Position, PrepareRenameResponse, Range, SemanticToken,
TextDocumentIdentifier, TextDocumentPositionParams,
Hover, Location, MarkupContent, MarkupKind, Position, PrepareRenameResponse, Range,
SemanticToken, TextDocumentIdentifier, TextDocumentPositionParams,
};
use std::sync::Arc;
use text_size::{TextRange, TextSize};
pub(crate) fn from_file(vfs: &Vfs, doc: &TextDocumentIdentifier) -> Result<FileId> {
@ -268,3 +267,13 @@ pub(crate) fn to_semantic_tokens(line_map: &LineMap, hls: &[HlRange]) -> Vec<Sem
toks
}
pub(crate) fn to_hover(line_map: &LineMap, hover: HoverResult) -> Hover {
Hover {
range: Some(to_range(line_map, hover.range)),
contents: lsp::HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: hover.markup,
}),
}
}

View File

@ -1,10 +1,10 @@
use crate::{convert, Result, StateSnapshot};
use ide::FileRange;
use lsp_types::{
CompletionParams, CompletionResponse, GotoDefinitionParams, GotoDefinitionResponse, Location,
PrepareRenameResponse, ReferenceParams, RenameParams, SelectionRange, SelectionRangeParams,
SemanticTokens, SemanticTokensParams, SemanticTokensRangeParams, SemanticTokensRangeResult,
SemanticTokensResult, TextDocumentPositionParams, WorkspaceEdit,
CompletionParams, CompletionResponse, GotoDefinitionParams, GotoDefinitionResponse, Hover,
HoverParams, Location, PrepareRenameResponse, ReferenceParams, RenameParams, SelectionRange,
SelectionRangeParams, SemanticTokens, SemanticTokensParams, SemanticTokensRangeParams,
SemanticTokensRangeResult, SemanticTokensResult, TextDocumentPositionParams, WorkspaceEdit,
};
use text_size::TextRange;
@ -145,3 +145,10 @@ pub(crate) fn semantic_token_range(
data: toks,
})))
}
pub(crate) fn hover(snap: StateSnapshot, params: HoverParams) -> Result<Option<Hover>> {
let (line_map, fpos) =
convert::from_file_pos(&snap.vfs(), &params.text_document_position_params)?;
let ret = snap.analysis.hover(fpos)?;
Ok(ret.map(|hover| convert::to_hover(&line_map, hover)))
}

View File

@ -121,6 +121,7 @@ impl State {
.on::<req::Rename>(handler::rename)
.on::<req::SemanticTokensFullRequest>(handler::semantic_token_full)
.on::<req::SemanticTokensRangeRequest>(handler::semantic_token_range)
.on::<req::HoverRequest>(handler::hover)
.finish();
}