feat(lsp): hover and goto definition
This commit is contained in:
@@ -2,10 +2,6 @@ use std::env;
|
||||
|
||||
use aiken_project::{config::Config, paths};
|
||||
use lsp_server::Connection;
|
||||
use lsp_types::{
|
||||
OneOf, SaveOptions, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
|
||||
TextDocumentSyncOptions, TextDocumentSyncSaveOptions,
|
||||
};
|
||||
|
||||
mod cast;
|
||||
pub mod error;
|
||||
@@ -53,21 +49,32 @@ pub fn start() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn capabilities() -> ServerCapabilities {
|
||||
ServerCapabilities {
|
||||
text_document_sync: Some(TextDocumentSyncCapability::Options(
|
||||
TextDocumentSyncOptions {
|
||||
fn capabilities() -> lsp_types::ServerCapabilities {
|
||||
lsp_types::ServerCapabilities {
|
||||
completion_provider: Some(lsp_types::CompletionOptions {
|
||||
resolve_provider: None,
|
||||
trigger_characters: Some(vec![".".into(), " ".into()]),
|
||||
all_commit_characters: None,
|
||||
work_done_progress_options: lsp_types::WorkDoneProgressOptions {
|
||||
work_done_progress: None,
|
||||
},
|
||||
}),
|
||||
document_formatting_provider: Some(lsp_types::OneOf::Left(true)),
|
||||
definition_provider: Some(lsp_types::OneOf::Left(true)),
|
||||
hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
|
||||
text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Options(
|
||||
lsp_types::TextDocumentSyncOptions {
|
||||
open_close: None,
|
||||
change: Some(TextDocumentSyncKind::FULL),
|
||||
change: Some(lsp_types::TextDocumentSyncKind::FULL),
|
||||
will_save: None,
|
||||
will_save_wait_until: None,
|
||||
save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
|
||||
include_text: Some(false),
|
||||
})),
|
||||
save: Some(lsp_types::TextDocumentSyncSaveOptions::SaveOptions(
|
||||
lsp_types::SaveOptions {
|
||||
include_text: Some(false),
|
||||
},
|
||||
)),
|
||||
},
|
||||
)),
|
||||
// definition_provider: Some(OneOf::Left(true)),
|
||||
document_formatting_provider: Some(OneOf::Left(true)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,24 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use aiken_lang::{ast::ModuleKind, parser};
|
||||
use aiken_lang::{
|
||||
ast::{Located, ModuleKind, Span},
|
||||
parser,
|
||||
tipo::pretty::Printer,
|
||||
};
|
||||
use aiken_project::{
|
||||
config,
|
||||
error::{Error as ProjectError, GetSource},
|
||||
module::CheckedModule,
|
||||
};
|
||||
use indoc::formatdoc;
|
||||
use lsp_server::{Connection, Message};
|
||||
use lsp_types::{
|
||||
notification::{
|
||||
DidChangeTextDocument, DidSaveTextDocument, Notification, Progress, PublishDiagnostics,
|
||||
ShowMessage,
|
||||
},
|
||||
request::{Formatting, Request, WorkDoneProgressCreate},
|
||||
request::{Formatting, GotoDefinition, HoverRequest, Request, WorkDoneProgressCreate},
|
||||
DocumentFormattingParams, InitializeParams, TextEdit,
|
||||
};
|
||||
use miette::Diagnostic;
|
||||
@@ -25,7 +31,8 @@ use crate::{
|
||||
error::Error as ServerError,
|
||||
line_numbers::LineNumbers,
|
||||
utils::{
|
||||
path_to_uri, text_edit_replace, COMPILING_PROGRESS_TOKEN, CREATE_COMPILING_PROGRESS_TOKEN,
|
||||
path_to_uri, span_to_lsp_range, text_edit_replace, uri_to_module_name,
|
||||
COMPILING_PROGRESS_TOKEN, CREATE_COMPILING_PROGRESS_TOKEN,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -230,12 +237,138 @@ impl Server {
|
||||
}
|
||||
}
|
||||
}
|
||||
HoverRequest::METHOD => {
|
||||
let params = cast_request::<HoverRequest>(request)?;
|
||||
|
||||
let opt_hover = self.hover(params)?;
|
||||
|
||||
Ok(lsp_server::Response {
|
||||
id,
|
||||
error: None,
|
||||
result: Some(serde_json::to_value(opt_hover)?),
|
||||
})
|
||||
}
|
||||
|
||||
GotoDefinition::METHOD => {
|
||||
let params = cast_request::<GotoDefinition>(request)?;
|
||||
|
||||
let location = self.goto_definition(params)?;
|
||||
|
||||
Ok(lsp_server::Response {
|
||||
id,
|
||||
error: None,
|
||||
result: Some(serde_json::to_value(location)?),
|
||||
})
|
||||
}
|
||||
|
||||
unsupported => Err(ServerError::UnsupportedLspRequest {
|
||||
request: unsupported.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn goto_definition(
|
||||
&self,
|
||||
params: lsp_types::GotoDefinitionParams,
|
||||
) -> Result<Option<lsp_types::Location>, ServerError> {
|
||||
let params = params.text_document_position_params;
|
||||
|
||||
let (line_numbers, node) = match self.node_at_position(¶ms) {
|
||||
Some(location) => location,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let location = match node.definition_location() {
|
||||
Some(location) => location,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let (uri, line_numbers) = match location.module {
|
||||
None => (params.text_document.uri, &line_numbers),
|
||||
Some(name) => {
|
||||
let module = match self
|
||||
.compiler
|
||||
.as_ref()
|
||||
.and_then(|compiler| compiler.sources.get(name))
|
||||
{
|
||||
Some(module) => module,
|
||||
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let url = url::Url::parse(&format!("file:///{}", &module.path))
|
||||
.expect("goto definition URL parse");
|
||||
|
||||
(url, &module.line_numbers)
|
||||
}
|
||||
};
|
||||
|
||||
let range = span_to_lsp_range(location.span, line_numbers);
|
||||
|
||||
Ok(Some(lsp_types::Location { uri, range }))
|
||||
}
|
||||
|
||||
fn node_at_position(
|
||||
&self,
|
||||
params: &lsp_types::TextDocumentPositionParams,
|
||||
) -> Option<(LineNumbers, Located<'_>)> {
|
||||
let module = self.module_for_uri(¶ms.text_document.uri);
|
||||
|
||||
let module = module?;
|
||||
|
||||
let line_numbers = LineNumbers::new(&module.code);
|
||||
|
||||
let byte_index = line_numbers.byte_index(
|
||||
params.position.line as usize,
|
||||
params.position.character as usize,
|
||||
);
|
||||
|
||||
let node = module.find_node(byte_index);
|
||||
|
||||
let node = node?;
|
||||
|
||||
Some((line_numbers, node))
|
||||
}
|
||||
|
||||
fn module_for_uri(&self, uri: &url::Url) -> Option<&CheckedModule> {
|
||||
self.compiler.as_ref().and_then(|compiler| {
|
||||
let module_name = uri_to_module_name(uri, &self.root).expect("uri to module name");
|
||||
|
||||
compiler.modules.get(&module_name)
|
||||
})
|
||||
}
|
||||
|
||||
fn hover(
|
||||
&self,
|
||||
params: lsp_types::HoverParams,
|
||||
) -> Result<Option<lsp_types::Hover>, ServerError> {
|
||||
let params = params.text_document_position_params;
|
||||
|
||||
let (line_numbers, found) = match self.node_at_position(¶ms) {
|
||||
Some(value) => value,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let expression = match found {
|
||||
Located::Expression(expression) => expression,
|
||||
Located::Definition(_) => return Ok(None),
|
||||
};
|
||||
|
||||
// Show the type of the hovered node to the user
|
||||
let type_ = Printer::new().pretty_print(expression.tipo().as_ref(), 0);
|
||||
|
||||
let contents = formatdoc! {r#"
|
||||
```aiken
|
||||
{type_}
|
||||
```
|
||||
"#};
|
||||
|
||||
Ok(Some(lsp_types::Hover {
|
||||
contents: lsp_types::HoverContents::Scalar(lsp_types::MarkedString::String(contents)),
|
||||
range: Some(span_to_lsp_range(expression.location(), &line_numbers)),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn listen(&mut self, connection: Connection) -> Result<(), ServerError> {
|
||||
self.create_compilation_progress_token(&connection)?;
|
||||
self.start_watching_aiken_toml(&connection)?;
|
||||
@@ -366,21 +499,13 @@ impl Server {
|
||||
|
||||
let line_numbers = LineNumbers::new(&src);
|
||||
|
||||
let start = line_numbers.line_and_column_number(labeled_span.inner().offset());
|
||||
let end = line_numbers.line_and_column_number(
|
||||
labeled_span.inner().offset() + labeled_span.inner().len(),
|
||||
);
|
||||
|
||||
let lsp_diagnostic = lsp_types::Diagnostic {
|
||||
range: lsp_types::Range::new(
|
||||
lsp_types::Position {
|
||||
line: start.line as u32 - 1,
|
||||
character: start.column as u32 - 1,
|
||||
},
|
||||
lsp_types::Position {
|
||||
line: end.line as u32 - 1,
|
||||
character: end.column as u32 - 1,
|
||||
range: span_to_lsp_range(
|
||||
Span {
|
||||
start: labeled_span.inner().offset(),
|
||||
end: labeled_span.inner().offset() + labeled_span.inner().len(),
|
||||
},
|
||||
&line_numbers,
|
||||
),
|
||||
severity: Some(severity),
|
||||
code: error
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use aiken_lang::ast::Span;
|
||||
use itertools::Itertools;
|
||||
use lsp_types::TextEdit;
|
||||
use urlencoding::decode;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::{error::Error, line_numbers::LineNumbers};
|
||||
|
||||
pub const COMPILING_PROGRESS_TOKEN: &str = "compiling-gleam";
|
||||
pub const CREATE_COMPILING_PROGRESS_TOKEN: &str = "create-compiling-progress-token";
|
||||
@@ -32,3 +35,52 @@ pub fn path_to_uri(path: PathBuf) -> Result<lsp_types::Url, Error> {
|
||||
|
||||
Ok(uri)
|
||||
}
|
||||
|
||||
pub fn span_to_lsp_range(location: Span, line_numbers: &LineNumbers) -> lsp_types::Range {
|
||||
let start = line_numbers.line_and_column_number(location.start);
|
||||
let end = line_numbers.line_and_column_number(location.end);
|
||||
|
||||
lsp_types::Range {
|
||||
start: lsp_types::Position {
|
||||
line: start.line as u32 - 1,
|
||||
character: start.column as u32 - 1,
|
||||
},
|
||||
end: lsp_types::Position {
|
||||
line: end.line as u32 - 1,
|
||||
character: end.column as u32 - 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uri_to_module_name(uri: &url::Url, root: &Path) -> Option<String> {
|
||||
let path = if cfg!(target_os = "windows") {
|
||||
let mut uri_path = decode(&uri.path().replace('/', "\\"))
|
||||
.expect("Invalid formatting")
|
||||
.to_string();
|
||||
|
||||
if uri_path.starts_with('\\') {
|
||||
uri_path = uri_path
|
||||
.strip_prefix('\\')
|
||||
.expect("Failed to remove \"\\\" prefix")
|
||||
.to_string();
|
||||
}
|
||||
|
||||
PathBuf::from(uri_path)
|
||||
} else {
|
||||
PathBuf::from(uri.path())
|
||||
};
|
||||
|
||||
let components = path
|
||||
.strip_prefix(root)
|
||||
.ok()?
|
||||
.components()
|
||||
.skip(1)
|
||||
.map(|c| c.as_os_str().to_string_lossy());
|
||||
|
||||
let module_name = Itertools::intersperse(components, "/".into())
|
||||
.collect::<String>()
|
||||
.strip_suffix(".ak")?
|
||||
.to_string();
|
||||
|
||||
Some(module_name)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user