Skip to content

Commit

Permalink
Merge pull request #297 from baszalmstra/feat/document_symbol_provider2
Browse files Browse the repository at this point in the history
feature(lsp): adds document symbol provider
  • Loading branch information
baszalmstra authored Jan 20, 2021
2 parents 6081592 + 0bbdbd4 commit b83c2ce
Show file tree
Hide file tree
Showing 15 changed files with 558 additions and 32 deletions.
1 change: 1 addition & 0 deletions crates/mun_language_server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ paths = {path="../mun_paths", package="mun_paths"}
[dev-dependencies]
tempdir = "0.3.7"
mun_test = { path = "../mun_test"}
insta = "0.16"
20 changes: 13 additions & 7 deletions crates/mun_language_server/src/analysis.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
use crate::cancelation::Canceled;
use crate::change::AnalysisChange;
use crate::db::AnalysisDatabase;
use crate::diagnostics;
use crate::diagnostics::Diagnostic;
use hir::line_index::LineIndex;
use hir::SourceDatabase;
use crate::{
cancelation::Canceled, change::AnalysisChange, db::AnalysisDatabase, diagnostics,
diagnostics::Diagnostic, file_structure,
};
use hir::{line_index::LineIndex, AstDatabase, SourceDatabase};
use salsa::{ParallelDatabase, Snapshot};
use std::sync::Arc;

Expand Down Expand Up @@ -74,6 +72,14 @@ impl AnalysisSnapshot {
self.with_db(|db| db.line_index(file_id))
}

/// Returns a tree structure of the symbols of a file.
pub fn file_structure(
&self,
file_id: hir::FileId,
) -> Cancelable<Vec<file_structure::StructureNode>> {
self.with_db(|db| file_structure::file_structure(&db.parse(file_id).tree()))
}

/// Performs an operation on that may be Canceled.
fn with_db<F: FnOnce(&AnalysisDatabase) -> T + std::panic::UnwindSafe, T>(
&self,
Expand Down
3 changes: 2 additions & 1 deletion crates/mun_language_server/src/capabilities.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use lsp_types::{
ClientCapabilities, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
ClientCapabilities, OneOf, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
};

/// Returns the capabilities of this LSP server implementation given the capabilities of the client.
pub fn server_capabilities(_client_caps: &ClientCapabilities) -> ServerCapabilities {
ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::Full)),
document_symbol_provider: Some(OneOf::Left(true)),
..Default::default()
}
}
10 changes: 10 additions & 0 deletions crates/mun_language_server/src/conversion.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::symbol_kind::SymbolKind;
use lsp_types::Url;
use mun_syntax::{TextRange, TextUnit};
use paths::AbsPathBuf;
Expand Down Expand Up @@ -74,3 +75,12 @@ pub fn convert_uri(uri: &Url) -> anyhow::Result<AbsPathBuf> {
.and_then(|path| AbsPathBuf::try_from(path).ok())
.ok_or_else(|| anyhow::anyhow!("invalid uri: {}", uri))
}

/// Converts a symbol kind from this crate to one for the LSP protocol.
pub fn convert_symbol_kind(symbol_kind: SymbolKind) -> lsp_types::SymbolKind {
match symbol_kind {
SymbolKind::Function => lsp_types::SymbolKind::Function,
SymbolKind::Struct => lsp_types::SymbolKind::Struct,
SymbolKind::TypeAlias => lsp_types::SymbolKind::TypeParameter,
}
}
132 changes: 132 additions & 0 deletions crates/mun_language_server/src/file_structure.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use crate::SymbolKind;
use mun_syntax::{
ast::{self, NameOwner},
match_ast, AstNode, SourceFile, SyntaxNode, TextRange, WalkEvent,
};

/// A description of a symbol in a source file.
#[derive(Debug, Clone)]
pub struct StructureNode {
/// An optional parent of this symbol. Refers to the index of the symbol in the collection that
/// this instance resides in.
pub parent: Option<usize>,

/// The text label
pub label: String,

/// The range to navigate to if selected
pub navigation_range: TextRange,

/// The entire range of the node in the file
pub node_range: TextRange,

/// The type of symbol
pub kind: SymbolKind,

/// Optional detailed information
pub detail: Option<String>,
}

/// Provides a tree of symbols defined in a `SourceFile`.
pub(crate) fn file_structure(file: &SourceFile) -> Vec<StructureNode> {
let mut result = Vec::new();
let mut stack = Vec::new();

for event in file.syntax().preorder() {
match event {
WalkEvent::Enter(node) => {
if let Some(mut symbol) = try_convert_to_structure_node(&node) {
symbol.parent = stack.last().copied();
stack.push(result.len());
result.push(symbol);
}
}
WalkEvent::Leave(node) => {
if try_convert_to_structure_node(&node).is_some() {
stack.pop().unwrap();
}
}
}
}

result
}

/// Tries to convert an ast node to something that would reside in the hierarchical file structure.
fn try_convert_to_structure_node(node: &SyntaxNode) -> Option<StructureNode> {
/// Create a `StructureNode` from a declaration
fn decl<N: NameOwner>(node: N, kind: SymbolKind) -> Option<StructureNode> {
decl_with_detail(&node, None, kind)
}

/// Create a `StructureNode` from a declaration with extra text detail
fn decl_with_detail<N: NameOwner>(
node: &N,
detail: Option<String>,
kind: SymbolKind,
) -> Option<StructureNode> {
let name = node.name()?;

Some(StructureNode {
parent: None,
label: name.text().to_string(),
navigation_range: name.syntax().text_range(),
node_range: node.syntax().text_range(),
kind,
detail,
})
}

/// Given a `SyntaxNode` get the text without any whitespaces
fn collapse_whitespaces(node: &SyntaxNode, output: &mut String) {
let mut can_insert_ws = false;
node.text().for_each_chunk(|chunk| {
for line in chunk.lines() {
let line = line.trim();
if line.is_empty() {
if can_insert_ws {
output.push(' ');
can_insert_ws = false;
}
} else {
output.push_str(line);
can_insert_ws = true;
}
}
})
}

/// Given a `SyntaxNode` construct a `StructureNode` by referring to the type of a node.
fn decl_with_type_ref<N: NameOwner>(
node: &N,
type_ref: Option<ast::TypeRef>,
kind: SymbolKind,
) -> Option<StructureNode> {
let detail = type_ref.map(|type_ref| {
let mut detail = String::new();
collapse_whitespaces(type_ref.syntax(), &mut detail);
detail
});
decl_with_detail(node, detail, kind)
}

match_ast! {
match node {
ast::FunctionDef(it) => {
let mut detail = String::from("fn");
if let Some(param_list) = it.param_list() {
collapse_whitespaces(param_list.syntax(), &mut detail);
}
if let Some(ret_type) = it.ret_type() {
detail.push(' ');
collapse_whitespaces(ret_type.syntax(), &mut detail);
}

decl_with_detail(&it, Some(detail), SymbolKind::Function)
},
ast::StructDef(it) => decl(it, SymbolKind::Struct),
ast::TypeAliasDef(it) => decl_with_type_ref(&it, it.type_ref(), SymbolKind::TypeAlias),
_ => None
}
}
}
148 changes: 148 additions & 0 deletions crates/mun_language_server/src/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use crate::{
conversion::{convert_range, convert_symbol_kind},
state::LanguageServerSnapshot,
};
use lsp_types::DocumentSymbol;

/// Computes the document symbols for a specific document. Converts the LSP types to internal
/// formats and calls [`LanguageServerSnapshot::file_structure`] to fetch the symbols in the
/// requested document. Once completed, returns the result converted back to LSP types.
pub(crate) fn handle_document_symbol(
snapshot: LanguageServerSnapshot,
params: lsp_types::DocumentSymbolParams,
) -> anyhow::Result<Option<lsp_types::DocumentSymbolResponse>> {
let file_id = snapshot.uri_to_file_id(&params.text_document.uri)?;
let line_index = snapshot.analysis.file_line_index(file_id)?;

let mut parents: Vec<(DocumentSymbol, Option<usize>)> = Vec::new();

for symbol in snapshot.analysis.file_structure(file_id)? {
#[allow(deprecated)]
let doc_symbol = DocumentSymbol {
name: symbol.label,
detail: symbol.detail,
kind: convert_symbol_kind(symbol.kind),
tags: None,
deprecated: None,
range: convert_range(symbol.node_range, &line_index),
selection_range: convert_range(symbol.navigation_range, &line_index),
children: None,
};

parents.push((doc_symbol, symbol.parent));
}

Ok(Some(build_hierarchy_from_flat_list(parents).into()))
}

/// Constructs a hierarchy of DocumentSymbols for a list of symbols that specify which index is the
/// parent of a symbol. The parent index must always be smaller than the current index.
fn build_hierarchy_from_flat_list(
mut symbols_and_parent: Vec<(DocumentSymbol, Option<usize>)>,
) -> Vec<DocumentSymbol> {
let mut result = Vec::new();

// Iterate over all elements in the list from back to front.
while let Some((mut node, parent_index)) = symbols_and_parent.pop() {
// If this node has children (added by the code below), they are in the reverse order. This
// is because we iterate the input from back to front.
if let Some(children) = &mut node.children {
children.reverse();
}

// Get the parent index of the current node.
let parent = match parent_index {
// If the parent doesnt have a node, directly use the result vector (its a root).
None => &mut result,

// If there is a parent, get a reference to the children vector of that parent.
Some(i) => symbols_and_parent[i]
.0
.children
.get_or_insert_with(Vec::new),
};

parent.push(node);
}

// The items where pushed in the reverse order, so reverse it right back
result.reverse();
result
}

#[cfg(test)]
mod tests {
use crate::handlers::build_hierarchy_from_flat_list;
use lsp_types::{DocumentSymbol, SymbolKind};

#[test]
fn test_build_hierarchy_from_flat_list() {
#[allow(deprecated)]
let default_symbol = DocumentSymbol {
name: "".to_string(),
detail: None,
kind: SymbolKind::File,
tags: None,
deprecated: None,
range: Default::default(),
selection_range: Default::default(),
children: None,
};

let mut list = Vec::new();

list.push((
DocumentSymbol {
name: "a".to_string(),
..default_symbol.clone()
},
None,
));

list.push((
DocumentSymbol {
name: "b".to_string(),
..default_symbol.clone()
},
Some(0),
));

list.push((
DocumentSymbol {
name: "c".to_string(),
..default_symbol.clone()
},
Some(0),
));

list.push((
DocumentSymbol {
name: "d".to_string(),
..default_symbol.clone()
},
Some(1),
));

assert_eq!(
build_hierarchy_from_flat_list(list),
vec![DocumentSymbol {
name: "a".to_string(),
children: Some(vec![
DocumentSymbol {
name: "b".to_string(),
children: Some(vec![DocumentSymbol {
name: "d".to_string(),
..default_symbol.clone()
}]),
..default_symbol.clone()
},
DocumentSymbol {
name: "c".to_string(),
..default_symbol.clone()
}
]),
..default_symbol.clone()
}]
)
}
}
4 changes: 4 additions & 0 deletions crates/mun_language_server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub use main_loop::main_loop;
use paths::AbsPathBuf;
use project::ProjectManifest;
pub(crate) use state::LanguageServerState;
pub(crate) use symbol_kind::SymbolKind;

mod analysis;
mod cancelation;
Expand All @@ -16,8 +17,11 @@ mod config;
mod conversion;
mod db;
mod diagnostics;
mod file_structure;
mod handlers;
mod main_loop;
mod state;
mod symbol_kind;

/// Deserializes a `T` from a json value.
pub fn from_json<T: DeserializeOwned>(
Expand Down
Loading

0 comments on commit b83c2ce

Please sign in to comment.