Skip to content

Commit

Permalink
LS: Clean up hover definition logic (#5780)
Browse files Browse the repository at this point in the history
  • Loading branch information
mkaput authored Jun 13, 2024
1 parent ffe60ee commit db405e9
Show file tree
Hide file tree
Showing 12 changed files with 311 additions and 141 deletions.
19 changes: 17 additions & 2 deletions crates/cairo-lang-defs/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ pub trait DefsGroup:
fn module_dir(&self, module_id: ModuleId) -> Maybe<Directory>;

// TODO(mkaput): Add tests.
// TODO(mkaput): Support #[doc] attribute. This will be a bigger chunk of work because it would
// be the best to convert all /// comments to #[doc] attrs before processing items by plugins,
// so that plugins would get a nice and clean syntax of documentation to manipulate further.
/// Gets the documentation above an item definition.
fn get_item_documentation(&self, item_id: LookupItemId) -> Option<String>;
// TODO(mkaput): Add tests.
Expand Down Expand Up @@ -307,8 +310,20 @@ fn get_item_documentation(db: &dyn DefsGroup, item_id: LookupItemId) -> Option<S
!line.trim_start().chars().next().map_or(false, |c| c.is_alphabetic())
})
.filter_map(|line| {
(line.trim_start().starts_with("///") || line.trim_start().starts_with("//!"))
.then_some(line.trim_start())
// Remove indentation.
let dedent = line.trim_start();
// Check if this is a doc comment.
for prefix in ["///", "//!"] {
if let Some(content) = dedent.strip_prefix(prefix) {
// TODO(mkaput): The way how removing this indentation is performed is probably
// wrong. The code should probably learn how many spaces are used at the first
// line of comments block, and then remove the same amount of spaces in the
// block, instead of assuming just one space.
// Remove inner indentation if one exists.
return Some(content.strip_prefix(' ').unwrap_or(content));
}
}
None
})
.collect::<Vec<&str>>();
(!doc.is_empty()).then(|| doc.join("\n"))
Expand Down
2 changes: 1 addition & 1 deletion crates/cairo-lang-language-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ cairo-lang-starknet = { path = "../cairo-lang-starknet", version = "~2.6.4" }
cairo-lang-syntax = { path = "../cairo-lang-syntax", version = "~2.6.4" }
cairo-lang-test-plugin = { path = "../cairo-lang-test-plugin", version = "~2.6.4" }
cairo-lang-utils = { path = "../cairo-lang-utils", version = "~2.6.4" }
itertools.workspace = true
salsa.workspace = true
scarb-metadata = "1.12"
serde = { workspace = true, default-features = true }
Expand All @@ -40,7 +41,6 @@ assert_fs = "1.1"
cairo-lang-test-utils = { path = "../cairo-lang-test-utils", features = ["testing"] }
futures = "0.3"
indoc.workspace = true
itertools.workspace = true
pathdiff = "0.2"
test-log.workspace = true
tower-service = "0.3"
83 changes: 0 additions & 83 deletions crates/cairo-lang-language-server/src/ide/hover.rs

This file was deleted.

30 changes: 30 additions & 0 deletions crates/cairo-lang-language-server/src/ide/hover/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use cairo_lang_compiler::db::RootDatabase;
use cairo_lang_utils::Upcast;
use tower_lsp::lsp_types::{Hover, HoverContents, HoverParams, MarkupContent, MarkupKind};

use crate::lang::lsp::{LsProtoGroup, ToCairo};
use crate::lang::syntax::LsSyntaxGroup;
use crate::markdown::Markdown;

mod render;

/// Get hover information at a given text document position.
#[tracing::instrument(
level = "debug",
skip_all,
fields(uri = %params.text_document_position_params.text_document.uri)
)]
pub fn hover(params: HoverParams, db: &RootDatabase) -> Option<Hover> {
let file_id = db.file_for_url(&params.text_document_position_params.text_document.uri)?;
let position = params.text_document_position_params.position.to_cairo();
let identifier = db.find_identifier_at_position(file_id, position)?;

render::definition(db.upcast(), &identifier, file_id)

// TODO(mkaput): If client only supports plaintext, strip markdown formatting here like RA.
}

/// Convenience shortcut for building hover contents from markdown block.
fn markdown_contents(md: Markdown) -> HoverContents {
HoverContents::Markup(MarkupContent { kind: MarkupKind::Markdown, value: md.into() })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use cairo_lang_compiler::db::RootDatabase;
use cairo_lang_defs::db::DefsGroup;
use cairo_lang_filesystem::ids::FileId;
use cairo_lang_syntax::node::ast::TerminalIdentifier;
use cairo_lang_syntax::node::TypedSyntaxNode;
use cairo_lang_utils::Upcast;
use tower_lsp::lsp_types::Hover;

use crate::find_definition;
use crate::ide::hover::markdown_contents;
use crate::lang::lsp::ToLsp;
use crate::lang::semantic::LsSemanticGroup;
use crate::markdown::Markdown;

/// Get declaration and documentation "definition" of an item referred by the given identifier.
#[tracing::instrument(level = "trace", skip_all)]
pub fn definition(
db: &RootDatabase,
identifier: &TerminalIdentifier,
file_id: FileId,
) -> Option<Hover> {
// Get the syntax node of the definition.
let definition_node = {
let lookup_items = db.collect_lookup_items_stack(&identifier.as_syntax_node())?;
let stable_ptr = find_definition(db, identifier, &lookup_items)?;
stable_ptr.lookup(db.upcast())
};
// Get the lookup item representing the defining item.
let lookup_item_id = db.find_lookup_item(&definition_node)?;

let mut md = Markdown::empty();

let definition = db.get_item_definition(lookup_item_id);
// TODO(mkaput): Format this with Cairo formatter.
md += Markdown::fenced_code_block(&definition);

let documentation = db.get_item_documentation(lookup_item_id).unwrap_or_default();

if !documentation.is_empty() {
md += Markdown::rule();

let mut doc = Markdown::from(documentation);
doc.convert_fenced_code_blocks_to_cairo();
doc.ensure_trailing_newline();
md += doc;
}

Some(Hover {
contents: markdown_contents(md),
range: identifier
.as_syntax_node()
.span_without_trivia(db.upcast())
.position_in_file(db.upcast(), file_id)
.map(|p| p.to_lsp()),
})
}
3 changes: 3 additions & 0 deletions crates/cairo-lang-language-server/src/ide/hover/render/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod definition;

pub use self::definition::*;
1 change: 1 addition & 0 deletions crates/cairo-lang-language-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ mod env_config;
mod ide;
mod lang;
mod lsp;
mod markdown;
mod project;
mod server;
mod toolchain;
Expand Down
110 changes: 110 additions & 0 deletions crates/cairo-lang-language-server/src/markdown.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use std::borrow::Cow;
use std::fmt;
use std::ops::AddAssign;

use itertools::Itertools;

#[cfg(test)]
#[path = "markdown_test.rs"]
mod test;

/// A convenience wrapper for building Markdown texts for used to display rich text in the IDE.
///
/// Markdown is used because this is the format used by the LSP protocol for rich text.
#[derive(Default, Debug)]
pub struct Markdown {
text: String,
}

/// Constructors and primitive operations.
impl Markdown {
/// Creates a new [`Markdown`] instance with empty text.
pub fn empty() -> Self {
Default::default()
}

/// Horizontal rule.
pub fn rule() -> Self {
"\n---\n".into()
}

/// Creates a new [`Markdown`] instance with the given code surrounded with `cairo` fenced code
/// block.
pub fn fenced_code_block(contents: &str) -> Self {
format!("```cairo\n{contents}\n```").into()
}

/// Appends the given Markdown text to the current text.
pub fn append(&mut self, other: Self) {
self.text.push_str(&other.text);
}
}

impl From<Markdown> for String {
fn from(markdown: Markdown) -> Self {
markdown.text
}
}

impl From<String> for Markdown {
fn from(text: String) -> Self {
Markdown { text }
}
}

impl From<&str> for Markdown {
fn from(text: &str) -> Self {
text.to_owned().into()
}
}

impl fmt::Display for Markdown {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.text, f)
}
}

impl AddAssign for Markdown {
fn add_assign(&mut self, rhs: Self) {
self.append(rhs)
}
}

/// High-level operations used throughout LS functionality.
impl Markdown {
/// Adds `cairo` language code to all fenced code blocks in the text that do not specify
/// language.
pub fn convert_fenced_code_blocks_to_cairo(&mut self) {
let mut in_cairo_fence = false;
self.text = self
.text
.lines()
.map(|line| match (line.strip_prefix("```"), in_cairo_fence) {
// Start of a fenced code block without language code.
(Some(rest), false) if rest.trim_start().is_empty() => {
in_cairo_fence = true;
Cow::Owned(format!("```cairo{rest}"))
}
// Start of a fenced code block but with some language code.
(Some(_), false) => {
in_cairo_fence = true;
Cow::Borrowed(line)
}
// End of a fenced code block.
(Some(_), true) => {
in_cairo_fence = false;
Cow::Borrowed(line)
}
// Unrelated line.
(None, _) => Cow::Borrowed(line),
})
.join("\n");
}

/// Ensures that the text ends with `\n`.
pub fn ensure_trailing_newline(&mut self) {
if !self.text.ends_with('\n') {
self.text.push('\n');
}
}
}
Loading

0 comments on commit db405e9

Please sign in to comment.