mirror of
https://github.com/nushell/nushell.git
synced 2024-11-07 09:04:18 +01:00
Integrated Language Server (#10723)
# Description This commit integrates a language server into nushell so that IDEs don't have to convert CLI option back and forth. - fixes https://github.com/nushell/vscode-nushell-lang/issues/117 - fixes https://github.com/jokeyrhyme/nuls/issues/8 Tracking tasks - [x] [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) -> `nu --ide-hover` - [x] [textDocument/completion](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion) -> `nu --ide-complete` - [x] [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) -> `nu --ide-goto-def` - ~~[ ] [textDocument/didChange](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didChange), [textDocument/didClose](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didClose), and [textDocument/didOpen](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didOpen)~~ (will be done in a follow-up PR) - ~~[ ] [textDocument/inlayHint](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_inlayHint) -> `nu --ide-check`~~ (will be done in a follow-up PR) - ~~[ ] [textDocument/publishDiagnostics](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics) -> `nu --ide-check`~~ (will be done in a follow-up PR) - ~~[ ] [workspace/configuration](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration)~~ (will be done in a follow-up PR) - ~~[ ] [workspace/didChangeConfiguration](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeConfiguration)~~ (will be done in a follow-up PR) # User-Facing Changes The command line options `--lsp` will start a LSP server. # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
This commit is contained in:
parent
a46048f362
commit
0ca8fcf58c
75
Cargo.lock
generated
75
Cargo.lock
generated
@ -2309,6 +2309,31 @@ dependencies = [
|
|||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lsp-server"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b52dccdf3302eefab8c8a1273047f0a3c3dca4b527c8458d00c09484c8371928"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lsp-types"
|
||||||
|
version = "0.94.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lz4"
|
name = "lz4"
|
||||||
version = "1.24.0"
|
version = "1.24.0"
|
||||||
@ -2641,6 +2666,7 @@ dependencies = [
|
|||||||
"nu-engine",
|
"nu-engine",
|
||||||
"nu-explore",
|
"nu-explore",
|
||||||
"nu-json",
|
"nu-json",
|
||||||
|
"nu-lsp",
|
||||||
"nu-parser",
|
"nu-parser",
|
||||||
"nu-path",
|
"nu-path",
|
||||||
"nu-plugin",
|
"nu-plugin",
|
||||||
@ -2940,6 +2966,27 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-lsp"
|
||||||
|
version = "0.86.1"
|
||||||
|
dependencies = [
|
||||||
|
"assert-json-diff",
|
||||||
|
"lsp-server",
|
||||||
|
"lsp-types",
|
||||||
|
"miette",
|
||||||
|
"nu-cli",
|
||||||
|
"nu-cmd-lang",
|
||||||
|
"nu-command",
|
||||||
|
"nu-parser",
|
||||||
|
"nu-protocol",
|
||||||
|
"nu-test-support",
|
||||||
|
"reedline",
|
||||||
|
"ropey",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-parser"
|
name = "nu-parser"
|
||||||
version = "0.86.1"
|
version = "0.86.1"
|
||||||
@ -4332,6 +4379,16 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ropey"
|
||||||
|
version = "1.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5"
|
||||||
|
dependencies = [
|
||||||
|
"smallvec",
|
||||||
|
"str_indices",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "roxmltree"
|
name = "roxmltree"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
@ -4636,6 +4693,17 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_repr"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.37",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
@ -4903,6 +4971,12 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "str_indices"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8eeaedde8e50d8a331578c9fa9a288df146ce5e16173ad26ce82f6e263e2be4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "streaming-decompression"
|
name = "streaming-decompression"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@ -5586,6 +5660,7 @@ dependencies = [
|
|||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -33,6 +33,7 @@ members = [
|
|||||||
"crates/nu-cmd-lang",
|
"crates/nu-cmd-lang",
|
||||||
"crates/nu-cmd-dataframe",
|
"crates/nu-cmd-dataframe",
|
||||||
"crates/nu-command",
|
"crates/nu-command",
|
||||||
|
"crates/nu-lsp",
|
||||||
"crates/nu-protocol",
|
"crates/nu-protocol",
|
||||||
"crates/nu-plugin",
|
"crates/nu-plugin",
|
||||||
"crates/nu_plugin_inc",
|
"crates/nu_plugin_inc",
|
||||||
@ -56,6 +57,7 @@ nu-command = { path = "./crates/nu-command", version = "0.86.1" }
|
|||||||
nu-engine = { path = "./crates/nu-engine", version = "0.86.1" }
|
nu-engine = { path = "./crates/nu-engine", version = "0.86.1" }
|
||||||
nu-explore = { path = "./crates/nu-explore", version = "0.86.1" }
|
nu-explore = { path = "./crates/nu-explore", version = "0.86.1" }
|
||||||
nu-json = { path = "./crates/nu-json", version = "0.86.1" }
|
nu-json = { path = "./crates/nu-json", version = "0.86.1" }
|
||||||
|
nu-lsp = { path = "./crates/nu-lsp/", version = "0.86.1" }
|
||||||
nu-parser = { path = "./crates/nu-parser", version = "0.86.1" }
|
nu-parser = { path = "./crates/nu-parser", version = "0.86.1" }
|
||||||
nu-path = { path = "./crates/nu-path", version = "0.86.1" }
|
nu-path = { path = "./crates/nu-path", version = "0.86.1" }
|
||||||
nu-plugin = { path = "./crates/nu-plugin", optional = true, version = "0.86.1" }
|
nu-plugin = { path = "./crates/nu-plugin", optional = true, version = "0.86.1" }
|
||||||
|
29
crates/nu-lsp/Cargo.toml
Normal file
29
crates/nu-lsp/Cargo.toml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
[package]
|
||||||
|
authors = ["The Nushell Project Developers"]
|
||||||
|
description = "Nushell's integrated LSP server"
|
||||||
|
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-lsp"
|
||||||
|
name = "nu-lsp"
|
||||||
|
version = "0.86.1"
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nu-cli = { path = "../nu-cli", version = "0.86.1" }
|
||||||
|
nu-parser = { path = "../nu-parser", version = "0.86.1" }
|
||||||
|
nu-protocol = { path = "../nu-protocol", version = "0.86.1" }
|
||||||
|
reedline = { version = "0.25" }
|
||||||
|
|
||||||
|
lsp-types = "0.94.1"
|
||||||
|
lsp-server = "0.7.4"
|
||||||
|
miette = "5.10"
|
||||||
|
ropey = "1.6.1"
|
||||||
|
serde = "1.0"
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.86.1" }
|
||||||
|
nu-command = { path = "../nu-command", version = "0.86.1" }
|
||||||
|
nu-test-support = { path = "../nu-test-support", version = "0.86.1" }
|
||||||
|
|
||||||
|
assert-json-diff = "2.0"
|
||||||
|
tempfile = "3.2"
|
28
crates/nu-lsp/LICENSE
Normal file
28
crates/nu-lsp/LICENSE
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 The Rust Project Developers
|
||||||
|
Copyright (c) 2020-2023 The Nushell Project Developers
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any
|
||||||
|
person obtaining a copy of this software and associated
|
||||||
|
documentation files (the "Software"), to deal in the
|
||||||
|
Software without restriction, including without
|
||||||
|
limitation the rights to use, copy, modify, merge,
|
||||||
|
publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software
|
||||||
|
is furnished to do so, subject to the following
|
||||||
|
conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice
|
||||||
|
shall be included in all copies or substantial portions
|
||||||
|
of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||||
|
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||||
|
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||||
|
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||||
|
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
933
crates/nu-lsp/src/lib.rs
Normal file
933
crates/nu-lsp/src/lib.rs
Normal file
@ -0,0 +1,933 @@
|
|||||||
|
use std::{fs::File, io::Cursor, sync::Arc};
|
||||||
|
|
||||||
|
use lsp_server::{Connection, IoThreads, Message, Response, ResponseError};
|
||||||
|
use lsp_types::{
|
||||||
|
request::{Completion, GotoDefinition, HoverRequest, Request},
|
||||||
|
CompletionItem, CompletionParams, CompletionResponse, CompletionTextEdit, GotoDefinitionParams,
|
||||||
|
GotoDefinitionResponse, Hover, HoverContents, HoverParams, Location, MarkupContent, MarkupKind,
|
||||||
|
OneOf, Range, ServerCapabilities, TextEdit, Url,
|
||||||
|
};
|
||||||
|
use miette::{IntoDiagnostic, Result};
|
||||||
|
use nu_cli::NuCompleter;
|
||||||
|
use nu_parser::{flatten_block, parse, FlatShape};
|
||||||
|
use nu_protocol::{
|
||||||
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
|
DeclId, Span, Value, VarId,
|
||||||
|
};
|
||||||
|
use reedline::Completer;
|
||||||
|
use ropey::Rope;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Id {
|
||||||
|
Variable(VarId),
|
||||||
|
Declaration(DeclId),
|
||||||
|
Value(FlatShape),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LanguageServer {
|
||||||
|
connection: Connection,
|
||||||
|
io_threads: Option<IoThreads>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LanguageServer {
|
||||||
|
pub fn initialize_stdio_connection() -> Result<Self> {
|
||||||
|
let (connection, io_threads) = Connection::stdio();
|
||||||
|
Self::initialize_connection(connection, Some(io_threads))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize_connection(
|
||||||
|
connection: Connection,
|
||||||
|
io_threads: Option<IoThreads>,
|
||||||
|
) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
connection,
|
||||||
|
io_threads,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serve_requests(self, engine_state: EngineState) -> Result<()> {
|
||||||
|
let server_capabilities = serde_json::to_value(&ServerCapabilities {
|
||||||
|
definition_provider: Some(OneOf::Left(true)),
|
||||||
|
hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
|
||||||
|
completion_provider: Some(lsp_types::CompletionOptions::default()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.expect("Must be serializable");
|
||||||
|
|
||||||
|
let _initialization_params = self
|
||||||
|
.connection
|
||||||
|
.initialize(server_capabilities)
|
||||||
|
.into_diagnostic()?;
|
||||||
|
|
||||||
|
for msg in &self.connection.receiver {
|
||||||
|
match msg {
|
||||||
|
Message::Request(request) => {
|
||||||
|
if self
|
||||||
|
.connection
|
||||||
|
.handle_shutdown(&request)
|
||||||
|
.into_diagnostic()?
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut engine_state = engine_state.clone();
|
||||||
|
match request.method.as_str() {
|
||||||
|
GotoDefinition::METHOD => {
|
||||||
|
self.handle_lsp_request(
|
||||||
|
&mut engine_state,
|
||||||
|
request,
|
||||||
|
Self::goto_definition,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
HoverRequest::METHOD => {
|
||||||
|
self.handle_lsp_request(&mut engine_state, request, Self::hover)?;
|
||||||
|
}
|
||||||
|
Completion::METHOD => {
|
||||||
|
self.handle_lsp_request(&mut engine_state, request, Self::complete)?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::Response(_) => {}
|
||||||
|
Message::Notification(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(io_threads) = self.io_threads {
|
||||||
|
io_threads.join().into_diagnostic()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_lsp_request<P, H, R>(
|
||||||
|
&self,
|
||||||
|
engine_state: &mut EngineState,
|
||||||
|
req: lsp_server::Request,
|
||||||
|
param_handler: H,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
P: serde::de::DeserializeOwned,
|
||||||
|
H: Fn(&mut EngineState, &P) -> Option<R>,
|
||||||
|
R: serde::ser::Serialize,
|
||||||
|
{
|
||||||
|
let resp = {
|
||||||
|
match serde_json::from_value::<P>(req.params) {
|
||||||
|
Ok(params) => Response {
|
||||||
|
id: req.id,
|
||||||
|
result: param_handler(engine_state, ¶ms)
|
||||||
|
.and_then(|response| serde_json::to_value(response).ok()),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
|
||||||
|
Err(err) => Response {
|
||||||
|
id: req.id,
|
||||||
|
result: None,
|
||||||
|
error: Some(ResponseError {
|
||||||
|
code: 1,
|
||||||
|
message: err.to_string(),
|
||||||
|
data: None,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.connection
|
||||||
|
.sender
|
||||||
|
.send(Message::Response(resp))
|
||||||
|
.into_diagnostic()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn span_to_range(span: &Span, rope_of_file: &Rope, offset: usize) -> lsp_types::Range {
|
||||||
|
let line = rope_of_file.char_to_line(span.start - offset);
|
||||||
|
let character = span.start - offset - rope_of_file.line_to_char(line);
|
||||||
|
|
||||||
|
let start = lsp_types::Position {
|
||||||
|
line: line as u32,
|
||||||
|
character: character as u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
let line = rope_of_file.char_to_line(span.end - offset);
|
||||||
|
let character = span.end - offset - rope_of_file.line_to_char(line);
|
||||||
|
|
||||||
|
let end = lsp_types::Position {
|
||||||
|
line: line as u32,
|
||||||
|
character: character as u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
lsp_types::Range { start, end }
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
line_idx + position.character as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_id(
|
||||||
|
working_set: &mut StateWorkingSet,
|
||||||
|
file_path: &str,
|
||||||
|
file: &[u8],
|
||||||
|
location: usize,
|
||||||
|
) -> Option<(Id, usize, Span)> {
|
||||||
|
let file_id = working_set.add_file(file_path.to_string(), file);
|
||||||
|
let offset = working_set.get_span_for_file(file_id).start;
|
||||||
|
let block = parse(working_set, Some(file_path), file, false);
|
||||||
|
let flattened = flatten_block(working_set, &block);
|
||||||
|
|
||||||
|
let location = location + offset;
|
||||||
|
for item in flattened {
|
||||||
|
if location >= item.0.start && location < item.0.end {
|
||||||
|
match &item.1 {
|
||||||
|
FlatShape::Variable(var_id) | FlatShape::VarDecl(var_id) => {
|
||||||
|
return Some((Id::Variable(*var_id), offset, item.0));
|
||||||
|
}
|
||||||
|
FlatShape::InternalCall(decl_id) => {
|
||||||
|
return Some((Id::Declaration(*decl_id), offset, item.0));
|
||||||
|
}
|
||||||
|
_ => return Some((Id::Value(item.1), offset, item.0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_in_file<'a>(
|
||||||
|
engine_state: &'a mut EngineState,
|
||||||
|
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));
|
||||||
|
|
||||||
|
let working_set = StateWorkingSet::new(engine_state);
|
||||||
|
|
||||||
|
Ok((file, working_set))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn goto_definition(
|
||||||
|
engine_state: &mut EngineState,
|
||||||
|
params: &GotoDefinitionParams,
|
||||||
|
) -> Option<GotoDefinitionResponse> {
|
||||||
|
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 file_path = params
|
||||||
|
.text_document_position_params
|
||||||
|
.text_document
|
||||||
|
.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(
|
||||||
|
&mut working_set,
|
||||||
|
&file_path,
|
||||||
|
&file,
|
||||||
|
Self::lsp_position_to_location(
|
||||||
|
¶ms.text_document_position_params.position,
|
||||||
|
&rope_of_file,
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
match id {
|
||||||
|
Id::Declaration(decl_id) => {
|
||||||
|
if let Some(block_id) = working_set.get_decl(decl_id).get_block_id() {
|
||||||
|
let block = working_set.get_block(block_id);
|
||||||
|
if let Some(span) = &block.span {
|
||||||
|
for (file_path, file_start, file_end) in working_set.files() {
|
||||||
|
if span.start >= *file_start && span.start < *file_end {
|
||||||
|
return Some(GotoDefinitionResponse::Scalar(Location {
|
||||||
|
uri: Url::from_file_path(file_path).ok()?,
|
||||||
|
range: Self::span_to_range(span, &rope_of_file, *file_start),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Id::Variable(var_id) => {
|
||||||
|
let var = working_set.get_variable(var_id);
|
||||||
|
for (_, file_start, file_end) in working_set.files() {
|
||||||
|
if var.declaration_span.start >= *file_start
|
||||||
|
&& var.declaration_span.start < *file_end
|
||||||
|
{
|
||||||
|
return Some(GotoDefinitionResponse::Scalar(Location {
|
||||||
|
uri: params
|
||||||
|
.text_document_position_params
|
||||||
|
.text_document
|
||||||
|
.uri
|
||||||
|
.clone(),
|
||||||
|
range: Self::span_to_range(
|
||||||
|
&var.declaration_span,
|
||||||
|
&rope_of_file,
|
||||||
|
*file_start,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hover(engine_state: &mut EngineState, params: &HoverParams) -> Option<Hover> {
|
||||||
|
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 file_path = params
|
||||||
|
.text_document_position_params
|
||||||
|
.text_document
|
||||||
|
.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(
|
||||||
|
&mut working_set,
|
||||||
|
&file_path,
|
||||||
|
&file,
|
||||||
|
Self::lsp_position_to_location(
|
||||||
|
¶ms.text_document_position_params.position,
|
||||||
|
&rope_of_file,
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
match id {
|
||||||
|
Id::Variable(var_id) => {
|
||||||
|
let var = working_set.get_variable(var_id);
|
||||||
|
let contents = format!("{}{}", if var.mutable { "mutable " } else { "" }, var.ty);
|
||||||
|
|
||||||
|
Some(Hover {
|
||||||
|
contents: HoverContents::Scalar(lsp_types::MarkedString::String(contents)),
|
||||||
|
// TODO
|
||||||
|
range: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Id::Declaration(decl_id) => {
|
||||||
|
let decl = working_set.get_decl(decl_id);
|
||||||
|
|
||||||
|
let mut description = "```\n### Signature\n```\n".to_string();
|
||||||
|
let signature = decl.signature();
|
||||||
|
description.push_str(&format!(" {}", signature.name));
|
||||||
|
if !signature.named.is_empty() {
|
||||||
|
description.push_str(" {flags}")
|
||||||
|
}
|
||||||
|
for required_arg in &signature.required_positional {
|
||||||
|
description.push_str(&format!(" <{}>", required_arg.name));
|
||||||
|
}
|
||||||
|
for optional_arg in &signature.optional_positional {
|
||||||
|
description.push_str(&format!(" <{}?>", optional_arg.name));
|
||||||
|
}
|
||||||
|
if let Some(arg) = &signature.rest_positional {
|
||||||
|
description.push_str(&format!(" <...{}>", arg.name));
|
||||||
|
}
|
||||||
|
description.push_str("\n```\n");
|
||||||
|
if !signature.required_positional.is_empty()
|
||||||
|
|| !signature.optional_positional.is_empty()
|
||||||
|
|| signature.rest_positional.is_some()
|
||||||
|
{
|
||||||
|
description.push_str("\n### Parameters\n\n");
|
||||||
|
let mut first = true;
|
||||||
|
for required_arg in &signature.required_positional {
|
||||||
|
if !first {
|
||||||
|
description.push_str("\\\n");
|
||||||
|
} else {
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
description.push_str(&format!(
|
||||||
|
" `{}: {}`",
|
||||||
|
required_arg.name,
|
||||||
|
required_arg.shape.to_type()
|
||||||
|
));
|
||||||
|
if !required_arg.desc.is_empty() {
|
||||||
|
description.push_str(&format!(" - {}", required_arg.desc));
|
||||||
|
}
|
||||||
|
description.push('\n');
|
||||||
|
}
|
||||||
|
for optional_arg in &signature.optional_positional {
|
||||||
|
if !first {
|
||||||
|
description.push_str("\\\n");
|
||||||
|
} else {
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
description.push_str(&format!(
|
||||||
|
" `{}: {}`",
|
||||||
|
optional_arg.name,
|
||||||
|
optional_arg.shape.to_type()
|
||||||
|
));
|
||||||
|
if !optional_arg.desc.is_empty() {
|
||||||
|
description.push_str(&format!(" - {}", optional_arg.desc));
|
||||||
|
}
|
||||||
|
description.push('\n');
|
||||||
|
}
|
||||||
|
if let Some(arg) = &signature.rest_positional {
|
||||||
|
if !first {
|
||||||
|
description.push_str("\\\n");
|
||||||
|
}
|
||||||
|
description.push_str(&format!(
|
||||||
|
" `...{}: {}`",
|
||||||
|
arg.name,
|
||||||
|
arg.shape.to_type()
|
||||||
|
));
|
||||||
|
if !arg.desc.is_empty() {
|
||||||
|
description.push_str(&format!(" - {}", arg.desc));
|
||||||
|
}
|
||||||
|
description.push('\n');
|
||||||
|
}
|
||||||
|
description.push('\n');
|
||||||
|
}
|
||||||
|
if !signature.named.is_empty() {
|
||||||
|
description.push_str("\n### Flags\n\n");
|
||||||
|
let mut first = true;
|
||||||
|
for named in &signature.named {
|
||||||
|
if !first {
|
||||||
|
description.push_str("\\\n");
|
||||||
|
} else {
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
description.push_str(" ");
|
||||||
|
if let Some(short_flag) = &named.short {
|
||||||
|
description.push_str(&format!("`-{}`", short_flag));
|
||||||
|
}
|
||||||
|
if !named.long.is_empty() {
|
||||||
|
if named.short.is_some() {
|
||||||
|
description.push_str(", ")
|
||||||
|
}
|
||||||
|
description.push_str(&format!("`--{}`", named.long));
|
||||||
|
}
|
||||||
|
if let Some(arg) = &named.arg {
|
||||||
|
description.push_str(&format!(" `<{}>`", arg.to_type()))
|
||||||
|
}
|
||||||
|
if !named.desc.is_empty() {
|
||||||
|
description.push_str(&format!(" - {}", named.desc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
description.push('\n');
|
||||||
|
}
|
||||||
|
if !signature.input_output_types.is_empty() {
|
||||||
|
description.push_str("\n### Input/output\n");
|
||||||
|
description.push_str("\n```\n");
|
||||||
|
for input_output in &signature.input_output_types {
|
||||||
|
description
|
||||||
|
.push_str(&format!(" {} | {}\n", input_output.0, input_output.1));
|
||||||
|
}
|
||||||
|
description.push_str("\n```\n");
|
||||||
|
}
|
||||||
|
description.push_str(&format!(
|
||||||
|
"### Usage\n {}\n",
|
||||||
|
decl.usage().replace('\r', "")
|
||||||
|
));
|
||||||
|
if !decl.extra_usage().is_empty() {
|
||||||
|
description
|
||||||
|
.push_str(&format!("\n### Extra usage:\n {}\n", decl.extra_usage()));
|
||||||
|
}
|
||||||
|
if !decl.examples().is_empty() {
|
||||||
|
description.push_str("### Example(s)\n```\n");
|
||||||
|
for example in decl.examples() {
|
||||||
|
description.push_str(&format!(
|
||||||
|
"```\n {}\n```\n {}\n\n",
|
||||||
|
example.description, example.example
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Hover {
|
||||||
|
contents: HoverContents::Markup(MarkupContent {
|
||||||
|
kind: MarkupKind::Markdown,
|
||||||
|
value: description,
|
||||||
|
}),
|
||||||
|
// TODO
|
||||||
|
range: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Id::Value(shape) => {
|
||||||
|
let hover = String::from(match shape {
|
||||||
|
FlatShape::And => "and",
|
||||||
|
FlatShape::Binary => "binary",
|
||||||
|
FlatShape::Block => "block",
|
||||||
|
FlatShape::Bool => "bool",
|
||||||
|
FlatShape::Closure => "closure",
|
||||||
|
FlatShape::DateTime => "datetime",
|
||||||
|
FlatShape::Directory => "directory",
|
||||||
|
FlatShape::External => "external",
|
||||||
|
FlatShape::ExternalArg => "external arg",
|
||||||
|
FlatShape::Filepath => "file path",
|
||||||
|
FlatShape::Flag => "flag",
|
||||||
|
FlatShape::Float => "float",
|
||||||
|
FlatShape::GlobPattern => "glob pattern",
|
||||||
|
FlatShape::Int => "int",
|
||||||
|
FlatShape::Keyword => "keyword",
|
||||||
|
FlatShape::List => "list",
|
||||||
|
FlatShape::MatchPattern => "match-pattern",
|
||||||
|
FlatShape::Nothing => "nothing",
|
||||||
|
FlatShape::Range => "range",
|
||||||
|
FlatShape::Record => "record",
|
||||||
|
FlatShape::String => "string",
|
||||||
|
FlatShape::StringInterpolation => "string interpolation",
|
||||||
|
FlatShape::Table => "table",
|
||||||
|
_ => {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(Hover {
|
||||||
|
contents: HoverContents::Scalar(lsp_types::MarkedString::String(hover)),
|
||||||
|
// TODO
|
||||||
|
range: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete(
|
||||||
|
engine_state: &mut EngineState,
|
||||||
|
params: &CompletionParams,
|
||||||
|
) -> Option<CompletionResponse> {
|
||||||
|
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 file_path = params
|
||||||
|
.text_document_position
|
||||||
|
.text_document
|
||||||
|
.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 mut completer = NuCompleter::new(Arc::new(engine_state.clone()), stack);
|
||||||
|
|
||||||
|
let location =
|
||||||
|
Self::lsp_position_to_location(¶ms.text_document_position.position, &rope_of_file);
|
||||||
|
let results = completer.complete(&rope_of_file.to_string(), location);
|
||||||
|
if results.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(CompletionResponse::Array(
|
||||||
|
results
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| {
|
||||||
|
let mut start = params.text_document_position.position;
|
||||||
|
start.character -= (r.span.end - r.span.start) as u32;
|
||||||
|
|
||||||
|
CompletionItem {
|
||||||
|
label: r.value.clone(),
|
||||||
|
detail: r.description,
|
||||||
|
text_edit: Some(CompletionTextEdit::Edit(TextEdit {
|
||||||
|
range: Range {
|
||||||
|
start,
|
||||||
|
end: params.text_document_position.position,
|
||||||
|
},
|
||||||
|
new_text: r.value,
|
||||||
|
})),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use assert_json_diff::assert_json_eq;
|
||||||
|
use lsp_types::{
|
||||||
|
notification::{Exit, Initialized, Notification},
|
||||||
|
request::{Completion, GotoDefinition, HoverRequest, Initialize, Request, Shutdown},
|
||||||
|
CompletionParams, GotoDefinitionParams, InitializeParams, InitializedParams,
|
||||||
|
TextDocumentIdentifier, TextDocumentPositionParams, Url,
|
||||||
|
};
|
||||||
|
use nu_test_support::fs::{fixtures, root};
|
||||||
|
use std::sync::mpsc::Receiver;
|
||||||
|
|
||||||
|
fn initialize_language_server() -> (Connection, Receiver<Result<()>>) {
|
||||||
|
use std::sync::mpsc;
|
||||||
|
let (client_connection, server_connection) = Connection::memory();
|
||||||
|
let lsp_server = LanguageServer::initialize_connection(server_connection, None).unwrap();
|
||||||
|
|
||||||
|
let (send, recv) = mpsc::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let engine_state = nu_cmd_lang::create_default_context();
|
||||||
|
let engine_state = nu_command::add_shell_command_context(engine_state);
|
||||||
|
send.send(lsp_server.serve_requests(engine_state))
|
||||||
|
});
|
||||||
|
|
||||||
|
client_connection
|
||||||
|
.sender
|
||||||
|
.send(Message::Request(lsp_server::Request {
|
||||||
|
id: 1.into(),
|
||||||
|
method: Initialize::METHOD.to_string(),
|
||||||
|
params: serde_json::to_value(InitializeParams {
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
client_connection
|
||||||
|
.sender
|
||||||
|
.send(Message::Notification(lsp_server::Notification {
|
||||||
|
method: Initialized::METHOD.to_string(),
|
||||||
|
params: serde_json::to_value(InitializedParams {}).unwrap(),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let _initialize_response = client_connection
|
||||||
|
.receiver
|
||||||
|
.recv_timeout(std::time::Duration::from_secs(2))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
(client_connection, recv)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shutdown_on_request() {
|
||||||
|
let (client_connection, recv) = initialize_language_server();
|
||||||
|
|
||||||
|
client_connection
|
||||||
|
.sender
|
||||||
|
.send(Message::Request(lsp_server::Request {
|
||||||
|
id: 2.into(),
|
||||||
|
method: Shutdown::METHOD.to_string(),
|
||||||
|
params: serde_json::Value::Null,
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
client_connection
|
||||||
|
.sender
|
||||||
|
.send(Message::Notification(lsp_server::Notification {
|
||||||
|
method: Exit::METHOD.to_string(),
|
||||||
|
params: serde_json::Value::Null,
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(recv
|
||||||
|
.recv_timeout(std::time::Duration::from_secs(2))
|
||||||
|
.unwrap()
|
||||||
|
.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn goto_definition_for_none_existing_file() {
|
||||||
|
let (client_connection, _recv) = initialize_language_server();
|
||||||
|
|
||||||
|
let mut none_existent_path = root();
|
||||||
|
none_existent_path.push("none-existent.nu");
|
||||||
|
|
||||||
|
client_connection
|
||||||
|
.sender
|
||||||
|
.send(Message::Request(lsp_server::Request {
|
||||||
|
id: 2.into(),
|
||||||
|
method: GotoDefinition::METHOD.to_string(),
|
||||||
|
params: serde_json::to_value(GotoDefinitionParams {
|
||||||
|
text_document_position_params: TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier {
|
||||||
|
uri: Url::from_file_path(none_existent_path).unwrap(),
|
||||||
|
},
|
||||||
|
position: lsp_types::Position {
|
||||||
|
line: 0,
|
||||||
|
character: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
work_done_progress_params: Default::default(),
|
||||||
|
partial_result_params: Default::default(),
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let resp = client_connection
|
||||||
|
.receiver
|
||||||
|
.recv_timeout(std::time::Duration::from_secs(2))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
resp,
|
||||||
|
Message::Response(response) if response.result.is_none()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn goto_definition(uri: Url, line: u32, character: u32) -> Message {
|
||||||
|
let (client_connection, _recv) = initialize_language_server();
|
||||||
|
|
||||||
|
client_connection
|
||||||
|
.sender
|
||||||
|
.send(Message::Request(lsp_server::Request {
|
||||||
|
id: 2.into(),
|
||||||
|
method: GotoDefinition::METHOD.to_string(),
|
||||||
|
params: serde_json::to_value(GotoDefinitionParams {
|
||||||
|
text_document_position_params: TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri },
|
||||||
|
position: lsp_types::Position { line, character },
|
||||||
|
},
|
||||||
|
work_done_progress_params: Default::default(),
|
||||||
|
partial_result_params: Default::default(),
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
client_connection
|
||||||
|
.receiver
|
||||||
|
.recv_timeout(std::time::Duration::from_secs(2))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn goto_definition_of_variable() {
|
||||||
|
let mut script = fixtures();
|
||||||
|
script.push("lsp");
|
||||||
|
script.push("goto");
|
||||||
|
script.push("var.nu");
|
||||||
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
|
let resp = goto_definition(script.clone(), 2, 12);
|
||||||
|
let result = if let Message::Response(response) = resp {
|
||||||
|
response.result
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_json_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!({
|
||||||
|
"uri": script,
|
||||||
|
"range": {
|
||||||
|
"start": { "line": 0, "character": 4 },
|
||||||
|
"end": { "line": 0, "character": 12 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn goto_definition_of_command() {
|
||||||
|
let mut script = fixtures();
|
||||||
|
script.push("lsp");
|
||||||
|
script.push("goto");
|
||||||
|
script.push("command.nu");
|
||||||
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
|
let resp = goto_definition(script.clone(), 4, 1);
|
||||||
|
let result = if let Message::Response(response) = resp {
|
||||||
|
response.result
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_json_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!({
|
||||||
|
"uri": script,
|
||||||
|
"range": {
|
||||||
|
"start": { "line": 0, "character": 17 },
|
||||||
|
"end": { "line": 2, "character": 1 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn goto_definition_of_command_parameter() {
|
||||||
|
let mut script = fixtures();
|
||||||
|
script.push("lsp");
|
||||||
|
script.push("goto");
|
||||||
|
script.push("command.nu");
|
||||||
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
|
let resp = goto_definition(script.clone(), 1, 14);
|
||||||
|
let result = if let Message::Response(response) = resp {
|
||||||
|
response.result
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_json_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!({
|
||||||
|
"uri": script,
|
||||||
|
"range": {
|
||||||
|
"start": { "line": 0, "character": 11 },
|
||||||
|
"end": { "line": 0, "character": 15 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hover(uri: Url, line: u32, character: u32) -> Message {
|
||||||
|
let (client_connection, _recv) = initialize_language_server();
|
||||||
|
|
||||||
|
client_connection
|
||||||
|
.sender
|
||||||
|
.send(Message::Request(lsp_server::Request {
|
||||||
|
id: 2.into(),
|
||||||
|
method: HoverRequest::METHOD.to_string(),
|
||||||
|
params: serde_json::to_value(HoverParams {
|
||||||
|
text_document_position_params: TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri },
|
||||||
|
position: lsp_types::Position { line, character },
|
||||||
|
},
|
||||||
|
work_done_progress_params: Default::default(),
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
client_connection
|
||||||
|
.receiver
|
||||||
|
.recv_timeout(std::time::Duration::from_secs(2))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_on_variable() {
|
||||||
|
let mut script = fixtures();
|
||||||
|
script.push("lsp");
|
||||||
|
script.push("hover");
|
||||||
|
script.push("var.nu");
|
||||||
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
|
let resp = hover(script.clone(), 2, 0);
|
||||||
|
let result = if let Message::Response(response) = resp {
|
||||||
|
response.result
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_json_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!({
|
||||||
|
"contents": "table"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_on_command() {
|
||||||
|
let mut script = fixtures();
|
||||||
|
script.push("lsp");
|
||||||
|
script.push("hover");
|
||||||
|
script.push("command.nu");
|
||||||
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
|
let resp = hover(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 greeting message\n"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete(uri: Url, line: u32, character: u32) -> Message {
|
||||||
|
let (client_connection, _recv) = initialize_language_server();
|
||||||
|
|
||||||
|
client_connection
|
||||||
|
.sender
|
||||||
|
.send(Message::Request(lsp_server::Request {
|
||||||
|
id: 2.into(),
|
||||||
|
method: Completion::METHOD.to_string(),
|
||||||
|
params: serde_json::to_value(CompletionParams {
|
||||||
|
text_document_position: TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri },
|
||||||
|
position: lsp_types::Position { line, character },
|
||||||
|
},
|
||||||
|
work_done_progress_params: Default::default(),
|
||||||
|
partial_result_params: Default::default(),
|
||||||
|
context: None,
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
client_connection
|
||||||
|
.receiver
|
||||||
|
.recv_timeout(std::time::Duration::from_secs(2))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complete_on_variable() {
|
||||||
|
let mut script = fixtures();
|
||||||
|
script.push("lsp");
|
||||||
|
script.push("completion");
|
||||||
|
script.push("var.nu");
|
||||||
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
|
let resp = complete(script, 2, 9);
|
||||||
|
let result = if let Message::Response(response) = resp {
|
||||||
|
response.result
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_json_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!([
|
||||||
|
{
|
||||||
|
"label": "$greeting",
|
||||||
|
"textEdit": {
|
||||||
|
"newText": "$greeting",
|
||||||
|
"range": {
|
||||||
|
"start": { "character": 5, "line": 2 },
|
||||||
|
"end": { "character": 9, "line": 2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complete_command_with_space() {
|
||||||
|
let mut script = fixtures();
|
||||||
|
script.push("lsp");
|
||||||
|
script.push("completion");
|
||||||
|
script.push("command.nu");
|
||||||
|
let script = Url::from_file_path(script).unwrap();
|
||||||
|
|
||||||
|
let resp = complete(script, 0, 8);
|
||||||
|
let result = if let Message::Response(response) = resp {
|
||||||
|
response.result
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_json_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!([
|
||||||
|
{
|
||||||
|
"label": "config nu",
|
||||||
|
"detail": "Edit nu configurations.",
|
||||||
|
"textEdit": {
|
||||||
|
"range": {
|
||||||
|
"start": { "line": 0, "character": 0 },
|
||||||
|
"end": { "line": 0, "character": 8 },
|
||||||
|
},
|
||||||
|
"newText": "config nu"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -37,7 +37,7 @@ pub(crate) fn gather_commandline_args() -> (Vec<String>, String, Vec<String>) {
|
|||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
"--plugin-config" => args.next().map(|a| escape_quote_string(&a)),
|
"--plugin-config" => args.next().map(|a| escape_quote_string(&a)),
|
||||||
"--log-level" | "--log-target" | "--testbin" | "--threads" | "-t"
|
"--log-level" | "--log-target" | "--testbin" | "--threads" | "-t"
|
||||||
| "--include-path" | "--ide-goto-def" | "--ide-hover" | "--ide-complete"
|
| "--include-path" | "--lsp" | "--ide-goto-def" | "--ide-hover" | "--ide-complete"
|
||||||
| "--ide-check" => args.next(),
|
| "--ide-check" => args.next(),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
@ -108,6 +108,7 @@ pub(crate) fn parse_commandline_args(
|
|||||||
call.get_flag(engine_state, &mut stack, "table-mode")?;
|
call.get_flag(engine_state, &mut stack, "table-mode")?;
|
||||||
|
|
||||||
// ide flags
|
// ide flags
|
||||||
|
let lsp = call.has_flag("lsp");
|
||||||
let include_path: Option<Expression> = call.get_flag_expr("include-path");
|
let include_path: Option<Expression> = call.get_flag_expr("include-path");
|
||||||
let ide_goto_def: Option<Value> =
|
let ide_goto_def: Option<Value> =
|
||||||
call.get_flag(engine_state, &mut stack, "ide-goto-def")?;
|
call.get_flag(engine_state, &mut stack, "ide-goto-def")?;
|
||||||
@ -193,6 +194,7 @@ pub(crate) fn parse_commandline_args(
|
|||||||
ide_goto_def,
|
ide_goto_def,
|
||||||
ide_hover,
|
ide_hover,
|
||||||
ide_complete,
|
ide_complete,
|
||||||
|
lsp,
|
||||||
ide_check,
|
ide_check,
|
||||||
ide_ast,
|
ide_ast,
|
||||||
table_mode,
|
table_mode,
|
||||||
@ -229,6 +231,7 @@ pub(crate) struct NushellCliArgs {
|
|||||||
pub(crate) execute: Option<Spanned<String>>,
|
pub(crate) execute: Option<Spanned<String>>,
|
||||||
pub(crate) table_mode: Option<Value>,
|
pub(crate) table_mode: Option<Value>,
|
||||||
pub(crate) include_path: Option<Spanned<String>>,
|
pub(crate) include_path: Option<Spanned<String>>,
|
||||||
|
pub(crate) lsp: bool,
|
||||||
pub(crate) ide_goto_def: Option<Value>,
|
pub(crate) ide_goto_def: Option<Value>,
|
||||||
pub(crate) ide_hover: Option<Value>,
|
pub(crate) ide_hover: Option<Value>,
|
||||||
pub(crate) ide_complete: Option<Value>,
|
pub(crate) ide_complete: Option<Value>,
|
||||||
@ -298,7 +301,12 @@ impl Command for Nu {
|
|||||||
"start with an alternate environment config file",
|
"start with an alternate environment config file",
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.named(
|
.switch(
|
||||||
|
"lsp",
|
||||||
|
"start nu's language server protocol",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.named(
|
||||||
"ide-goto-def",
|
"ide-goto-def",
|
||||||
SyntaxShape::Int,
|
SyntaxShape::Int,
|
||||||
"go to the definition of the item at the given position",
|
"go to the definition of the item at the given position",
|
||||||
@ -309,7 +317,7 @@ impl Command for Nu {
|
|||||||
SyntaxShape::Int,
|
SyntaxShape::Int,
|
||||||
"give information about the item at the given position",
|
"give information about the item at the given position",
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.named(
|
.named(
|
||||||
"ide-complete",
|
"ide-complete",
|
||||||
SyntaxShape::Int,
|
SyntaxShape::Int,
|
||||||
|
@ -24,6 +24,7 @@ use log::Level;
|
|||||||
use miette::Result;
|
use miette::Result;
|
||||||
use nu_cli::gather_parent_env_vars;
|
use nu_cli::gather_parent_env_vars;
|
||||||
use nu_cmd_base::util::get_init_cwd;
|
use nu_cmd_base::util::get_init_cwd;
|
||||||
|
use nu_lsp::LanguageServer;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::EngineState, eval_const::create_nu_constant, report_error_new, util::BufferedReader,
|
engine::EngineState, eval_const::create_nu_constant, report_error_new, util::BufferedReader,
|
||||||
PipelineData, RawStream, Span, Value, NU_VARIABLE_ID,
|
PipelineData, RawStream, Span, Value, NU_VARIABLE_ID,
|
||||||
@ -191,6 +192,10 @@ fn main() -> Result<()> {
|
|||||||
load_standard_library(&mut engine_state)?;
|
load_standard_library(&mut engine_state)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if parsed_nu_cli_args.lsp {
|
||||||
|
return LanguageServer::initialize_stdio_connection()?.serve_requests(engine_state);
|
||||||
|
}
|
||||||
|
|
||||||
// IDE commands
|
// IDE commands
|
||||||
if let Some(ide_goto_def) = parsed_nu_cli_args.ide_goto_def {
|
if let Some(ide_goto_def) = parsed_nu_cli_args.ide_goto_def {
|
||||||
ide::goto_def(&mut engine_state, &script_name, &ide_goto_def);
|
ide::goto_def(&mut engine_state, &script_name, &ide_goto_def);
|
||||||
|
1
tests/fixtures/lsp/completion/command.nu
vendored
Normal file
1
tests/fixtures/lsp/completion/command.nu
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
config n
|
3
tests/fixtures/lsp/completion/var.nu
vendored
Normal file
3
tests/fixtures/lsp/completion/var.nu
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
let greeting = "Hello"
|
||||||
|
|
||||||
|
echo $gre
|
5
tests/fixtures/lsp/goto/command.nu
vendored
Normal file
5
tests/fixtures/lsp/goto/command.nu
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
def greet [name] {
|
||||||
|
$"hello ($name)"
|
||||||
|
}
|
||||||
|
|
||||||
|
greet nushell
|
3
tests/fixtures/lsp/goto/var.nu
vendored
Normal file
3
tests/fixtures/lsp/goto/var.nu
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
let greeting = "hello"
|
||||||
|
|
||||||
|
print $"($greeting) world!"
|
4
tests/fixtures/lsp/hover/command.nu
vendored
Normal file
4
tests/fixtures/lsp/hover/command.nu
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Renders some greeting message
|
||||||
|
def hello [] {}
|
||||||
|
|
||||||
|
hello
|
3
tests/fixtures/lsp/hover/var.nu
vendored
Normal file
3
tests/fixtures/lsp/hover/var.nu
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
let my_var = (ls)
|
||||||
|
|
||||||
|
$my_var
|
Loading…
Reference in New Issue
Block a user