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

Commit

Permalink
feat: textDocument/inlayHint
Browse files Browse the repository at this point in the history
  • Loading branch information
jokeyrhyme committed Oct 12, 2023
1 parent 36fabfd commit 4b02a9b
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 34 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ lsp-textdocument = { git = "https://github.com/GiveMe-A-Name/lsp-textdocument.gi
mktemp = "0.5"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1.32.0", features = ["fs", "io-std", "macros", "process", "rt-multi-thread"] }
tokio = { version = "1.32.0", features = ["fs", "io-std", "macros", "process", "rt-multi-thread", "time"] }
tower-lsp = "0.20.0"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ Language Server Protocol implementation for nushell
- [x] [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)
- [x] [textDocument/inlayHint](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_inlayHint) -> `nu --ide-check`
- [x] [textDocument/publishDiagnostics](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics) -> `nu --ide-check`
- [x] [workspace/configuration](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration)
- [x] [workspace/didChangeConfiguration](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeConfiguration)
- [ ] [textDocument/inlayHint](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_inlayHint) -> `nu --ide-check`
- [ ] raise a PR for `vscode-nushell-lang` to replace its wrapper/glue code with `nuls`

### stretch goals
Expand Down
18 changes: 15 additions & 3 deletions src/backend/language_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,15 @@ impl LanguageServer for Backend {

Ok(InitializeResult {
capabilities: ServerCapabilities {
// `nu --ide-complete`
completion_provider: Some(CompletionOptions::default()),
// `nu --ide-goto-def`
definition_provider: Some(OneOf::Left(true)),
// `nu --ide-hover`
hover_provider: Some(HoverProviderCapability::Simple(true)),
inlay_hint_provider: Some(OneOf::Right(InlayHintServerCapabilities::Options(
InlayHintOptions {
resolve_provider: Some(false),
..Default::default()
},
))),
// TODO: what do we do when the client doesn't support UTF-16 ?
// lsp-textdocument crate requires UTF-16
position_encoding: Some(PositionEncodingKind::UTF16),
Expand Down Expand Up @@ -286,4 +289,13 @@ impl LanguageServer for Backend {
range,
}))
}

async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
let document_inlay_hints = self.document_inlay_hints.read().map_err(|e| {
tower_lsp::jsonrpc::Error::invalid_params(format!(
"cannot read from inlay hints cache: {e:?}"
))
})?;
Ok(document_inlay_hints.get(&params.text_document.uri).cloned())
}
}
32 changes: 22 additions & 10 deletions src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ use std::time::{Duration, Instant};
use std::{ffi::OsStr, sync::RwLock};

pub(crate) mod language_server;
use crate::nu::{IdeCheckHint, IdeCheckResponse};
use crate::{
error::map_err_to_internal_error,
nu::{run_compiler, IdeCheck, IdeCheckDiagnostic, IdeSettings},
nu::{run_compiler, IdeCheckDiagnostic, IdeSettings},
};
use lsp_textdocument::{FullTextDocument, TextDocuments};

Expand All @@ -25,6 +26,7 @@ pub(crate) struct Backend {
can_publish_diagnostics: OnceLock<bool>,
client: Client,
documents: RwLock<TextDocuments>,
document_inlay_hints: RwLock<HashMap<Url, Vec<InlayHint>>>,
document_settings: RwLock<HashMap<Url, IdeSettings>>,
global_settings: RwLock<IdeSettings>,
last_validated: RwLock<Instant>,
Expand Down Expand Up @@ -112,6 +114,7 @@ impl Backend {
can_publish_diagnostics: OnceLock::new(),
client,
documents: RwLock::new(TextDocuments::new()),
document_inlay_hints: RwLock::new(HashMap::new()),
document_settings: RwLock::new(HashMap::new()),
global_settings: RwLock::new(IdeSettings::default()),
last_validated: RwLock::new(Instant::now()),
Expand Down Expand Up @@ -227,23 +230,17 @@ impl Backend {
let text = self.for_document(uri, &|doc| String::from(doc.get_content(None)))?;

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

let ide_checks: Vec<IdeCheck> = output
.stdout
.lines()
.filter_map(|l| serde_json::from_slice(l.as_bytes()).ok())
.collect();
let ide_checks = IdeCheckResponse::from_compiler_response(&output);

let (diagnostics, version) = self.for_document(uri, &|doc| {
(
ide_checks
.diagnostics
.iter()
.filter_map(|c| match c {
IdeCheck::Diagnostic(d) => Some(d),
IdeCheck::Hint(_) => None,
})
.map(|d| IdeCheckDiagnostic::to_diagnostic(d, doc, uri))
.collect::<Vec<_>>(),
doc.version(),
Expand All @@ -254,6 +251,21 @@ impl Backend {
.publish_diagnostics(uri.clone(), diagnostics, Some(version))
.await;

if show_inferred_types {
let inlay_hints = self.for_document(uri, &|doc| {
ide_checks
.inlay_hints
.iter()
.map(|d| IdeCheckHint::to_inlay_hint(d, doc))
.collect::<Vec<_>>()
})?;

let mut documents = self.document_inlay_hints.write().map_err(|e| {
map_err_to_internal_error(&e, format!("cannot write inlay hints cache: {e:?}"))
})?;
documents.insert(uri.clone(), inlay_hints);
}

Ok(())
}
}
Expand Down
139 changes: 120 additions & 19 deletions src/nu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ use std::{ffi::OsStr, path::PathBuf, time::Duration};

use lsp_textdocument::FullTextDocument;
use serde::Deserialize;
use tokio::fs;
use tokio::{fs, time::timeout};
use tower_lsp::lsp_types::{
CompletionItem, CompletionItemKind, CompletionResponse, DiagnosticSeverity, Range, Url,
CompletionItem, CompletionItemKind, CompletionResponse, DiagnosticSeverity, InlayHint,
InlayHintKind, Range, Url,
};
use tower_lsp::{jsonrpc::Result, lsp_types::Diagnostic};

Expand All @@ -17,7 +18,7 @@ pub(crate) enum IdeCheck {
Hint(IdeCheckHint),
}

#[derive(Debug, Deserialize, PartialEq)]
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub(crate) struct IdeCheckDiagnostic {
pub message: String,
pub severity: IdeDiagnosticSeverity,
Expand All @@ -38,11 +39,63 @@ impl IdeCheckDiagnostic {
}
}

#[derive(Debug, Deserialize, PartialEq)]
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub(crate) struct IdeCheckHint {
pub position: IdeSpan,
pub typename: String,
}
impl IdeCheckHint {
pub fn to_inlay_hint(&self, doc: &FullTextDocument) -> InlayHint {
InlayHint {
position: doc.position_at(self.position.end),
label: tower_lsp::lsp_types::InlayHintLabel::String(format!(": {}", &self.typename)),
kind: Some(InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
}
}
}

#[derive(Debug, PartialEq)]
pub(crate) struct IdeCheckResponse {
pub diagnostics: Vec<IdeCheckDiagnostic>,
pub inlay_hints: Vec<IdeCheckHint>,
}
impl IdeCheckResponse {
pub fn from_compiler_response(value: &CompilerResponse) -> Self {
let ide_checks: Vec<IdeCheck> = value
.stdout
.lines()
.filter_map(|l| serde_json::from_slice(l.as_bytes()).ok())
.collect();

let diagnostics = ide_checks
.iter()
.filter_map(|c| match c {
IdeCheck::Diagnostic(d) => Some(d),
IdeCheck::Hint(_) => None,
})
.cloned()
.collect::<Vec<_>>();

let inlay_hints = ide_checks
.iter()
.filter_map(|c| match c {
IdeCheck::Diagnostic(_) => None,
IdeCheck::Hint(h) => Some(h),
})
.cloned()
.collect::<Vec<_>>();

Self {
diagnostics,
inlay_hints,
}
}
}

#[derive(Deserialize)]
pub(crate) struct IdeComplete {
Expand Down Expand Up @@ -82,7 +135,7 @@ impl From<IdeComplete> for CompletionResponse {
}
}

#[derive(Debug, Deserialize, PartialEq)]
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub(crate) enum IdeDiagnosticSeverity {
Error,
Warning,
Expand Down Expand Up @@ -113,13 +166,12 @@ pub(crate) struct IdeHover {
pub hover: String,
pub span: Option<IdeSpan>,
}
#[derive(Debug, Deserialize, PartialEq)]
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub(crate) struct IdeSpan {
pub end: u32,
pub start: u32,
}

#[allow(dead_code)]
#[derive(Clone, Debug, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub(crate) struct IdeSettings {
Expand All @@ -141,7 +193,6 @@ impl Default for IdeSettings {
}
}

#[allow(dead_code)]
#[derive(Clone, Debug, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub(crate) struct IdeSettingsHints {
Expand All @@ -155,6 +206,7 @@ impl Default for IdeSettingsHints {
}
}

#[derive(Debug)]
pub(crate) struct CompilerResponse {
pub cmdline: String,
pub stdout: String,
Expand All @@ -167,8 +219,6 @@ pub(crate) async fn run_compiler(
settings: IdeSettings,
uri: &Url,
) -> Result<CompilerResponse> {
// TODO: support allowErrors and label options like vscode-nushell-lang?

let max_number_of_problems = format!("{}", settings.max_number_of_problems);
let max_number_of_problems_flag = OsStr::new(&max_number_of_problems);
if flags.contains(&OsStr::new("--ide-check")) {
Expand Down Expand Up @@ -210,16 +260,27 @@ pub(crate) async fn run_compiler(

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

// TODO: honour max_nushell_invocation_time like vscode-nushell-lang
// TODO: call settings.nushell_executable_path

// TODO: call nushell Rust code directly instead of via separate process,
// https://github.com/jokeyrhyme/nuls/issues/7
let output = tokio::process::Command::new("nu")
.args(flags)
.output()
.await
.map_err(|e| map_err_to_internal_error(e, format!("`{cmdline}` failed")))?;
let output = timeout(
settings.max_nushell_invocation_time,
tokio::process::Command::new(settings.nushell_executable_path)
.args(flags)
.output(),
)
.await
.map_err(|e| {
map_err_to_internal_error(
e,
format!(
"`{cmdline}` timeout, {:?} elapsed",
&settings.max_nushell_invocation_time
),
)
})?
.map_err(|e| map_err_to_internal_error(e, format!("`{cmdline}` failed")))?;
// intentionally skip checking the ExitStatus, we always want stdout regardless

let stdout = String::from_utf8(output.stdout).map_err(|e| {
map_err_to_parse_error(e, format!("`{cmdline}` did not return valid UTF-8"))
})?;
Expand Down Expand Up @@ -285,7 +346,7 @@ mod tests {
}

#[tokio::test]
async fn completion_ok() {
async fn run_compiler_for_completion_ok() {
let output = run_compiler(
"wh",
vec![OsStr::new("--ide-complete"), OsStr::new(&format!("{}", 2))],
Expand Down Expand Up @@ -315,4 +376,44 @@ mod tests {
unreachable!();
}
}

#[tokio::test]
async fn run_compiler_for_diagnostic_ok() {
let doc = FullTextDocument::new(
String::from("nushell"),
1,
String::from(
"
let foo = ['one', 'two', 'three']
ls ||
",
),
);
let uri = Url::parse("file:///foo.nu").expect("unable to parse test URL");
let output = run_compiler(
doc.get_content(None),
vec![OsStr::new("--ide-check")],
IdeSettings::default(),
&uri,
)
.await
.expect("unable to run `nu --ide-check ...`");

let got = IdeCheckResponse::from_compiler_response(&output);

assert_eq!(
got,
IdeCheckResponse {
diagnostics: vec![IdeCheckDiagnostic {
message: String::from("The '||' operator is not supported in Nushell"),
severity: IdeDiagnosticSeverity::Error,
span: IdeSpan { end: 72, start: 70 }
}],
inlay_hints: vec![IdeCheckHint {
position: IdeSpan { end: 24, start: 21 },
typename: String::from("list<string>")
}],
}
);
}
}

0 comments on commit 4b02a9b

Please sign in to comment.