Skip to content
This repository was archived by the owner on Oct 24, 2023. It is now read-only.

Commit 4b02a9b

Browse files
committed
feat: textDocument/inlayHint
1 parent 36fabfd commit 4b02a9b

File tree

5 files changed

+159
-34
lines changed

5 files changed

+159
-34
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ lsp-textdocument = { git = "https://github.com/GiveMe-A-Name/lsp-textdocument.gi
1111
mktemp = "0.5"
1212
serde = { version = "1", features = ["derive"] }
1313
serde_json = "1"
14-
tokio = { version = "1.32.0", features = ["fs", "io-std", "macros", "process", "rt-multi-thread"] }
14+
tokio = { version = "1.32.0", features = ["fs", "io-std", "macros", "process", "rt-multi-thread", "time"] }
1515
tower-lsp = "0.20.0"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ Language Server Protocol implementation for nushell
3939
- [x] [textDocument/didChange](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didChange),
4040
[textDocument/didClose](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didClose),
4141
and [textDocument/didOpen](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didOpen)
42+
- [x] [textDocument/inlayHint](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_inlayHint) -> `nu --ide-check`
4243
- [x] [textDocument/publishDiagnostics](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics) -> `nu --ide-check`
4344
- [x] [workspace/configuration](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration)
4445
- [x] [workspace/didChangeConfiguration](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeConfiguration)
45-
- [ ] [textDocument/inlayHint](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_inlayHint) -> `nu --ide-check`
4646
- [ ] raise a PR for `vscode-nushell-lang` to replace its wrapper/glue code with `nuls`
4747

4848
### stretch goals

src/backend/language_server.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,15 @@ impl LanguageServer for Backend {
105105

106106
Ok(InitializeResult {
107107
capabilities: ServerCapabilities {
108-
// `nu --ide-complete`
109108
completion_provider: Some(CompletionOptions::default()),
110-
// `nu --ide-goto-def`
111109
definition_provider: Some(OneOf::Left(true)),
112-
// `nu --ide-hover`
113110
hover_provider: Some(HoverProviderCapability::Simple(true)),
111+
inlay_hint_provider: Some(OneOf::Right(InlayHintServerCapabilities::Options(
112+
InlayHintOptions {
113+
resolve_provider: Some(false),
114+
..Default::default()
115+
},
116+
))),
114117
// TODO: what do we do when the client doesn't support UTF-16 ?
115118
// lsp-textdocument crate requires UTF-16
116119
position_encoding: Some(PositionEncodingKind::UTF16),
@@ -286,4 +289,13 @@ impl LanguageServer for Backend {
286289
range,
287290
}))
288291
}
292+
293+
async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
294+
let document_inlay_hints = self.document_inlay_hints.read().map_err(|e| {
295+
tower_lsp::jsonrpc::Error::invalid_params(format!(
296+
"cannot read from inlay hints cache: {e:?}"
297+
))
298+
})?;
299+
Ok(document_inlay_hints.get(&params.text_document.uri).cloned())
300+
}
289301
}

src/backend/mod.rs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ use std::time::{Duration, Instant};
44
use std::{ffi::OsStr, sync::RwLock};
55

66
pub(crate) mod language_server;
7+
use crate::nu::{IdeCheckHint, IdeCheckResponse};
78
use crate::{
89
error::map_err_to_internal_error,
9-
nu::{run_compiler, IdeCheck, IdeCheckDiagnostic, IdeSettings},
10+
nu::{run_compiler, IdeCheckDiagnostic, IdeSettings},
1011
};
1112
use lsp_textdocument::{FullTextDocument, TextDocuments};
1213

@@ -25,6 +26,7 @@ pub(crate) struct Backend {
2526
can_publish_diagnostics: OnceLock<bool>,
2627
client: Client,
2728
documents: RwLock<TextDocuments>,
29+
document_inlay_hints: RwLock<HashMap<Url, Vec<InlayHint>>>,
2830
document_settings: RwLock<HashMap<Url, IdeSettings>>,
2931
global_settings: RwLock<IdeSettings>,
3032
last_validated: RwLock<Instant>,
@@ -112,6 +114,7 @@ impl Backend {
112114
can_publish_diagnostics: OnceLock::new(),
113115
client,
114116
documents: RwLock::new(TextDocuments::new()),
117+
document_inlay_hints: RwLock::new(HashMap::new()),
115118
document_settings: RwLock::new(HashMap::new()),
116119
global_settings: RwLock::new(IdeSettings::default()),
117120
last_validated: RwLock::new(Instant::now()),
@@ -227,23 +230,17 @@ impl Backend {
227230
let text = self.for_document(uri, &|doc| String::from(doc.get_content(None)))?;
228231

229232
let ide_settings = self.get_document_settings(uri).await?;
233+
let show_inferred_types = ide_settings.hints.show_inferred_types;
230234
let output =
231235
run_compiler(&text, vec![OsStr::new("--ide-check")], ide_settings, uri).await?;
232236

233-
let ide_checks: Vec<IdeCheck> = output
234-
.stdout
235-
.lines()
236-
.filter_map(|l| serde_json::from_slice(l.as_bytes()).ok())
237-
.collect();
237+
let ide_checks = IdeCheckResponse::from_compiler_response(&output);
238238

239239
let (diagnostics, version) = self.for_document(uri, &|doc| {
240240
(
241241
ide_checks
242+
.diagnostics
242243
.iter()
243-
.filter_map(|c| match c {
244-
IdeCheck::Diagnostic(d) => Some(d),
245-
IdeCheck::Hint(_) => None,
246-
})
247244
.map(|d| IdeCheckDiagnostic::to_diagnostic(d, doc, uri))
248245
.collect::<Vec<_>>(),
249246
doc.version(),
@@ -254,6 +251,21 @@ impl Backend {
254251
.publish_diagnostics(uri.clone(), diagnostics, Some(version))
255252
.await;
256253

254+
if show_inferred_types {
255+
let inlay_hints = self.for_document(uri, &|doc| {
256+
ide_checks
257+
.inlay_hints
258+
.iter()
259+
.map(|d| IdeCheckHint::to_inlay_hint(d, doc))
260+
.collect::<Vec<_>>()
261+
})?;
262+
263+
let mut documents = self.document_inlay_hints.write().map_err(|e| {
264+
map_err_to_internal_error(&e, format!("cannot write inlay hints cache: {e:?}"))
265+
})?;
266+
documents.insert(uri.clone(), inlay_hints);
267+
}
268+
257269
Ok(())
258270
}
259271
}

src/nu.rs

Lines changed: 120 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ use std::{ffi::OsStr, path::PathBuf, time::Duration};
22

33
use lsp_textdocument::FullTextDocument;
44
use serde::Deserialize;
5-
use tokio::fs;
5+
use tokio::{fs, time::timeout};
66
use tower_lsp::lsp_types::{
7-
CompletionItem, CompletionItemKind, CompletionResponse, DiagnosticSeverity, Range, Url,
7+
CompletionItem, CompletionItemKind, CompletionResponse, DiagnosticSeverity, InlayHint,
8+
InlayHintKind, Range, Url,
89
};
910
use tower_lsp::{jsonrpc::Result, lsp_types::Diagnostic};
1011

@@ -17,7 +18,7 @@ pub(crate) enum IdeCheck {
1718
Hint(IdeCheckHint),
1819
}
1920

20-
#[derive(Debug, Deserialize, PartialEq)]
21+
#[derive(Clone, Debug, Deserialize, PartialEq)]
2122
pub(crate) struct IdeCheckDiagnostic {
2223
pub message: String,
2324
pub severity: IdeDiagnosticSeverity,
@@ -38,11 +39,63 @@ impl IdeCheckDiagnostic {
3839
}
3940
}
4041

41-
#[derive(Debug, Deserialize, PartialEq)]
42+
#[derive(Clone, Debug, Deserialize, PartialEq)]
4243
pub(crate) struct IdeCheckHint {
4344
pub position: IdeSpan,
4445
pub typename: String,
4546
}
47+
impl IdeCheckHint {
48+
pub fn to_inlay_hint(&self, doc: &FullTextDocument) -> InlayHint {
49+
InlayHint {
50+
position: doc.position_at(self.position.end),
51+
label: tower_lsp::lsp_types::InlayHintLabel::String(format!(": {}", &self.typename)),
52+
kind: Some(InlayHintKind::TYPE),
53+
text_edits: None,
54+
tooltip: None,
55+
padding_left: None,
56+
padding_right: None,
57+
data: None,
58+
}
59+
}
60+
}
61+
62+
#[derive(Debug, PartialEq)]
63+
pub(crate) struct IdeCheckResponse {
64+
pub diagnostics: Vec<IdeCheckDiagnostic>,
65+
pub inlay_hints: Vec<IdeCheckHint>,
66+
}
67+
impl IdeCheckResponse {
68+
pub fn from_compiler_response(value: &CompilerResponse) -> Self {
69+
let ide_checks: Vec<IdeCheck> = value
70+
.stdout
71+
.lines()
72+
.filter_map(|l| serde_json::from_slice(l.as_bytes()).ok())
73+
.collect();
74+
75+
let diagnostics = ide_checks
76+
.iter()
77+
.filter_map(|c| match c {
78+
IdeCheck::Diagnostic(d) => Some(d),
79+
IdeCheck::Hint(_) => None,
80+
})
81+
.cloned()
82+
.collect::<Vec<_>>();
83+
84+
let inlay_hints = ide_checks
85+
.iter()
86+
.filter_map(|c| match c {
87+
IdeCheck::Diagnostic(_) => None,
88+
IdeCheck::Hint(h) => Some(h),
89+
})
90+
.cloned()
91+
.collect::<Vec<_>>();
92+
93+
Self {
94+
diagnostics,
95+
inlay_hints,
96+
}
97+
}
98+
}
4699

47100
#[derive(Deserialize)]
48101
pub(crate) struct IdeComplete {
@@ -82,7 +135,7 @@ impl From<IdeComplete> for CompletionResponse {
82135
}
83136
}
84137

85-
#[derive(Debug, Deserialize, PartialEq)]
138+
#[derive(Clone, Debug, Deserialize, PartialEq)]
86139
pub(crate) enum IdeDiagnosticSeverity {
87140
Error,
88141
Warning,
@@ -113,13 +166,12 @@ pub(crate) struct IdeHover {
113166
pub hover: String,
114167
pub span: Option<IdeSpan>,
115168
}
116-
#[derive(Debug, Deserialize, PartialEq)]
169+
#[derive(Clone, Debug, Deserialize, PartialEq)]
117170
pub(crate) struct IdeSpan {
118171
pub end: u32,
119172
pub start: u32,
120173
}
121174

122-
#[allow(dead_code)]
123175
#[derive(Clone, Debug, Deserialize)]
124176
#[serde(default, rename_all = "camelCase")]
125177
pub(crate) struct IdeSettings {
@@ -141,7 +193,6 @@ impl Default for IdeSettings {
141193
}
142194
}
143195

144-
#[allow(dead_code)]
145196
#[derive(Clone, Debug, Deserialize)]
146197
#[serde(default, rename_all = "camelCase")]
147198
pub(crate) struct IdeSettingsHints {
@@ -155,6 +206,7 @@ impl Default for IdeSettingsHints {
155206
}
156207
}
157208

209+
#[derive(Debug)]
158210
pub(crate) struct CompilerResponse {
159211
pub cmdline: String,
160212
pub stdout: String,
@@ -167,8 +219,6 @@ pub(crate) async fn run_compiler(
167219
settings: IdeSettings,
168220
uri: &Url,
169221
) -> Result<CompilerResponse> {
170-
// TODO: support allowErrors and label options like vscode-nushell-lang?
171-
172222
let max_number_of_problems = format!("{}", settings.max_number_of_problems);
173223
let max_number_of_problems_flag = OsStr::new(&max_number_of_problems);
174224
if flags.contains(&OsStr::new("--ide-check")) {
@@ -210,16 +260,27 @@ pub(crate) async fn run_compiler(
210260

211261
let cmdline = format!("nu {flags:?}");
212262

213-
// TODO: honour max_nushell_invocation_time like vscode-nushell-lang
214-
// TODO: call settings.nushell_executable_path
215-
216263
// TODO: call nushell Rust code directly instead of via separate process,
217264
// https://github.com/jokeyrhyme/nuls/issues/7
218-
let output = tokio::process::Command::new("nu")
219-
.args(flags)
220-
.output()
221-
.await
222-
.map_err(|e| map_err_to_internal_error(e, format!("`{cmdline}` failed")))?;
265+
let output = timeout(
266+
settings.max_nushell_invocation_time,
267+
tokio::process::Command::new(settings.nushell_executable_path)
268+
.args(flags)
269+
.output(),
270+
)
271+
.await
272+
.map_err(|e| {
273+
map_err_to_internal_error(
274+
e,
275+
format!(
276+
"`{cmdline}` timeout, {:?} elapsed",
277+
&settings.max_nushell_invocation_time
278+
),
279+
)
280+
})?
281+
.map_err(|e| map_err_to_internal_error(e, format!("`{cmdline}` failed")))?;
282+
// intentionally skip checking the ExitStatus, we always want stdout regardless
283+
223284
let stdout = String::from_utf8(output.stdout).map_err(|e| {
224285
map_err_to_parse_error(e, format!("`{cmdline}` did not return valid UTF-8"))
225286
})?;
@@ -285,7 +346,7 @@ mod tests {
285346
}
286347

287348
#[tokio::test]
288-
async fn completion_ok() {
349+
async fn run_compiler_for_completion_ok() {
289350
let output = run_compiler(
290351
"wh",
291352
vec![OsStr::new("--ide-complete"), OsStr::new(&format!("{}", 2))],
@@ -315,4 +376,44 @@ mod tests {
315376
unreachable!();
316377
}
317378
}
379+
380+
#[tokio::test]
381+
async fn run_compiler_for_diagnostic_ok() {
382+
let doc = FullTextDocument::new(
383+
String::from("nushell"),
384+
1,
385+
String::from(
386+
"
387+
let foo = ['one', 'two', 'three']
388+
ls ||
389+
",
390+
),
391+
);
392+
let uri = Url::parse("file:///foo.nu").expect("unable to parse test URL");
393+
let output = run_compiler(
394+
doc.get_content(None),
395+
vec![OsStr::new("--ide-check")],
396+
IdeSettings::default(),
397+
&uri,
398+
)
399+
.await
400+
.expect("unable to run `nu --ide-check ...`");
401+
402+
let got = IdeCheckResponse::from_compiler_response(&output);
403+
404+
assert_eq!(
405+
got,
406+
IdeCheckResponse {
407+
diagnostics: vec![IdeCheckDiagnostic {
408+
message: String::from("The '||' operator is not supported in Nushell"),
409+
severity: IdeDiagnosticSeverity::Error,
410+
span: IdeSpan { end: 72, start: 70 }
411+
}],
412+
inlay_hints: vec![IdeCheckHint {
413+
position: IdeSpan { end: 24, start: 21 },
414+
typename: String::from("list<string>")
415+
}],
416+
}
417+
);
418+
}
318419
}

0 commit comments

Comments
 (0)