Skip to content

Commit

Permalink
feat(lsp): adds document symbol provider
Browse files Browse the repository at this point in the history
  • Loading branch information
baszalmstra committed Jan 14, 2021
1 parent e950f85 commit e8de30e
Show file tree
Hide file tree
Showing 15 changed files with 455 additions and 25 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"
13 changes: 10 additions & 3 deletions crates/mun_language_server/src/analysis.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
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::{diagnostics, file_structure};
use hir::{line_index::LineIndex, AstDatabase, SourceDatabase};
use salsa::{ParallelDatabase, Snapshot};
use std::sync::Arc;

Expand Down Expand Up @@ -74,6 +73,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 an 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 an 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
}
}
}
48 changes: 48 additions & 0 deletions crates/mun_language_server/src/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use crate::conversion::{convert_range, convert_symbol_kind};
use crate::state::LanguageServerSnapshot;
use lsp_types::DocumentSymbol;

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));
}

// Builds hierarchy from a flat list, in reverse order (so that indices
// makes sense)
let document_symbols = {
let mut acc = Vec::new();
while let Some((mut node, parent_idx)) = parents.pop() {
if let Some(children) = &mut node.children {
children.reverse();
}
let parent = match parent_idx {
None => &mut acc,
Some(i) => parents[i].0.children.get_or_insert_with(Vec::new),
};
parent.push(node);
}
acc.reverse();
acc
};

Ok(Some(document_symbols.into()))
}
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;

pub type Result<T> = anyhow::Result<T>;

Expand Down
25 changes: 23 additions & 2 deletions crates/mun_language_server/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use std::time::Instant;
use std::{ops::Deref, sync::Arc};

use anyhow::anyhow;
use crossbeam_channel::{select, unbounded, Receiver, Sender};
use lsp_server::ReqQueue;
use lsp_server::{ReqQueue, Response};
use lsp_types::notification::Notification;
use lsp_types::{notification::PublishDiagnostics, PublishDiagnosticsParams, Url};
use parking_lot::RwLock;
Expand All @@ -19,6 +20,7 @@ use crate::{
conversion::{convert_range, url_from_path_with_drive_lowercasing},
to_json, Result,
};
use std::convert::TryFrom;

mod protocol;
mod utils;
Expand All @@ -28,6 +30,7 @@ mod workspace;
/// enables synchronizing resources like the connection with the client.
#[derive(Debug)]
pub(crate) enum Task {
Response(Response),
Notify(lsp_server::Notification),
}

Expand Down Expand Up @@ -195,6 +198,7 @@ impl LanguageServerState {
Task::Notify(notification) => {
self.send(notification.into());
}
Task::Response(response) => self.respond(response),
}
Ok(())
}
Expand Down Expand Up @@ -371,13 +375,30 @@ impl LanguageServerState {

impl LanguageServerSnapshot {
/// Converts the specified `hir::FileId` to a `Url`
pub fn file_id_to_uri(&self, id: hir::FileId) -> Result<Url> {
pub fn file_id_to_uri(&self, id: hir::FileId) -> anyhow::Result<Url> {
let vfs = self.vfs.read();
let path = vfs.file_path(vfs::FileId(id.0));
let url = url_from_path_with_drive_lowercasing(path)?;

Ok(url)
}

/// Converts the specified `Url` to a `hir::FileId`
pub fn uri_to_file_id(&self, url: &Url) -> anyhow::Result<hir::FileId> {
url.to_file_path()
.map_err(|_| anyhow!("invalid uri: {}", url))
.and_then(|path| {
AbsPathBuf::try_from(path)
.map_err(|_| anyhow!("url does not refer to absolute path: {}", url))
})
.and_then(|path| {
self.vfs
.read()
.file_id(&path)
.ok_or_else(|| anyhow!("url does not refer to a file: {}", url))
.map(|id| hir::FileId(id.0))
})
}
}

impl Drop for LanguageServerState {
Expand Down
6 changes: 4 additions & 2 deletions crates/mun_language_server/src/state/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use dispatcher::{NotificationDispatcher, RequestDispatcher};

use super::LanguageServerState;
use crate::conversion::convert_uri;
use crate::handlers;
use crate::state::RequestHandler;
use lsp_types::notification::{
DidChangeTextDocument, DidChangeWatchedFiles, DidCloseTextDocument, DidOpenTextDocument,
Expand Down Expand Up @@ -91,10 +92,11 @@ impl LanguageServerState {

// Dispatch the event based on the type of event
RequestDispatcher::new(self, request)
.on::<lsp_types::request::Shutdown>(|state, _request| {
.on_sync::<lsp_types::request::Shutdown>(|state, _request| {
state.shutdown_requested = true;
Ok(())
})?
.on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol)?
.finish();

Ok(())
Expand Down Expand Up @@ -152,7 +154,7 @@ impl LanguageServerState {

/// Sends a response to a request to the client. This method logs the time it took us to reply
/// to a request from the client.
fn respond(&mut self, response: lsp_server::Response) {
pub(super) fn respond(&mut self, response: lsp_server::Response) {
if let Some((_method, start)) = self.request_queue.incoming.complete(response.id.clone()) {
let duration = start.elapsed();
log::info!("handled req#{} in {:?}", response.id, duration);
Expand Down
Loading

0 comments on commit e8de30e

Please sign in to comment.