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))
|
||||
}
|
||||
}
|
@ -8,15 +8,12 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub mod custom_notifications;
|
||||
|
||||
use anyhow::Result;
|
||||
#[cfg(feature = "cli")]
|
||||
use clap::Parser;
|
||||
use dashmap::DashMap;
|
||||
use sha2::Digest;
|
||||
use tokio::sync::RwLock;
|
||||
use tower_lsp::{
|
||||
jsonrpc::Result as RpcResult,
|
||||
lsp_types::{
|
||||
@ -28,7 +25,7 @@ use tower_lsp::{
|
||||
DidSaveTextDocumentParams, DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportResult,
|
||||
DocumentFilter, DocumentFormattingParams, DocumentSymbol, DocumentSymbolParams, DocumentSymbolResponse,
|
||||
Documentation, FoldingRange, FoldingRangeParams, FoldingRangeProviderCapability, FullDocumentDiagnosticReport,
|
||||
Hover, HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult,
|
||||
Hover as LspHover, HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult,
|
||||
InitializedParams, InlayHint, InlayHintParams, InsertTextFormat, MarkupContent, MarkupKind, MessageType, OneOf,
|
||||
Position, RelatedFullDocumentDiagnosticReport, RenameFilesParams, RenameParams, SemanticToken,
|
||||
SemanticTokenModifier, SemanticTokenType, SemanticTokens, SemanticTokensFullOptions, SemanticTokensLegend,
|
||||
@ -44,7 +41,13 @@ use tower_lsp::{
|
||||
use crate::{
|
||||
docs::kcl_doc::DocData,
|
||||
errors::Suggestion,
|
||||
lsp::{backend::Backend as _, util::IntoDiagnostic},
|
||||
exec::KclValue,
|
||||
execution::{cache, kcl_value::FunctionSource},
|
||||
lsp::{
|
||||
backend::Backend as _,
|
||||
kcl::hover::{Hover, HoverOpts},
|
||||
util::IntoDiagnostic,
|
||||
},
|
||||
parsing::{
|
||||
ast::types::{Expr, VariableKind},
|
||||
token::TokenStream,
|
||||
@ -52,6 +55,10 @@ use crate::{
|
||||
},
|
||||
ModuleId, Program, SourceRange,
|
||||
};
|
||||
|
||||
pub mod custom_notifications;
|
||||
mod hover;
|
||||
|
||||
const SEMANTIC_TOKEN_TYPES: [SemanticTokenType; 10] = [
|
||||
SemanticTokenType::NUMBER,
|
||||
SemanticTokenType::VARIABLE,
|
||||
@ -99,6 +106,8 @@ pub struct Backend {
|
||||
pub stdlib_completions: HashMap<String, CompletionItem>,
|
||||
/// The stdlib signatures for the language.
|
||||
pub stdlib_signatures: HashMap<String, SignatureHelp>,
|
||||
/// For all KwArg functions in std, a map from their arg names to arg help snippets (markdown format).
|
||||
pub stdlib_args: HashMap<String, HashMap<String, String>>,
|
||||
/// Token maps.
|
||||
pub(super) token_map: DashMap<String, TokenStream>,
|
||||
/// AST maps.
|
||||
@ -168,12 +177,14 @@ impl Backend {
|
||||
let kcl_std = crate::docs::kcl_doc::walk_prelude();
|
||||
let stdlib_completions = get_completions_from_stdlib(&stdlib, &kcl_std).map_err(|e| e.to_string())?;
|
||||
let stdlib_signatures = get_signatures_from_stdlib(&stdlib, &kcl_std);
|
||||
let stdlib_args = get_arg_maps_from_stdlib(&stdlib, &kcl_std);
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
fs: Arc::new(fs),
|
||||
stdlib_completions,
|
||||
stdlib_signatures,
|
||||
stdlib_args,
|
||||
zoo_client,
|
||||
can_send_telemetry,
|
||||
can_execute: Arc::new(RwLock::new(executor_ctx.is_some())),
|
||||
@ -1010,7 +1021,7 @@ impl LanguageServer for Backend {
|
||||
}
|
||||
}
|
||||
|
||||
async fn hover(&self, params: HoverParams) -> RpcResult<Option<Hover>> {
|
||||
async fn hover(&self, params: HoverParams) -> RpcResult<Option<LspHover>> {
|
||||
let filename = params.text_document_position_params.text_document.uri.to_string();
|
||||
|
||||
let Some(current_code) = self.code_map.get(&filename) else {
|
||||
@ -1027,48 +1038,128 @@ impl LanguageServer for Backend {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some(hover) = ast.ast.get_hover_value_for_position(pos, current_code) else {
|
||||
let Some(hover) = ast
|
||||
.ast
|
||||
.get_hover_value_for_position(pos, current_code, &HoverOpts::default_for_hover())
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
match hover {
|
||||
crate::parsing::ast::types::Hover::Function { name, range } => {
|
||||
// Get the docs for this function.
|
||||
let Some(completion) = self.stdlib_completions.get(&name) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(docs) = &completion.documentation else {
|
||||
return Ok(None);
|
||||
Hover::Function { name, range } => {
|
||||
let (sig, docs) = if let Some(Some(result)) = with_cached_var(&name, |value| {
|
||||
match value {
|
||||
// User-defined or KCL std function
|
||||
KclValue::Function {
|
||||
value: FunctionSource::User { ast, .. },
|
||||
..
|
||||
} => {
|
||||
// TODO get docs from comments
|
||||
Some((ast.signature(), ""))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.await
|
||||
{
|
||||
result
|
||||
} else {
|
||||
// Get the docs for this function.
|
||||
let Some(completion) = self.stdlib_completions.get(&name) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(docs) = &completion.documentation else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let docs = match docs {
|
||||
Documentation::String(docs) => docs,
|
||||
Documentation::MarkupContent(MarkupContent { value, .. }) => value,
|
||||
};
|
||||
|
||||
let docs = if docs.len() > 320 {
|
||||
let end = docs.find("\n\n").or_else(|| docs.find("\n\r\n")).unwrap_or(320);
|
||||
&docs[..end]
|
||||
} else {
|
||||
&**docs
|
||||
};
|
||||
|
||||
let Some(label_details) = &completion.label_details else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let sig = if let Some(detail) = &label_details.detail {
|
||||
detail.clone()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
(sig, docs)
|
||||
};
|
||||
|
||||
let docs = match docs {
|
||||
Documentation::String(docs) => docs,
|
||||
Documentation::MarkupContent(MarkupContent { value, .. }) => value,
|
||||
};
|
||||
|
||||
let Some(label_details) = &completion.label_details else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(Hover {
|
||||
Ok(Some(LspHover {
|
||||
contents: HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: format!(
|
||||
"```\n{}{}\n```\n\n{}",
|
||||
name,
|
||||
if let Some(detail) = &label_details.detail {
|
||||
detail
|
||||
} else {
|
||||
""
|
||||
},
|
||||
docs
|
||||
),
|
||||
value: format!("```\n{}{}\n```\n\n{}", name, sig, docs),
|
||||
}),
|
||||
range: Some(range),
|
||||
}))
|
||||
}
|
||||
crate::parsing::ast::types::Hover::Signature { .. } => Ok(None),
|
||||
crate::parsing::ast::types::Hover::Comment { value, range } => Ok(Some(Hover {
|
||||
Hover::KwArg {
|
||||
name,
|
||||
callee_name,
|
||||
range,
|
||||
} => {
|
||||
// TODO handle user-defined functions too
|
||||
|
||||
let Some(arg_map) = self.stdlib_args.get(&callee_name) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some(tip) = arg_map.get(&name) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(LspHover {
|
||||
contents: HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: tip.clone(),
|
||||
}),
|
||||
range: Some(range),
|
||||
}))
|
||||
}
|
||||
Hover::Variable {
|
||||
name,
|
||||
ty: Some(ty),
|
||||
range,
|
||||
} => Ok(Some(LspHover {
|
||||
contents: HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: format!("```\n{}: {}\n```", name, ty),
|
||||
}),
|
||||
range: Some(range),
|
||||
})),
|
||||
Hover::Variable { name, ty: None, range } => Ok(with_cached_var(&name, |value| {
|
||||
let mut text: String = format!("```\n{}", name);
|
||||
if let Some(ty) = value.principal_type() {
|
||||
text.push_str(&format!(": {}", ty));
|
||||
}
|
||||
if let Some(v) = value.value_str() {
|
||||
text.push_str(&format!(" = {}", v));
|
||||
}
|
||||
text.push_str("\n```");
|
||||
|
||||
LspHover {
|
||||
contents: HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: text,
|
||||
}),
|
||||
range: Some(range),
|
||||
}
|
||||
})
|
||||
.await),
|
||||
Hover::Signature { .. } => Ok(None),
|
||||
Hover::Comment { value, range } => Ok(Some(LspHover {
|
||||
contents: HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value,
|
||||
@ -1221,12 +1312,14 @@ impl LanguageServer for Backend {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some(hover) = value.get_hover_value_for_position(pos, current_code) else {
|
||||
let Some(hover) =
|
||||
value.get_hover_value_for_position(pos, current_code, &HoverOpts::default_for_signature_help())
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
match hover {
|
||||
crate::parsing::ast::types::Hover::Function { name, range: _ } => {
|
||||
Hover::Function { name, range: _ } => {
|
||||
// Get the docs for this function.
|
||||
let Some(signature) = self.stdlib_signatures.get(&name) else {
|
||||
return Ok(None);
|
||||
@ -1234,7 +1327,7 @@ impl LanguageServer for Backend {
|
||||
|
||||
Ok(Some(signature.clone()))
|
||||
}
|
||||
crate::parsing::ast::types::Hover::Signature {
|
||||
Hover::Signature {
|
||||
name,
|
||||
parameter_index,
|
||||
range: _,
|
||||
@ -1249,7 +1342,7 @@ impl LanguageServer for Backend {
|
||||
|
||||
Ok(Some(signature))
|
||||
}
|
||||
crate::parsing::ast::types::Hover::Comment { value: _, range: _ } => {
|
||||
_ => {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
@ -1452,6 +1545,50 @@ pub fn get_signatures_from_stdlib(stdlib: &crate::std::StdLib, kcl_std: &[DocDat
|
||||
signatures
|
||||
}
|
||||
|
||||
/// Get signatures from our stdlib.
|
||||
pub fn get_arg_maps_from_stdlib(
|
||||
stdlib: &crate::std::StdLib,
|
||||
kcl_std: &[DocData],
|
||||
) -> HashMap<String, HashMap<String, String>> {
|
||||
let mut result = HashMap::new();
|
||||
let combined = stdlib.combined();
|
||||
|
||||
for internal_fn in combined.values() {
|
||||
if internal_fn.keyword_arguments() {
|
||||
let arg_map: HashMap<String, String> = internal_fn
|
||||
.args(false)
|
||||
.into_iter()
|
||||
.map(|data| {
|
||||
let mut tip = "```\n".to_owned();
|
||||
tip.push_str(&data.name.clone());
|
||||
if !data.required {
|
||||
tip.push('?');
|
||||
}
|
||||
if !data.type_.is_empty() {
|
||||
tip.push_str(": ");
|
||||
tip.push_str(&data.type_);
|
||||
}
|
||||
tip.push_str("\n```");
|
||||
if !data.description.is_empty() {
|
||||
tip.push_str("\n\n");
|
||||
tip.push_str(&data.description);
|
||||
}
|
||||
(data.name, tip)
|
||||
})
|
||||
.collect();
|
||||
if !arg_map.is_empty() {
|
||||
result.insert(internal_fn.name(), arg_map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _d in kcl_std {
|
||||
// TODO add KCL std fns
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Convert a position to a character index from the start of the file.
|
||||
fn position_to_char_index(position: Position, code: &str) -> usize {
|
||||
// Get the character position from the start of the file.
|
||||
@ -1467,3 +1604,11 @@ fn position_to_char_index(position: Position, code: &str) -> usize {
|
||||
|
||||
char_position
|
||||
}
|
||||
|
||||
async fn with_cached_var<T>(name: &str, f: impl Fn(&KclValue) -> T) -> Option<T> {
|
||||
let result_env = cache::read_old_ast().await?.result_env;
|
||||
let mem = cache::read_old_memory().await?;
|
||||
let value = mem.get_from(name, result_env, SourceRange::default()).ok()?;
|
||||
|
||||
Some(f(value))
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ pub async fn kcl_lsp_server(execute: bool) -> Result<crate::lsp::kcl::Backend> {
|
||||
let kcl_std = crate::docs::kcl_doc::walk_prelude();
|
||||
let stdlib_completions = crate::lsp::kcl::get_completions_from_stdlib(&stdlib, &kcl_std)?;
|
||||
let stdlib_signatures = crate::lsp::kcl::get_signatures_from_stdlib(&stdlib, &kcl_std);
|
||||
let stdlib_args = crate::lsp::kcl::get_arg_maps_from_stdlib(&stdlib, &kcl_std);
|
||||
|
||||
let zoo_client = crate::engine::new_zoo_client(None, None)?;
|
||||
|
||||
@ -19,6 +20,7 @@ pub async fn kcl_lsp_server(execute: bool) -> Result<crate::lsp::kcl::Backend> {
|
||||
};
|
||||
|
||||
let can_execute = executor_ctx.is_some();
|
||||
assert!(!execute || can_execute);
|
||||
|
||||
// Create the backend.
|
||||
let (service, _) = tower_lsp::LspService::build(|client| crate::lsp::kcl::Backend {
|
||||
@ -27,6 +29,7 @@ pub async fn kcl_lsp_server(execute: bool) -> Result<crate::lsp::kcl::Backend> {
|
||||
workspace_folders: Default::default(),
|
||||
stdlib_completions,
|
||||
stdlib_signatures,
|
||||
stdlib_args,
|
||||
token_map: Default::default(),
|
||||
ast_map: Default::default(),
|
||||
code_map: Default::default(),
|
||||
|
@ -60,7 +60,7 @@ async fn test_updating_kcl_lsp_files() {
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(server.code_map.len(), 10);
|
||||
assert_eq!(server.code_map.len(), 11);
|
||||
|
||||
// Run open file.
|
||||
server
|
||||
@ -75,7 +75,7 @@ async fn test_updating_kcl_lsp_files() {
|
||||
.await;
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 11);
|
||||
assert_eq!(server.code_map.len(), 12);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -91,7 +91,7 @@ async fn test_updating_kcl_lsp_files() {
|
||||
.await;
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 11);
|
||||
assert_eq!(server.code_map.len(), 12);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -110,7 +110,7 @@ async fn test_updating_kcl_lsp_files() {
|
||||
.await;
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 12);
|
||||
assert_eq!(server.code_map.len(), 13);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -136,7 +136,7 @@ async fn test_updating_kcl_lsp_files() {
|
||||
.await;
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 12);
|
||||
assert_eq!(server.code_map.len(), 13);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -157,7 +157,7 @@ async fn test_updating_kcl_lsp_files() {
|
||||
.await;
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 12);
|
||||
assert_eq!(server.code_map.len(), 13);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -177,7 +177,7 @@ async fn test_updating_kcl_lsp_files() {
|
||||
.await;
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 13);
|
||||
assert_eq!(server.code_map.len(), 14);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -198,7 +198,7 @@ async fn test_updating_kcl_lsp_files() {
|
||||
.await;
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 12);
|
||||
assert_eq!(server.code_map.len(), 13);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -232,7 +232,7 @@ async fn test_updating_kcl_lsp_files() {
|
||||
);
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 12);
|
||||
assert_eq!(server.code_map.len(), 13);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -268,7 +268,7 @@ async fn test_updating_kcl_lsp_files() {
|
||||
name: "my-project2".to_string()
|
||||
}
|
||||
);
|
||||
assert_eq!(server.code_map.len(), 10);
|
||||
assert_eq!(server.code_map.len(), 11);
|
||||
// Just make sure that one of the current files read from disk is accurate.
|
||||
assert_eq!(
|
||||
server
|
||||
@ -313,7 +313,7 @@ async fn test_updating_copilot_lsp_files() {
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(server.code_map.len(), 10);
|
||||
assert_eq!(server.code_map.len(), 11);
|
||||
|
||||
// Run open file.
|
||||
server
|
||||
@ -328,7 +328,7 @@ async fn test_updating_copilot_lsp_files() {
|
||||
.await;
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 11);
|
||||
assert_eq!(server.code_map.len(), 12);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -344,7 +344,7 @@ async fn test_updating_copilot_lsp_files() {
|
||||
.await;
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 11);
|
||||
assert_eq!(server.code_map.len(), 12);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -363,7 +363,7 @@ async fn test_updating_copilot_lsp_files() {
|
||||
.await;
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 12);
|
||||
assert_eq!(server.code_map.len(), 13);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -389,7 +389,7 @@ async fn test_updating_copilot_lsp_files() {
|
||||
.await;
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 12);
|
||||
assert_eq!(server.code_map.len(), 13);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -410,7 +410,7 @@ async fn test_updating_copilot_lsp_files() {
|
||||
.await;
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 12);
|
||||
assert_eq!(server.code_map.len(), 13);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -430,7 +430,7 @@ async fn test_updating_copilot_lsp_files() {
|
||||
.await;
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 13);
|
||||
assert_eq!(server.code_map.len(), 14);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -451,7 +451,7 @@ async fn test_updating_copilot_lsp_files() {
|
||||
.await;
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 12);
|
||||
assert_eq!(server.code_map.len(), 13);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -485,7 +485,7 @@ async fn test_updating_copilot_lsp_files() {
|
||||
);
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 12);
|
||||
assert_eq!(server.code_map.len(), 13);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -516,7 +516,7 @@ async fn test_updating_copilot_lsp_files() {
|
||||
);
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 12);
|
||||
assert_eq!(server.code_map.len(), 13);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -552,7 +552,7 @@ async fn test_updating_copilot_lsp_files() {
|
||||
name: "my-project2".to_string()
|
||||
}
|
||||
);
|
||||
assert_eq!(server.code_map.len(), 10);
|
||||
assert_eq!(server.code_map.len(), 11);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -588,7 +588,7 @@ async fn test_kcl_lsp_create_zip() {
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(server.code_map.len(), 10);
|
||||
assert_eq!(server.code_map.len(), 11);
|
||||
|
||||
// Run open file.
|
||||
server
|
||||
@ -603,7 +603,7 @@ async fn test_kcl_lsp_create_zip() {
|
||||
.await;
|
||||
|
||||
// Check the code map.
|
||||
assert_eq!(server.code_map.len(), 11);
|
||||
assert_eq!(server.code_map.len(), 12);
|
||||
assert_eq!(
|
||||
server.code_map.get("file:///test.kcl").unwrap().clone(),
|
||||
"test".as_bytes()
|
||||
@ -627,7 +627,7 @@ async fn test_kcl_lsp_create_zip() {
|
||||
files.insert(file.name().to_string(), file.size());
|
||||
}
|
||||
|
||||
assert_eq!(files.len(), 11);
|
||||
assert_eq!(files.len(), 12);
|
||||
let util_path = format!("{}/util.rs", string_path).replace("file://", "");
|
||||
assert!(files.contains_key(&util_path));
|
||||
assert_eq!(files.get("/test.kcl"), Some(&4));
|
||||
@ -873,7 +873,7 @@ x = b"#
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_kcl_lsp_on_hover() {
|
||||
let server = kcl_lsp_server(false).await.unwrap();
|
||||
let server = kcl_lsp_server(true).await.unwrap();
|
||||
|
||||
// Send open file.
|
||||
server
|
||||
@ -882,12 +882,27 @@ async fn test_kcl_lsp_on_hover() {
|
||||
uri: "file:///test.kcl".try_into().unwrap(),
|
||||
language_id: "kcl".to_string(),
|
||||
version: 1,
|
||||
text: "startSketchOn()".to_string(),
|
||||
text: r#"startSketchOn(XY)
|
||||
foo = 42
|
||||
foo
|
||||
|
||||
fn bar(x: string): string {
|
||||
return x
|
||||
}
|
||||
|
||||
bar("an arg")
|
||||
|
||||
startSketchOn(XY)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line(end = [10, 0])
|
||||
|> line(end = [0, 10])
|
||||
"#
|
||||
.to_string(),
|
||||
},
|
||||
})
|
||||
.await;
|
||||
|
||||
// Send hover request.
|
||||
// Std lib call
|
||||
let hover = server
|
||||
.hover(tower_lsp::lsp_types::HoverParams {
|
||||
text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams {
|
||||
@ -901,12 +916,99 @@ async fn test_kcl_lsp_on_hover() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check the hover.
|
||||
match hover.unwrap().contents {
|
||||
tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => {
|
||||
value.contains("startSketchOn");
|
||||
value.contains("-> SketchSurface");
|
||||
value.contains("Start a new 2-dimensional sketch on a specific");
|
||||
assert!(value.contains("startSketchOn"));
|
||||
assert!(value.contains(": SketchSurface"));
|
||||
assert!(value.contains("Start a new 2-dimensional sketch on a specific"));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// Variable use
|
||||
let hover = server
|
||||
.hover(tower_lsp::lsp_types::HoverParams {
|
||||
text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams {
|
||||
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
|
||||
uri: "file:///test.kcl".try_into().unwrap(),
|
||||
},
|
||||
position: tower_lsp::lsp_types::Position { line: 2, character: 1 },
|
||||
},
|
||||
work_done_progress_params: Default::default(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match hover.unwrap().contents {
|
||||
tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => {
|
||||
assert!(value.contains("foo: number = 42"));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// User-defined function call.
|
||||
let hover = server
|
||||
.hover(tower_lsp::lsp_types::HoverParams {
|
||||
text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams {
|
||||
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
|
||||
uri: "file:///test.kcl".try_into().unwrap(),
|
||||
},
|
||||
position: tower_lsp::lsp_types::Position { line: 8, character: 1 },
|
||||
},
|
||||
work_done_progress_params: Default::default(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match hover.unwrap().contents {
|
||||
tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => {
|
||||
assert!(value.contains("bar(x: string): string"));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// Variable inside a function
|
||||
let hover = server
|
||||
.hover(tower_lsp::lsp_types::HoverParams {
|
||||
text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams {
|
||||
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
|
||||
uri: "file:///test.kcl".try_into().unwrap(),
|
||||
},
|
||||
position: tower_lsp::lsp_types::Position { line: 5, character: 9 },
|
||||
},
|
||||
work_done_progress_params: Default::default(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match hover.unwrap().contents {
|
||||
tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => {
|
||||
assert!(value.contains("x: string"));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// std function KwArg
|
||||
let hover = server
|
||||
.hover(tower_lsp::lsp_types::HoverParams {
|
||||
text_document_position_params: tower_lsp::lsp_types::TextDocumentPositionParams {
|
||||
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
|
||||
uri: "file:///test.kcl".try_into().unwrap(),
|
||||
},
|
||||
position: tower_lsp::lsp_types::Position {
|
||||
line: 12,
|
||||
character: 11,
|
||||
},
|
||||
},
|
||||
work_done_progress_params: Default::default(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match hover.unwrap().contents {
|
||||
tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => {
|
||||
assert!(value.contains("end?: [number]"));
|
||||
assert!(value.contains("How far away (along the X and Y axes) should this line go?"));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
Reference in New Issue
Block a user