nushell/crates/nu-lsp/src/notification.rs
zc he 6cdc9e3b77
Fix LSP non-ascii characters offset issues. (#14002)
<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->
This PR is supposed to fix #13582, #11522, as well as related goto
definition/reference issues (wrong position if non ascii characters
ahead).

# Description
<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->

<img width="411" alt="image"
src="https://github.com/user-attachments/assets/9a81953c-81b2-490d-a842-14ccaefd6972">

Changes:
1. span/completion should use byte offset instead of character index
2. lsp Postions related ops in Ropey remain to use character index

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

Should be none, tested in neovim with config:
```lua
require("lspconfig").nushell.setup({
  cmd = {
    "nu",
    "-I",
    vim.fn.getcwd(),
    "--no-config-file",
    "--lsp",
  },
  filetypes = { "nu" },
})
```

# 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 toolkit.nu; toolkit test stdlib"` 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
> ```
-->

tests::complete_command_with_utf_line parameters fixed to align with
true lsp requests (in character index, not byte).
As for the issue_11522.nu, manually tested:

<img width="520" alt="image"
src="https://github.com/user-attachments/assets/45496ba8-5a2d-4998-9190-d7bde31ee72c">


# 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.
-->
2024-10-29 06:35:37 -05:00

238 lines
8.1 KiB
Rust

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(&param.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 {
Some(range) => {
entry.and_modify(|rope| {
let start = Self::lsp_position_to_location(
&range.start,
rope,
&self.position_encoding,
);
let end = Self::lsp_position_to_location(
&range.end,
rope,
&self.position_encoding,
);
rope.remove(start..end);
rope.insert(start, &content_change.text);
});
}
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, open_unchecked, update};
#[test]
fn hover_correct_documentation_on_let() {
let (client_connection, _recv) = initialize_language_server(None);
let mut script = fixtures();
script.push("lsp");
script.push("hover");
script.push("var.nu");
let script = Url::from_file_path(script).unwrap();
open_unchecked(&client_connection, script.clone());
let resp = hover(&client_connection, script.clone(), 0, 0);
let result = if let Message::Response(response) = resp {
response.result
} else {
panic!()
};
assert_json_eq!(
result,
serde_json::json!({
"contents": {
"kind": "markdown",
"value": "Create a variable and give it a value.\n\nThis command is a parser keyword. For details, check:\n https://www.nushell.sh/book/thinking_in_nu.html\n### Usage \n```nu\n let {flags} <var_name> <initial_value>\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n\n### Parameters\n\n `var_name: any` - Variable name.\n\n `initial_value: any` - Equals sign followed by value.\n\n\n### Input/output types\n\n```nu\n any | nothing\n\n```\n### Example(s)\n Set a variable to a value\n```nu\n let x = 10\n```\n Set a variable to the result of an expression\n```nu\n let x = 10 + 100\n```\n Set a variable based on the condition\n```nu\n let x = if false { -1 } else { 1 }\n```\n"
}
})
);
}
#[test]
fn hover_on_command_after_full_content_change() {
let (client_connection, _recv) = initialize_language_server(None);
let mut script = fixtures();
script.push("lsp");
script.push("hover");
script.push("command.nu");
let script = Url::from_file_path(script).unwrap();
open_unchecked(&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": "Renders some updated greeting message\n### Usage \n```nu\n hello {flags}\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n"
}
})
);
}
#[test]
fn hover_on_command_after_partial_content_change() {
let (client_connection, _recv) = initialize_language_server(None);
let mut script = fixtures();
script.push("lsp");
script.push("hover");
script.push("command.nu");
let script = Url::from_file_path(script).unwrap();
open_unchecked(&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": "Renders some updated greeting message\n### Usage \n```nu\n hello {flags}\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n"
}
})
);
}
#[test]
fn open_document_with_utf_char() {
let (client_connection, _recv) = initialize_language_server(None);
let mut script = fixtures();
script.push("lsp");
script.push("notifications");
script.push("issue_11522.nu");
let script = Url::from_file_path(script).unwrap();
let result = open(&client_connection, script);
assert_eq!(result.map(|_| ()), Ok(()))
}
}