Various hover improvements (#5617)
* Show more info on hover for variables Signed-off-by: Nick Cameron <nrc@ncameron.org> * Move hover impls to lsp module Signed-off-by: Nick Cameron <nrc@ncameron.org> * Make hover work on names inside calls, fix doc line breaking, trim docs in tool tips Signed-off-by: Nick Cameron <nrc@ncameron.org> * Test the new hovers; fix signature syntax Signed-off-by: Nick Cameron <nrc@ncameron.org> * Hover tips for kwargs Signed-off-by: Nick Cameron <nrc@ncameron.org> --------- Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
379
rust/kcl-lib/src/lsp/kcl/hover.rs
Normal file
379
rust/kcl-lib/src/lsp/kcl/hover.rs
Normal file
@ -0,0 +1,379 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tower_lsp::lsp_types::Range as LspRange;
|
||||
|
||||
use crate::{parsing::ast::types::*, SourceRange};
|
||||
|
||||
/// Describes information about a hover.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) enum Hover {
|
||||
Function {
|
||||
name: String,
|
||||
range: LspRange,
|
||||
},
|
||||
Signature {
|
||||
name: String,
|
||||
parameter_index: u32,
|
||||
range: LspRange,
|
||||
},
|
||||
Comment {
|
||||
value: String,
|
||||
range: LspRange,
|
||||
},
|
||||
Variable {
|
||||
name: String,
|
||||
ty: Option<String>,
|
||||
range: LspRange,
|
||||
},
|
||||
KwArg {
|
||||
name: String,
|
||||
callee_name: String,
|
||||
range: LspRange,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct HoverOpts {
|
||||
vars: Option<HashMap<String, Option<String>>>,
|
||||
prefer_sig: bool,
|
||||
}
|
||||
|
||||
impl HoverOpts {
|
||||
pub fn default_for_signature_help() -> Self {
|
||||
HoverOpts {
|
||||
vars: None,
|
||||
prefer_sig: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_for_hover() -> Self {
|
||||
HoverOpts {
|
||||
vars: None,
|
||||
prefer_sig: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Program {
|
||||
/// Returns a hover value that includes the given character position.
|
||||
/// This is really recursive so keep that in mind.
|
||||
pub(super) fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
// Check if we are in shebang.
|
||||
if let Some(node) = &self.shebang {
|
||||
if node.contains(pos) {
|
||||
let source_range: SourceRange = node.into();
|
||||
return Some(Hover::Comment {
|
||||
value: r#"The `#!` at the start of a script, known as a shebang, specifies the path to the interpreter that should execute the script. This line is not necessary for your `kcl` to run in the modeling-app. You can safely delete it. If you wish to learn more about what you _can_ do with a shebang, read this doc: [zoo.dev/docs/faq/shebang](https://zoo.dev/docs/faq/shebang)."#.to_string(),
|
||||
range: source_range.to_lsp_range(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let value = self.get_expr_for_position(pos)?;
|
||||
|
||||
value.get_hover_value_for_position(pos, code, opts)
|
||||
}
|
||||
}
|
||||
|
||||
impl Expr {
|
||||
pub(super) fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
match self {
|
||||
Expr::BinaryExpression(binary_expression) => {
|
||||
binary_expression.get_hover_value_for_position(pos, code, opts)
|
||||
}
|
||||
Expr::FunctionExpression(function_expression) => {
|
||||
function_expression.get_hover_value_for_position(pos, code, opts)
|
||||
}
|
||||
Expr::CallExpression(call_expression) => call_expression.get_hover_value_for_position(pos, code, opts),
|
||||
Expr::CallExpressionKw(call_expression) => call_expression.get_hover_value_for_position(pos, code, opts),
|
||||
Expr::PipeExpression(pipe_expression) => pipe_expression.get_hover_value_for_position(pos, code, opts),
|
||||
Expr::ArrayExpression(array_expression) => array_expression.get_hover_value_for_position(pos, code, opts),
|
||||
Expr::ArrayRangeExpression(array_range) => array_range.get_hover_value_for_position(pos, code, opts),
|
||||
Expr::ObjectExpression(object_expression) => {
|
||||
object_expression.get_hover_value_for_position(pos, code, opts)
|
||||
}
|
||||
Expr::MemberExpression(member_expression) => {
|
||||
member_expression.get_hover_value_for_position(pos, code, opts)
|
||||
}
|
||||
Expr::UnaryExpression(unary_expression) => unary_expression.get_hover_value_for_position(pos, code, opts),
|
||||
Expr::IfExpression(expr) => expr.get_hover_value_for_position(pos, code, opts),
|
||||
// TODO: LSP hover information for values/types. https://github.com/KittyCAD/modeling-app/issues/1126
|
||||
Expr::None(_) => None,
|
||||
Expr::Literal(_) => None,
|
||||
Expr::Identifier(id) => {
|
||||
if id.contains(pos) {
|
||||
let name = id.name.clone();
|
||||
Some(Hover::Variable {
|
||||
ty: opts
|
||||
.vars
|
||||
.as_ref()
|
||||
.and_then(|vars| vars.get(&name).and_then(Clone::clone)),
|
||||
name,
|
||||
range: id.as_source_range().to_lsp_range(code),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Expr::TagDeclarator(_) => None,
|
||||
// TODO LSP hover info for tag
|
||||
Expr::LabelledExpression(expr) => expr.expr.get_hover_value_for_position(pos, code, opts),
|
||||
// TODO LSP hover info for type
|
||||
Expr::AscribedExpression(expr) => expr.expr.get_hover_value_for_position(pos, code, opts),
|
||||
// TODO: LSP hover information for symbols. https://github.com/KittyCAD/modeling-app/issues/1127
|
||||
Expr::PipeSubstitution(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BinaryPart {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
match self {
|
||||
BinaryPart::Literal(_literal) => None,
|
||||
BinaryPart::Identifier(_identifier) => None,
|
||||
BinaryPart::BinaryExpression(binary_expression) => {
|
||||
binary_expression.get_hover_value_for_position(pos, code, opts)
|
||||
}
|
||||
BinaryPart::CallExpression(call_expression) => {
|
||||
call_expression.get_hover_value_for_position(pos, code, opts)
|
||||
}
|
||||
BinaryPart::CallExpressionKw(call_expression) => {
|
||||
call_expression.get_hover_value_for_position(pos, code, opts)
|
||||
}
|
||||
BinaryPart::UnaryExpression(unary_expression) => {
|
||||
unary_expression.get_hover_value_for_position(pos, code, opts)
|
||||
}
|
||||
BinaryPart::IfExpression(e) => e.get_hover_value_for_position(pos, code, opts),
|
||||
BinaryPart::MemberExpression(member_expression) => {
|
||||
member_expression.get_hover_value_for_position(pos, code, opts)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CallExpression {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
let callee_source_range: SourceRange = self.callee.clone().into();
|
||||
if callee_source_range.contains(pos) {
|
||||
return Some(Hover::Function {
|
||||
name: self.callee.name.clone(),
|
||||
range: callee_source_range.to_lsp_range(code),
|
||||
});
|
||||
}
|
||||
|
||||
for (index, arg) in self.arguments.iter().enumerate() {
|
||||
let source_range: SourceRange = arg.into();
|
||||
if source_range.contains(pos) {
|
||||
return if opts.prefer_sig {
|
||||
Some(Hover::Signature {
|
||||
name: self.callee.name.clone(),
|
||||
parameter_index: index as u32,
|
||||
range: source_range.to_lsp_range(code),
|
||||
})
|
||||
} else {
|
||||
arg.get_hover_value_for_position(pos, code, opts)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl CallExpressionKw {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
let callee_source_range: SourceRange = self.callee.clone().into();
|
||||
if callee_source_range.contains(pos) {
|
||||
return Some(Hover::Function {
|
||||
name: self.callee.name.clone(),
|
||||
range: callee_source_range.to_lsp_range(code),
|
||||
});
|
||||
}
|
||||
|
||||
for (index, (label, arg)) in self.iter_arguments().enumerate() {
|
||||
let source_range: SourceRange = arg.into();
|
||||
if source_range.contains(pos) {
|
||||
return if opts.prefer_sig {
|
||||
Some(Hover::Signature {
|
||||
name: self.callee.name.clone(),
|
||||
parameter_index: index as u32,
|
||||
range: source_range.to_lsp_range(code),
|
||||
})
|
||||
} else {
|
||||
arg.get_hover_value_for_position(pos, code, opts)
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(id) = label {
|
||||
if id.as_source_range().contains(pos) {
|
||||
return Some(Hover::KwArg {
|
||||
name: id.name.clone(),
|
||||
callee_name: self.callee.name.clone(),
|
||||
range: id.as_source_range().to_lsp_range(code),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl ArrayExpression {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
for element in &self.elements {
|
||||
let element_source_range: SourceRange = element.into();
|
||||
if element_source_range.contains(pos) {
|
||||
return element.get_hover_value_for_position(pos, code, opts);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl ArrayRangeExpression {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
for element in [&self.start_element, &self.end_element] {
|
||||
let element_source_range: SourceRange = element.into();
|
||||
if element_source_range.contains(pos) {
|
||||
return element.get_hover_value_for_position(pos, code, opts);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectExpression {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
for property in &self.properties {
|
||||
let property_source_range: SourceRange = property.into();
|
||||
if property_source_range.contains(pos) {
|
||||
return property.get_hover_value_for_position(pos, code, opts);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectProperty {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
let value_source_range: SourceRange = self.value.clone().into();
|
||||
if value_source_range.contains(pos) {
|
||||
return self.value.get_hover_value_for_position(pos, code, opts);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl MemberObject {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
match self {
|
||||
MemberObject::MemberExpression(member_expression) => {
|
||||
member_expression.get_hover_value_for_position(pos, code, opts)
|
||||
}
|
||||
MemberObject::Identifier(_identifier) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MemberExpression {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
let object_source_range: SourceRange = self.object.clone().into();
|
||||
if object_source_range.contains(pos) {
|
||||
return self.object.get_hover_value_for_position(pos, code, opts);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl BinaryExpression {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
let left_source_range: SourceRange = self.left.clone().into();
|
||||
let right_source_range: SourceRange = self.right.clone().into();
|
||||
|
||||
if left_source_range.contains(pos) {
|
||||
return self.left.get_hover_value_for_position(pos, code, opts);
|
||||
}
|
||||
|
||||
if right_source_range.contains(pos) {
|
||||
return self.right.get_hover_value_for_position(pos, code, opts);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl UnaryExpression {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
let argument_source_range: SourceRange = self.argument.clone().into();
|
||||
if argument_source_range.contains(pos) {
|
||||
return self.argument.get_hover_value_for_position(pos, code, opts);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl PipeExpression {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
for b in &self.body {
|
||||
let b_source_range: SourceRange = b.into();
|
||||
if b_source_range.contains(pos) {
|
||||
return b.get_hover_value_for_position(pos, code, opts);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl FunctionExpression {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
if let Some(value) = self.body.get_expr_for_position(pos) {
|
||||
let mut vars = opts.vars.clone().unwrap_or_default();
|
||||
for arg in &self.params {
|
||||
let ty = arg.type_.as_ref().map(|ty| ty.recast(&FormatOptions::default(), 0));
|
||||
vars.insert(arg.identifier.inner.name.clone(), ty);
|
||||
}
|
||||
return value.get_hover_value_for_position(
|
||||
pos,
|
||||
code,
|
||||
&HoverOpts {
|
||||
vars: Some(vars),
|
||||
prefer_sig: opts.prefer_sig,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl IfExpression {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
self.cond
|
||||
.get_hover_value_for_position(pos, code, opts)
|
||||
.or_else(|| self.then_val.get_hover_value_for_position(pos, code, opts))
|
||||
.or_else(|| {
|
||||
self.else_ifs
|
||||
.iter()
|
||||
.find_map(|else_if| else_if.get_hover_value_for_position(pos, code, opts))
|
||||
})
|
||||
.or_else(|| self.final_else.get_hover_value_for_position(pos, code, opts))
|
||||
}
|
||||
}
|
||||
|
||||
impl ElseIf {
|
||||
fn get_hover_value_for_position(&self, pos: usize, code: &str, opts: &HoverOpts) -> Option<Hover> {
|
||||
self.cond
|
||||
.get_hover_value_for_position(pos, code, opts)
|
||||
.or_else(|| self.then_val.get_hover_value_for_position(pos, code, opts))
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user