mirror of
https://github.com/nushell/nushell.git
synced 2025-02-08 14:40:53 +01:00
Implement LSP Text Document Synchronization (#10941)
This commit is contained in:
parent
a806717f35
commit
c110ddff66
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -2312,8 +2312,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "lsp-server"
|
name = "lsp-server"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/schrieveslaach/rust-analyzer.git?branch=cancelable-initialization#f95d538d3af1eac9266b898c8bed384c1498edfc"
|
||||||
checksum = "b52dccdf3302eefab8c8a1273047f0a3c3dca4b527c8458d00c09484c8371928"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"log",
|
"log",
|
||||||
@ -2972,6 +2971,7 @@ name = "nu-lsp"
|
|||||||
version = "0.87.1"
|
version = "0.87.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assert-json-diff",
|
"assert-json-diff",
|
||||||
|
"crossbeam-channel",
|
||||||
"lsp-server",
|
"lsp-server",
|
||||||
"lsp-types",
|
"lsp-types",
|
||||||
"miette",
|
"miette",
|
||||||
|
@ -14,8 +14,9 @@ nu-protocol = { path = "../nu-protocol", version = "0.87.1" }
|
|||||||
|
|
||||||
reedline = { version = "0.26" }
|
reedline = { version = "0.26" }
|
||||||
|
|
||||||
|
crossbeam-channel = "0.5.8"
|
||||||
lsp-types = "0.94.1"
|
lsp-types = "0.94.1"
|
||||||
lsp-server = "0.7.4"
|
lsp-server = { version = "0.7.4", git = "https://github.com/schrieveslaach/rust-analyzer.git", branch = "cancelable-initialization" }
|
||||||
miette = "5.10"
|
miette = "5.10"
|
||||||
ropey = "1.6.1"
|
ropey = "1.6.1"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
|
147
crates/nu-lsp/src/diagnostics.rs
Normal file
147
crates/nu-lsp/src/diagnostics.rs
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
use lsp_types::{
|
||||||
|
notification::{Notification, PublishDiagnostics},
|
||||||
|
Diagnostic, DiagnosticSeverity, PublishDiagnosticsParams, Url,
|
||||||
|
};
|
||||||
|
use miette::{IntoDiagnostic, Result};
|
||||||
|
use nu_parser::parse;
|
||||||
|
use nu_protocol::{
|
||||||
|
engine::{EngineState, StateWorkingSet},
|
||||||
|
eval_const::create_nu_constant,
|
||||||
|
Span, Value, NU_VARIABLE_ID,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::LanguageServer;
|
||||||
|
|
||||||
|
impl LanguageServer {
|
||||||
|
pub(crate) fn publish_diagnostics_for_file(
|
||||||
|
&self,
|
||||||
|
uri: Url,
|
||||||
|
engine_state: &mut EngineState,
|
||||||
|
) -> Result<()> {
|
||||||
|
let cwd = std::env::current_dir().expect("Could not get current working directory.");
|
||||||
|
engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));
|
||||||
|
|
||||||
|
let Ok(nu_const) = create_nu_constant(engine_state, Span::unknown()) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
engine_state.set_variable_const_val(NU_VARIABLE_ID, nu_const);
|
||||||
|
|
||||||
|
let mut working_set = StateWorkingSet::new(engine_state);
|
||||||
|
|
||||||
|
let Some((rope_of_file, file_path)) = self.rope(&uri) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let contents = rope_of_file.bytes().collect::<Vec<u8>>();
|
||||||
|
let offset = working_set.next_span_start();
|
||||||
|
parse(
|
||||||
|
&mut working_set,
|
||||||
|
Some(&file_path.to_string_lossy()),
|
||||||
|
&contents,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut diagnostics = PublishDiagnosticsParams {
|
||||||
|
uri,
|
||||||
|
diagnostics: Vec::new(),
|
||||||
|
version: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
for err in working_set.parse_errors.iter() {
|
||||||
|
let message = err.to_string();
|
||||||
|
|
||||||
|
diagnostics.diagnostics.push(Diagnostic {
|
||||||
|
range: Self::span_to_range(&err.span(), rope_of_file, offset),
|
||||||
|
severity: Some(DiagnosticSeverity::ERROR),
|
||||||
|
message,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.connection
|
||||||
|
.sender
|
||||||
|
.send(lsp_server::Message::Notification(
|
||||||
|
lsp_server::Notification::new(PublishDiagnostics::METHOD.to_string(), diagnostics),
|
||||||
|
))
|
||||||
|
.into_diagnostic()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use assert_json_diff::assert_json_eq;
|
||||||
|
use lsp_types::Url;
|
||||||
|
use nu_test_support::fs::fixtures;
|
||||||
|
|
||||||
|
use crate::tests::{initialize_language_server, open, update};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn publish_diagnostics_variable_does_not_exists() {
|
||||||
|
let (client_connection, _recv) = initialize_language_server();
|
||||||
|
|
||||||
|
let mut script = fixtures();
|
||||||
|
script.push("lsp");
|
||||||
|
script.push("diagnostics");
|
||||||
|
script.push("var.nu");
|
||||||
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
|
let notification = open(&client_connection, script.clone());
|
||||||
|
|
||||||
|
assert_json_eq!(
|
||||||
|
notification,
|
||||||
|
serde_json::json!({
|
||||||
|
"method": "textDocument/publishDiagnostics",
|
||||||
|
"params": {
|
||||||
|
"uri": script,
|
||||||
|
"diagnostics": [{
|
||||||
|
"range": {
|
||||||
|
"start": { "line": 0, "character": 6 },
|
||||||
|
"end": { "line": 0, "character": 30 }
|
||||||
|
},
|
||||||
|
"message": "Variable not found.",
|
||||||
|
"severity": 1
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn publish_diagnostics_fixed_unknown_variable() {
|
||||||
|
let (client_connection, _recv) = initialize_language_server();
|
||||||
|
|
||||||
|
let mut script = fixtures();
|
||||||
|
script.push("lsp");
|
||||||
|
script.push("diagnostics");
|
||||||
|
script.push("var.nu");
|
||||||
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
|
open(&client_connection, script.clone());
|
||||||
|
let notification = update(
|
||||||
|
&client_connection,
|
||||||
|
script.clone(),
|
||||||
|
String::from("$env"),
|
||||||
|
Some(lsp_types::Range {
|
||||||
|
start: lsp_types::Position {
|
||||||
|
line: 0,
|
||||||
|
character: 6,
|
||||||
|
},
|
||||||
|
end: lsp_types::Position {
|
||||||
|
line: 0,
|
||||||
|
character: 30,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_json_eq!(
|
||||||
|
notification,
|
||||||
|
serde_json::json!({
|
||||||
|
"method": "textDocument/publishDiagnostics",
|
||||||
|
"params": {
|
||||||
|
"uri": script,
|
||||||
|
"diagnostics": []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,19 @@
|
|||||||
use std::{fs::File, io::Cursor, sync::Arc};
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use lsp_server::{Connection, IoThreads, Message, Response, ResponseError};
|
use lsp_server::{Connection, IoThreads, Message, Response, ResponseError};
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
request::{Completion, GotoDefinition, HoverRequest, Request},
|
request::{Completion, GotoDefinition, HoverRequest, Request},
|
||||||
CompletionItem, CompletionParams, CompletionResponse, CompletionTextEdit, GotoDefinitionParams,
|
CompletionItem, CompletionParams, CompletionResponse, CompletionTextEdit, GotoDefinitionParams,
|
||||||
GotoDefinitionResponse, Hover, HoverContents, HoverParams, Location, MarkupContent, MarkupKind,
|
GotoDefinitionResponse, Hover, HoverContents, HoverParams, Location, MarkupContent, MarkupKind,
|
||||||
OneOf, Range, ServerCapabilities, TextEdit, Url,
|
OneOf, Range, ServerCapabilities, TextDocumentSyncKind, TextEdit, Url,
|
||||||
};
|
};
|
||||||
use miette::{IntoDiagnostic, Result};
|
use miette::{IntoDiagnostic, Result};
|
||||||
use nu_cli::NuCompleter;
|
use nu_cli::NuCompleter;
|
||||||
@ -17,6 +25,9 @@ use nu_protocol::{
|
|||||||
use reedline::Completer;
|
use reedline::Completer;
|
||||||
use ropey::Rope;
|
use ropey::Rope;
|
||||||
|
|
||||||
|
mod diagnostics;
|
||||||
|
mod notification;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Id {
|
enum Id {
|
||||||
Variable(VarId),
|
Variable(VarId),
|
||||||
@ -27,6 +38,7 @@ enum Id {
|
|||||||
pub struct LanguageServer {
|
pub struct LanguageServer {
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
io_threads: Option<IoThreads>,
|
io_threads: Option<IoThreads>,
|
||||||
|
ropes: BTreeMap<PathBuf, Rope>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LanguageServer {
|
impl LanguageServer {
|
||||||
@ -42,11 +54,19 @@ impl LanguageServer {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
connection,
|
connection,
|
||||||
io_threads,
|
io_threads,
|
||||||
|
ropes: BTreeMap::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serve_requests(self, engine_state: EngineState) -> Result<()> {
|
pub fn serve_requests(
|
||||||
|
mut self,
|
||||||
|
engine_state: EngineState,
|
||||||
|
ctrlc: Arc<AtomicBool>,
|
||||||
|
) -> Result<()> {
|
||||||
let server_capabilities = serde_json::to_value(&ServerCapabilities {
|
let server_capabilities = serde_json::to_value(&ServerCapabilities {
|
||||||
|
text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Kind(
|
||||||
|
TextDocumentSyncKind::INCREMENTAL,
|
||||||
|
)),
|
||||||
definition_provider: Some(OneOf::Left(true)),
|
definition_provider: Some(OneOf::Left(true)),
|
||||||
hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
|
hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
|
||||||
completion_provider: Some(lsp_types::CompletionOptions::default()),
|
completion_provider: Some(lsp_types::CompletionOptions::default()),
|
||||||
@ -56,10 +76,22 @@ impl LanguageServer {
|
|||||||
|
|
||||||
let _initialization_params = self
|
let _initialization_params = self
|
||||||
.connection
|
.connection
|
||||||
.initialize(server_capabilities)
|
.initialize_while(server_capabilities, || !ctrlc.load(Ordering::SeqCst))
|
||||||
.into_diagnostic()?;
|
.into_diagnostic()?;
|
||||||
|
|
||||||
for msg in &self.connection.receiver {
|
while !ctrlc.load(Ordering::SeqCst) {
|
||||||
|
let msg = match self
|
||||||
|
.connection
|
||||||
|
.receiver
|
||||||
|
.recv_timeout(Duration::from_secs(1))
|
||||||
|
{
|
||||||
|
Ok(msg) => msg,
|
||||||
|
Err(crossbeam_channel::RecvTimeoutError::Timeout) => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
|
||||||
match msg {
|
match msg {
|
||||||
Message::Request(request) => {
|
Message::Request(request) => {
|
||||||
if self
|
if self
|
||||||
@ -71,25 +103,39 @@ impl LanguageServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut engine_state = engine_state.clone();
|
let mut engine_state = engine_state.clone();
|
||||||
match request.method.as_str() {
|
let resp = match request.method.as_str() {
|
||||||
GotoDefinition::METHOD => {
|
GotoDefinition::METHOD => Self::handle_lsp_request(
|
||||||
self.handle_lsp_request(
|
|
||||||
&mut engine_state,
|
&mut engine_state,
|
||||||
request,
|
request,
|
||||||
Self::goto_definition,
|
|engine_state, params| self.goto_definition(engine_state, params),
|
||||||
)?;
|
),
|
||||||
}
|
HoverRequest::METHOD => Self::handle_lsp_request(
|
||||||
HoverRequest::METHOD => {
|
&mut engine_state,
|
||||||
self.handle_lsp_request(&mut engine_state, request, Self::hover)?;
|
request,
|
||||||
}
|
|engine_state, params| self.hover(engine_state, params),
|
||||||
Completion::METHOD => {
|
),
|
||||||
self.handle_lsp_request(&mut engine_state, request, Self::complete)?;
|
Completion::METHOD => Self::handle_lsp_request(
|
||||||
}
|
&mut engine_state,
|
||||||
_ => {}
|
request,
|
||||||
|
|engine_state, params| self.complete(engine_state, params),
|
||||||
|
),
|
||||||
|
_ => {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.connection
|
||||||
|
.sender
|
||||||
|
.send(Message::Response(resp))
|
||||||
|
.into_diagnostic()?;
|
||||||
}
|
}
|
||||||
Message::Response(_) => {}
|
Message::Response(_) => {}
|
||||||
Message::Notification(_) => {}
|
Message::Notification(notification) => {
|
||||||
|
if let Some(updated_file) = self.handle_lsp_notification(notification) {
|
||||||
|
let mut engine_state = engine_state.clone();
|
||||||
|
self.publish_diagnostics_for_file(updated_file, &mut engine_state)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,22 +147,23 @@ impl LanguageServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_lsp_request<P, H, R>(
|
fn handle_lsp_request<P, H, R>(
|
||||||
&self,
|
|
||||||
engine_state: &mut EngineState,
|
engine_state: &mut EngineState,
|
||||||
req: lsp_server::Request,
|
req: lsp_server::Request,
|
||||||
param_handler: H,
|
mut param_handler: H,
|
||||||
) -> Result<()>
|
) -> Response
|
||||||
where
|
where
|
||||||
P: serde::de::DeserializeOwned,
|
P: serde::de::DeserializeOwned,
|
||||||
H: Fn(&mut EngineState, &P) -> Option<R>,
|
H: FnMut(&mut EngineState, &P) -> Option<R>,
|
||||||
R: serde::ser::Serialize,
|
R: serde::ser::Serialize,
|
||||||
{
|
{
|
||||||
let resp = {
|
|
||||||
match serde_json::from_value::<P>(req.params) {
|
match serde_json::from_value::<P>(req.params) {
|
||||||
Ok(params) => Response {
|
Ok(params) => Response {
|
||||||
id: req.id,
|
id: req.id,
|
||||||
result: param_handler(engine_state, ¶ms)
|
result: Some(
|
||||||
.and_then(|response| serde_json::to_value(response).ok()),
|
param_handler(engine_state, ¶ms)
|
||||||
|
.and_then(|response| serde_json::to_value(response).ok())
|
||||||
|
.unwrap_or(serde_json::Value::Null),
|
||||||
|
),
|
||||||
error: None,
|
error: None,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -130,12 +177,6 @@ impl LanguageServer {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
self.connection
|
|
||||||
.sender
|
|
||||||
.send(Message::Response(resp))
|
|
||||||
.into_diagnostic()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn span_to_range(span: &Span, rope_of_file: &Rope, offset: usize) -> lsp_types::Range {
|
fn span_to_range(span: &Span, rope_of_file: &Rope, offset: usize) -> lsp_types::Range {
|
||||||
@ -158,79 +199,84 @@ impl LanguageServer {
|
|||||||
lsp_types::Range { start, end }
|
lsp_types::Range { start, end }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lsp_position_to_location(position: &lsp_types::Position, rope_of_file: &Rope) -> usize {
|
pub fn lsp_position_to_location(position: &lsp_types::Position, rope_of_file: &Rope) -> usize {
|
||||||
let line_idx = rope_of_file.line_to_char(position.line as usize);
|
let line_idx = rope_of_file.line_to_char(position.line as usize);
|
||||||
line_idx + position.character as usize
|
line_idx + position.character as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_id(
|
fn find_id(
|
||||||
working_set: &mut StateWorkingSet,
|
working_set: &mut StateWorkingSet,
|
||||||
file_path: &str,
|
path: &Path,
|
||||||
file: &[u8],
|
file: &Rope,
|
||||||
location: usize,
|
location: usize,
|
||||||
) -> Option<(Id, usize, Span)> {
|
) -> Option<(Id, usize, Span)> {
|
||||||
let file_id = working_set.add_file(file_path.to_string(), file);
|
let file_path = path.to_string_lossy();
|
||||||
let offset = working_set.get_span_for_file(file_id).start;
|
|
||||||
let block = parse(working_set, Some(file_path), file, false);
|
// TODO: think about passing down the rope into the working_set
|
||||||
|
let contents = file.bytes().collect::<Vec<u8>>();
|
||||||
|
let block = parse(working_set, Some(&file_path), &contents, false);
|
||||||
let flattened = flatten_block(working_set, &block);
|
let flattened = flatten_block(working_set, &block);
|
||||||
|
|
||||||
|
let offset = working_set.get_span_for_filename(&file_path)?.start;
|
||||||
let location = location + offset;
|
let location = location + offset;
|
||||||
for item in flattened {
|
|
||||||
if location >= item.0.start && location < item.0.end {
|
for (span, shape) in flattened {
|
||||||
match &item.1 {
|
if location >= span.start && location < span.end {
|
||||||
|
match &shape {
|
||||||
FlatShape::Variable(var_id) | FlatShape::VarDecl(var_id) => {
|
FlatShape::Variable(var_id) | FlatShape::VarDecl(var_id) => {
|
||||||
return Some((Id::Variable(*var_id), offset, item.0));
|
return Some((Id::Variable(*var_id), offset, span));
|
||||||
}
|
}
|
||||||
FlatShape::InternalCall(decl_id) => {
|
FlatShape::InternalCall(decl_id) => {
|
||||||
return Some((Id::Declaration(*decl_id), offset, item.0));
|
return Some((Id::Declaration(*decl_id), offset, span));
|
||||||
}
|
}
|
||||||
_ => return Some((Id::Value(item.1), offset, item.0)),
|
_ => return Some((Id::Value(shape), offset, span)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_in_file<'a>(
|
fn rope<'a, 'b: 'a>(&'b self, file_url: &Url) -> Option<(&'a Rope, &'a PathBuf)> {
|
||||||
engine_state: &'a mut EngineState,
|
let file_path = file_url.to_file_path().ok()?;
|
||||||
file_path: &str,
|
|
||||||
) -> Result<(Vec<u8>, StateWorkingSet<'a>)> {
|
|
||||||
let file = std::fs::read(file_path).into_diagnostic()?;
|
|
||||||
|
|
||||||
engine_state.start_in_file(Some(file_path));
|
self.ropes
|
||||||
|
.get_key_value(&file_path)
|
||||||
|
.map(|(path, rope)| (rope, path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_in_file<'a>(
|
||||||
|
&mut self,
|
||||||
|
engine_state: &'a mut EngineState,
|
||||||
|
file_url: &Url,
|
||||||
|
) -> Option<(&Rope, &PathBuf, StateWorkingSet<'a>)> {
|
||||||
|
let (file, path) = self.rope(file_url)?;
|
||||||
|
|
||||||
|
// TODO: AsPath thingy
|
||||||
|
engine_state.start_in_file(Some(&path.to_string_lossy()));
|
||||||
|
|
||||||
let working_set = StateWorkingSet::new(engine_state);
|
let working_set = StateWorkingSet::new(engine_state);
|
||||||
|
|
||||||
Ok((file, working_set))
|
Some((file, path, working_set))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn goto_definition(
|
fn goto_definition(
|
||||||
|
&mut self,
|
||||||
engine_state: &mut EngineState,
|
engine_state: &mut EngineState,
|
||||||
params: &GotoDefinitionParams,
|
params: &GotoDefinitionParams,
|
||||||
) -> Option<GotoDefinitionResponse> {
|
) -> Option<GotoDefinitionResponse> {
|
||||||
let cwd = std::env::current_dir().expect("Could not get current working directory.");
|
let cwd = std::env::current_dir().expect("Could not get current working directory.");
|
||||||
engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));
|
engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));
|
||||||
|
|
||||||
let file_path = params
|
let (file, path, mut working_set) = self.read_in_file(
|
||||||
.text_document_position_params
|
engine_state,
|
||||||
.text_document
|
¶ms.text_document_position_params.text_document.uri,
|
||||||
.uri
|
)?;
|
||||||
.to_file_path()
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
let file_path = file_path.to_string_lossy();
|
|
||||||
|
|
||||||
let (file, mut working_set) = Self::read_in_file(engine_state, &file_path).ok()?;
|
|
||||||
let rope_of_file = Rope::from_reader(Cursor::new(&file)).ok()?;
|
|
||||||
|
|
||||||
let (id, _, _) = Self::find_id(
|
let (id, _, _) = Self::find_id(
|
||||||
&mut working_set,
|
&mut working_set,
|
||||||
&file_path,
|
path,
|
||||||
&file,
|
file,
|
||||||
Self::lsp_position_to_location(
|
Self::lsp_position_to_location(¶ms.text_document_position_params.position, file),
|
||||||
¶ms.text_document_position_params.position,
|
|
||||||
&rope_of_file,
|
|
||||||
),
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
match id {
|
match id {
|
||||||
@ -242,7 +288,7 @@ impl LanguageServer {
|
|||||||
if span.start >= *file_start && span.start < *file_end {
|
if span.start >= *file_start && span.start < *file_end {
|
||||||
return Some(GotoDefinitionResponse::Scalar(Location {
|
return Some(GotoDefinitionResponse::Scalar(Location {
|
||||||
uri: Url::from_file_path(file_path).ok()?,
|
uri: Url::from_file_path(file_path).ok()?,
|
||||||
range: Self::span_to_range(span, &rope_of_file, *file_start),
|
range: Self::span_to_range(span, file, *file_start),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -261,11 +307,7 @@ impl LanguageServer {
|
|||||||
.text_document
|
.text_document
|
||||||
.uri
|
.uri
|
||||||
.clone(),
|
.clone(),
|
||||||
range: Self::span_to_range(
|
range: Self::span_to_range(&var.declaration_span, file, *file_start),
|
||||||
&var.declaration_span,
|
|
||||||
&rope_of_file,
|
|
||||||
*file_start,
|
|
||||||
),
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -275,30 +317,20 @@ impl LanguageServer {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hover(engine_state: &mut EngineState, params: &HoverParams) -> Option<Hover> {
|
fn hover(&mut self, engine_state: &mut EngineState, params: &HoverParams) -> Option<Hover> {
|
||||||
let cwd = std::env::current_dir().expect("Could not get current working directory.");
|
let cwd = std::env::current_dir().expect("Could not get current working directory.");
|
||||||
engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));
|
engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));
|
||||||
|
|
||||||
let file_path = params
|
let (file, path, mut working_set) = self.read_in_file(
|
||||||
.text_document_position_params
|
engine_state,
|
||||||
.text_document
|
¶ms.text_document_position_params.text_document.uri,
|
||||||
.uri
|
)?;
|
||||||
.to_file_path()
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
let file_path = file_path.to_string_lossy();
|
|
||||||
|
|
||||||
let (file, mut working_set) = Self::read_in_file(engine_state, &file_path).ok()?;
|
|
||||||
let rope_of_file = Rope::from_reader(Cursor::new(&file)).ok()?;
|
|
||||||
|
|
||||||
let (id, _, _) = Self::find_id(
|
let (id, _, _) = Self::find_id(
|
||||||
&mut working_set,
|
&mut working_set,
|
||||||
&file_path,
|
path,
|
||||||
&file,
|
file,
|
||||||
Self::lsp_position_to_location(
|
Self::lsp_position_to_location(¶ms.text_document_position_params.position, file),
|
||||||
¶ms.text_document_position_params.position,
|
|
||||||
&rope_of_file,
|
|
||||||
),
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
match id {
|
match id {
|
||||||
@ -489,27 +521,23 @@ impl LanguageServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn complete(
|
fn complete(
|
||||||
|
&mut self,
|
||||||
engine_state: &mut EngineState,
|
engine_state: &mut EngineState,
|
||||||
params: &CompletionParams,
|
params: &CompletionParams,
|
||||||
) -> Option<CompletionResponse> {
|
) -> Option<CompletionResponse> {
|
||||||
let cwd = std::env::current_dir().expect("Could not get current working directory.");
|
let cwd = std::env::current_dir().expect("Could not get current working directory.");
|
||||||
engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));
|
engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));
|
||||||
|
|
||||||
let file_path = params
|
let (rope_of_file, _, _) = self.read_in_file(
|
||||||
.text_document_position
|
engine_state,
|
||||||
.text_document
|
¶ms.text_document_position.text_document.uri,
|
||||||
.uri
|
)?;
|
||||||
.to_file_path()
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
let file_path = file_path.to_string_lossy();
|
|
||||||
let rope_of_file = Rope::from_reader(File::open(file_path.as_ref()).ok()?).ok()?;
|
|
||||||
|
|
||||||
let stack = Stack::new();
|
let stack = Stack::new();
|
||||||
let mut completer = NuCompleter::new(Arc::new(engine_state.clone()), stack);
|
let mut completer = NuCompleter::new(Arc::new(engine_state.clone()), stack);
|
||||||
|
|
||||||
let location =
|
let location =
|
||||||
Self::lsp_position_to_location(¶ms.text_document_position.position, &rope_of_file);
|
Self::lsp_position_to_location(¶ms.text_document_position.position, rope_of_file);
|
||||||
let results = completer.complete(&rope_of_file.to_string(), location);
|
let results = completer.complete(&rope_of_file.to_string(), location);
|
||||||
if results.is_empty() {
|
if results.is_empty() {
|
||||||
None
|
None
|
||||||
@ -545,15 +573,18 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use assert_json_diff::assert_json_eq;
|
use assert_json_diff::assert_json_eq;
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
notification::{Exit, Initialized, Notification},
|
notification::{
|
||||||
|
DidChangeTextDocument, DidOpenTextDocument, Exit, Initialized, Notification,
|
||||||
|
},
|
||||||
request::{Completion, GotoDefinition, HoverRequest, Initialize, Request, Shutdown},
|
request::{Completion, GotoDefinition, HoverRequest, Initialize, Request, Shutdown},
|
||||||
CompletionParams, GotoDefinitionParams, InitializeParams, InitializedParams,
|
CompletionParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams,
|
||||||
TextDocumentIdentifier, TextDocumentPositionParams, Url,
|
GotoDefinitionParams, InitializeParams, InitializedParams, TextDocumentContentChangeEvent,
|
||||||
|
TextDocumentIdentifier, TextDocumentItem, TextDocumentPositionParams, Url,
|
||||||
};
|
};
|
||||||
use nu_test_support::fs::{fixtures, root};
|
use nu_test_support::fs::{fixtures, root};
|
||||||
use std::sync::mpsc::Receiver;
|
use std::sync::mpsc::Receiver;
|
||||||
|
|
||||||
fn initialize_language_server() -> (Connection, Receiver<Result<()>>) {
|
pub fn initialize_language_server() -> (Connection, Receiver<Result<()>>) {
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
let (client_connection, server_connection) = Connection::memory();
|
let (client_connection, server_connection) = Connection::memory();
|
||||||
let lsp_server = LanguageServer::initialize_connection(server_connection, None).unwrap();
|
let lsp_server = LanguageServer::initialize_connection(server_connection, None).unwrap();
|
||||||
@ -562,7 +593,7 @@ mod tests {
|
|||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let engine_state = nu_cmd_lang::create_default_context();
|
let engine_state = nu_cmd_lang::create_default_context();
|
||||||
let engine_state = nu_command::add_shell_command_context(engine_state);
|
let engine_state = nu_command::add_shell_command_context(engine_state);
|
||||||
send.send(lsp_server.serve_requests(engine_state))
|
send.send(lsp_server.serve_requests(engine_state, Arc::new(AtomicBool::new(false))))
|
||||||
});
|
});
|
||||||
|
|
||||||
client_connection
|
client_connection
|
||||||
@ -651,16 +682,91 @@ mod tests {
|
|||||||
.receiver
|
.receiver
|
||||||
.recv_timeout(std::time::Duration::from_secs(2))
|
.recv_timeout(std::time::Duration::from_secs(2))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let result = if let Message::Response(response) = resp {
|
||||||
|
response.result
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
|
||||||
assert!(matches!(
|
assert_json_eq!(result, serde_json::json!(null));
|
||||||
resp,
|
|
||||||
Message::Response(response) if response.result.is_none()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn goto_definition(uri: Url, line: u32, character: u32) -> Message {
|
pub fn open(client_connection: &Connection, uri: Url) -> lsp_server::Notification {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
let text = std::fs::read_to_string(uri.to_file_path().unwrap()).unwrap();
|
||||||
|
|
||||||
|
client_connection
|
||||||
|
.sender
|
||||||
|
.send(Message::Notification(lsp_server::Notification {
|
||||||
|
method: DidOpenTextDocument::METHOD.to_string(),
|
||||||
|
params: serde_json::to_value(DidOpenTextDocumentParams {
|
||||||
|
text_document: TextDocumentItem {
|
||||||
|
uri,
|
||||||
|
language_id: String::from("nu"),
|
||||||
|
version: 1,
|
||||||
|
text,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let notification = client_connection
|
||||||
|
.receiver
|
||||||
|
.recv_timeout(Duration::from_secs(2))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if let Message::Notification(n) = notification {
|
||||||
|
n
|
||||||
|
} else {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(
|
||||||
|
client_connection: &Connection,
|
||||||
|
uri: Url,
|
||||||
|
text: String,
|
||||||
|
range: Option<Range>,
|
||||||
|
) -> lsp_server::Notification {
|
||||||
|
client_connection
|
||||||
|
.sender
|
||||||
|
.send(lsp_server::Message::Notification(
|
||||||
|
lsp_server::Notification {
|
||||||
|
method: DidChangeTextDocument::METHOD.to_string(),
|
||||||
|
params: serde_json::to_value(DidChangeTextDocumentParams {
|
||||||
|
text_document: lsp_types::VersionedTextDocumentIdentifier {
|
||||||
|
uri,
|
||||||
|
version: 2,
|
||||||
|
},
|
||||||
|
content_changes: vec![TextDocumentContentChangeEvent {
|
||||||
|
range,
|
||||||
|
range_length: None,
|
||||||
|
text,
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let notification = client_connection
|
||||||
|
.receiver
|
||||||
|
.recv_timeout(Duration::from_secs(2))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if let Message::Notification(n) = notification {
|
||||||
|
n
|
||||||
|
} else {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn goto_definition(
|
||||||
|
client_connection: &Connection,
|
||||||
|
uri: Url,
|
||||||
|
line: u32,
|
||||||
|
character: u32,
|
||||||
|
) -> Message {
|
||||||
client_connection
|
client_connection
|
||||||
.sender
|
.sender
|
||||||
.send(Message::Request(lsp_server::Request {
|
.send(Message::Request(lsp_server::Request {
|
||||||
@ -686,13 +792,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_definition_of_variable() {
|
fn goto_definition_of_variable() {
|
||||||
|
let (client_connection, _recv) = initialize_language_server();
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
script.push("goto");
|
script.push("goto");
|
||||||
script.push("var.nu");
|
script.push("var.nu");
|
||||||
let script = Url::from_file_path(script).unwrap();
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
let resp = goto_definition(script.clone(), 2, 12);
|
open(&client_connection, script.clone());
|
||||||
|
|
||||||
|
let resp = goto_definition(&client_connection, script.clone(), 2, 12);
|
||||||
let result = if let Message::Response(response) = resp {
|
let result = if let Message::Response(response) = resp {
|
||||||
response.result
|
response.result
|
||||||
} else {
|
} else {
|
||||||
@ -713,13 +823,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_definition_of_command() {
|
fn goto_definition_of_command() {
|
||||||
|
let (client_connection, _recv) = initialize_language_server();
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
script.push("goto");
|
script.push("goto");
|
||||||
script.push("command.nu");
|
script.push("command.nu");
|
||||||
let script = Url::from_file_path(script).unwrap();
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
let resp = goto_definition(script.clone(), 4, 1);
|
open(&client_connection, script.clone());
|
||||||
|
|
||||||
|
let resp = goto_definition(&client_connection, script.clone(), 4, 1);
|
||||||
let result = if let Message::Response(response) = resp {
|
let result = if let Message::Response(response) = resp {
|
||||||
response.result
|
response.result
|
||||||
} else {
|
} else {
|
||||||
@ -740,13 +854,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn goto_definition_of_command_parameter() {
|
fn goto_definition_of_command_parameter() {
|
||||||
|
let (client_connection, _recv) = initialize_language_server();
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
script.push("goto");
|
script.push("goto");
|
||||||
script.push("command.nu");
|
script.push("command.nu");
|
||||||
let script = Url::from_file_path(script).unwrap();
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
let resp = goto_definition(script.clone(), 1, 14);
|
open(&client_connection, script.clone());
|
||||||
|
|
||||||
|
let resp = goto_definition(&client_connection, script.clone(), 1, 14);
|
||||||
let result = if let Message::Response(response) = resp {
|
let result = if let Message::Response(response) = resp {
|
||||||
response.result
|
response.result
|
||||||
} else {
|
} else {
|
||||||
@ -765,9 +883,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hover(uri: Url, line: u32, character: u32) -> Message {
|
pub fn hover(client_connection: &Connection, uri: Url, line: u32, character: u32) -> Message {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
|
||||||
|
|
||||||
client_connection
|
client_connection
|
||||||
.sender
|
.sender
|
||||||
.send(Message::Request(lsp_server::Request {
|
.send(Message::Request(lsp_server::Request {
|
||||||
@ -792,13 +908,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hover_on_variable() {
|
fn hover_on_variable() {
|
||||||
|
let (client_connection, _recv) = initialize_language_server();
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
script.push("hover");
|
script.push("hover");
|
||||||
script.push("var.nu");
|
script.push("var.nu");
|
||||||
let script = Url::from_file_path(script).unwrap();
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
let resp = hover(script.clone(), 2, 0);
|
open(&client_connection, script.clone());
|
||||||
|
|
||||||
|
let resp = hover(&client_connection, script.clone(), 2, 0);
|
||||||
let result = if let Message::Response(response) = resp {
|
let result = if let Message::Response(response) = resp {
|
||||||
response.result
|
response.result
|
||||||
} else {
|
} else {
|
||||||
@ -815,13 +935,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hover_on_command() {
|
fn hover_on_command() {
|
||||||
|
let (client_connection, _recv) = initialize_language_server();
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
script.push("hover");
|
script.push("hover");
|
||||||
script.push("command.nu");
|
script.push("command.nu");
|
||||||
let script = Url::from_file_path(script).unwrap();
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
let resp = hover(script.clone(), 3, 0);
|
open(&client_connection, script.clone());
|
||||||
|
|
||||||
|
let resp = hover(&client_connection, script.clone(), 3, 0);
|
||||||
let result = if let Message::Response(response) = resp {
|
let result = if let Message::Response(response) = resp {
|
||||||
response.result
|
response.result
|
||||||
} else {
|
} else {
|
||||||
@ -839,9 +963,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn complete(uri: Url, line: u32, character: u32) -> Message {
|
fn complete(client_connection: &Connection, uri: Url, line: u32, character: u32) -> Message {
|
||||||
let (client_connection, _recv) = initialize_language_server();
|
|
||||||
|
|
||||||
client_connection
|
client_connection
|
||||||
.sender
|
.sender
|
||||||
.send(Message::Request(lsp_server::Request {
|
.send(Message::Request(lsp_server::Request {
|
||||||
@ -868,13 +990,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn complete_on_variable() {
|
fn complete_on_variable() {
|
||||||
|
let (client_connection, _recv) = initialize_language_server();
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
script.push("completion");
|
script.push("completion");
|
||||||
script.push("var.nu");
|
script.push("var.nu");
|
||||||
let script = Url::from_file_path(script).unwrap();
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
let resp = complete(script, 2, 9);
|
open(&client_connection, script.clone());
|
||||||
|
|
||||||
|
let resp = complete(&client_connection, script, 2, 9);
|
||||||
let result = if let Message::Response(response) = resp {
|
let result = if let Message::Response(response) = resp {
|
||||||
response.result
|
response.result
|
||||||
} else {
|
} else {
|
||||||
@ -900,13 +1026,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn complete_command_with_space() {
|
fn complete_command_with_space() {
|
||||||
|
let (client_connection, _recv) = initialize_language_server();
|
||||||
|
|
||||||
let mut script = fixtures();
|
let mut script = fixtures();
|
||||||
script.push("lsp");
|
script.push("lsp");
|
||||||
script.push("completion");
|
script.push("completion");
|
||||||
script.push("command.nu");
|
script.push("command.nu");
|
||||||
let script = Url::from_file_path(script).unwrap();
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
let resp = complete(script, 0, 8);
|
open(&client_connection, script.clone());
|
||||||
|
|
||||||
|
let resp = complete(&client_connection, script, 0, 8);
|
||||||
let result = if let Message::Response(response) = resp {
|
let result = if let Message::Response(response) = resp {
|
||||||
response.result
|
response.result
|
||||||
} else {
|
} else {
|
||||||
|
185
crates/nu-lsp/src/notification.rs
Normal file
185
crates/nu-lsp/src/notification.rs
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
use lsp_types::{
|
||||||
|
notification::{
|
||||||
|
DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Notification,
|
||||||
|
},
|
||||||
|
DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, Url,
|
||||||
|
};
|
||||||
|
use ropey::Rope;
|
||||||
|
|
||||||
|
use crate::LanguageServer;
|
||||||
|
|
||||||
|
impl LanguageServer {
|
||||||
|
pub(crate) fn handle_lsp_notification(
|
||||||
|
&mut self,
|
||||||
|
notification: lsp_server::Notification,
|
||||||
|
) -> Option<Url> {
|
||||||
|
match notification.method.as_str() {
|
||||||
|
DidOpenTextDocument::METHOD => Self::handle_notification_payload::<
|
||||||
|
DidOpenTextDocumentParams,
|
||||||
|
_,
|
||||||
|
>(notification, |param| {
|
||||||
|
if let Ok(file_path) = param.text_document.uri.to_file_path() {
|
||||||
|
let rope = Rope::from_str(¶m.text_document.text);
|
||||||
|
self.ropes.insert(file_path, rope);
|
||||||
|
Some(param.text_document.uri)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
DidChangeTextDocument::METHOD => {
|
||||||
|
Self::handle_notification_payload::<DidChangeTextDocumentParams, _>(
|
||||||
|
notification,
|
||||||
|
|params| self.update_rope(params),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DidCloseTextDocument::METHOD => Self::handle_notification_payload::<
|
||||||
|
DidCloseTextDocumentParams,
|
||||||
|
_,
|
||||||
|
>(notification, |param| {
|
||||||
|
if let Ok(file_path) = param.text_document.uri.to_file_path() {
|
||||||
|
self.ropes.remove(&file_path);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_notification_payload<P, H>(
|
||||||
|
notification: lsp_server::Notification,
|
||||||
|
mut param_handler: H,
|
||||||
|
) -> Option<Url>
|
||||||
|
where
|
||||||
|
P: serde::de::DeserializeOwned,
|
||||||
|
H: FnMut(P) -> Option<Url>,
|
||||||
|
{
|
||||||
|
if let Ok(params) = serde_json::from_value::<P>(notification.params) {
|
||||||
|
param_handler(params)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_rope(&mut self, params: DidChangeTextDocumentParams) -> Option<Url> {
|
||||||
|
if let Ok(file_path) = params.text_document.uri.to_file_path() {
|
||||||
|
for content_change in params.content_changes.into_iter() {
|
||||||
|
let entry = self.ropes.entry(file_path.clone());
|
||||||
|
match (content_change.range, content_change.range) {
|
||||||
|
(Some(range), _) => {
|
||||||
|
entry.and_modify(|rope| {
|
||||||
|
let start = Self::lsp_position_to_location(&range.start, rope);
|
||||||
|
let end = Self::lsp_position_to_location(&range.end, rope);
|
||||||
|
|
||||||
|
rope.remove(start..end);
|
||||||
|
rope.insert(start, &content_change.text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
(None, None) => {
|
||||||
|
entry.and_modify(|r| *r = Rope::from_str(&content_change.text));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(params.text_document.uri)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use assert_json_diff::assert_json_eq;
|
||||||
|
use lsp_server::Message;
|
||||||
|
use lsp_types::{Range, Url};
|
||||||
|
use nu_test_support::fs::fixtures;
|
||||||
|
|
||||||
|
use crate::tests::{hover, initialize_language_server, open, update};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_on_command_after_full_content_change() {
|
||||||
|
let (client_connection, _recv) = initialize_language_server();
|
||||||
|
|
||||||
|
let mut script = fixtures();
|
||||||
|
script.push("lsp");
|
||||||
|
script.push("hover");
|
||||||
|
script.push("command.nu");
|
||||||
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
|
open(&client_connection, script.clone());
|
||||||
|
update(
|
||||||
|
&client_connection,
|
||||||
|
script.clone(),
|
||||||
|
String::from(
|
||||||
|
r#"# Renders some updated greeting message
|
||||||
|
def hello [] {}
|
||||||
|
|
||||||
|
hello"#,
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let resp = hover(&client_connection, script.clone(), 3, 0);
|
||||||
|
let result = if let Message::Response(response) = resp {
|
||||||
|
response.result
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_json_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!({
|
||||||
|
"contents": {
|
||||||
|
"kind": "markdown",
|
||||||
|
"value": "```\n### Signature\n```\n hello {flags}\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n### Usage\n Renders some updated greeting message\n"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_on_command_after_partial_content_change() {
|
||||||
|
let (client_connection, _recv) = initialize_language_server();
|
||||||
|
|
||||||
|
let mut script = fixtures();
|
||||||
|
script.push("lsp");
|
||||||
|
script.push("hover");
|
||||||
|
script.push("command.nu");
|
||||||
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
|
open(&client_connection, script.clone());
|
||||||
|
update(
|
||||||
|
&client_connection,
|
||||||
|
script.clone(),
|
||||||
|
String::from("# Renders some updated greeting message"),
|
||||||
|
Some(Range {
|
||||||
|
start: lsp_types::Position {
|
||||||
|
line: 0,
|
||||||
|
character: 0,
|
||||||
|
},
|
||||||
|
end: lsp_types::Position {
|
||||||
|
line: 0,
|
||||||
|
character: 31,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let resp = hover(&client_connection, script.clone(), 3, 0);
|
||||||
|
let result = if let Message::Response(response) = resp {
|
||||||
|
response.result
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_json_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!({
|
||||||
|
"contents": {
|
||||||
|
"kind": "markdown",
|
||||||
|
"value": "```\n### Signature\n```\n hello {flags}\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n### Usage\n Renders some updated greeting message\n"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -317,6 +317,15 @@ impl<'a> StateWorkingSet<'a> {
|
|||||||
self.num_virtual_paths() - 1
|
self.num_virtual_paths() - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_span_for_filename(&self, filename: &str) -> Option<Span> {
|
||||||
|
let (file_id, ..) = self
|
||||||
|
.files()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, (fname, _, _))| fname == filename)?;
|
||||||
|
|
||||||
|
Some(self.get_span_for_file(file_id))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_span_for_file(&self, file_id: usize) -> Span {
|
pub fn get_span_for_file(&self, file_id: usize) -> Span {
|
||||||
let result = self
|
let result = self
|
||||||
.files()
|
.files()
|
||||||
|
@ -193,7 +193,7 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if parsed_nu_cli_args.lsp {
|
if parsed_nu_cli_args.lsp {
|
||||||
return LanguageServer::initialize_stdio_connection()?.serve_requests(engine_state);
|
return LanguageServer::initialize_stdio_connection()?.serve_requests(engine_state, ctrlc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDE commands
|
// IDE commands
|
||||||
|
1
tests/fixtures/lsp/diagnostics/var.nu
vendored
Normal file
1
tests/fixtures/lsp/diagnostics/var.nu
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
print $this_var_is_not_defined
|
Loading…
Reference in New Issue
Block a user