Skip to content

Commit 28a535f

Browse files
divybotlittledivy
andauthored
feat(lsp): add inferred type request (#35099)
Adds a `deno/inferredType` custom LSP request that returns the full TypeScript QuickInfo display string and source range for the symbol at a text document position. The TypeScript quick-info bridge now passes an explicit large `maximumLength`, so hover and inferred-type callers avoid the default truncation. Also advertises a `refactor.extract.inferredType` code action titled "Copy inferred type". The code action carries the precomputed inferred type text as command data, giving clients a discoverable native entry point for copying full inferred type text while allowing editor extensions to own clipboard access. Companion VS Code extension PR: denoland/vscode_deno#1381 Verification: - `cargo build --bin deno` - `cargo fmt --check` - `cargo test -p integration_tests --test integration lsp_inferred_type -- --nocapture` - `cargo build -p test_server` - `cargo test -p integration_tests --test integration lsp_hover -- --nocapture` Refs #23901 Closes denoland/divybot#425 --------- Co-authored-by: divybot <divybot@users.noreply.github.com> Co-authored-by: Divy Srivastava <me@littledivy.com>
1 parent 0a89dad commit 28a535f

8 files changed

Lines changed: 318 additions & 10 deletions

File tree

cli/lsp/capabilities.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,21 @@
55
//! language server, which helps determine what messages are sent from the
66
//! client.
77
//!
8+
use std::sync::LazyLock;
9+
810
use deno_core::serde_json::json;
911
use tower_lsp::lsp_types::*;
1012

1113
use super::refactor::ALL_KNOWN_REFACTOR_ACTION_KINDS;
1214
use super::semantic_tokens::get_legend;
1315

16+
pub static INFERRED_TYPE_CODE_ACTION_KIND: LazyLock<CodeActionKind> =
17+
LazyLock::new(|| {
18+
[CodeActionKind::REFACTOR_EXTRACT.as_str(), "inferredType"]
19+
.join(".")
20+
.into()
21+
});
22+
1423
fn code_action_capabilities(
1524
client_capabilities: &ClientCapabilities,
1625
) -> CodeActionProviderCapability {
@@ -23,6 +32,7 @@ fn code_action_capabilities(
2332
let mut code_action_kinds = vec![
2433
CodeActionKind::QUICKFIX,
2534
CodeActionKind::REFACTOR,
35+
INFERRED_TYPE_CODE_ACTION_KIND.clone(),
2636
CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
2737
];
2838
code_action_kinds.extend(

cli/lsp/language_server.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ use super::analysis::fix_ts_import_changes;
6262
use super::analysis::ts_changes_to_edit;
6363
use super::cache::LspCache;
6464
use super::capabilities;
65+
use super::capabilities::INFERRED_TYPE_CODE_ACTION_KIND;
6566
use super::capabilities::semantic_tokens_registration_options;
6667
use super::client::Client;
6768
use super::code_lens;
@@ -81,6 +82,7 @@ use super::jsr::CliJsrSearchApi;
8182
use super::logging::lsp_log;
8283
use super::logging::lsp_warn;
8384
use super::lsp_custom;
85+
use super::lsp_custom::INFERRED_TYPE_COMMAND;
8486
use super::lsp_custom::TaskDefinition;
8587
use super::npm::CliNpmSearchApi;
8688
use super::parent_process_checker;
@@ -500,6 +502,32 @@ impl LanguageServer {
500502
Ok(Some(self.inner.read().await.get_performance()))
501503
}
502504

505+
pub async fn inferred_type(
506+
&self,
507+
params: Option<Value>,
508+
token: CancellationToken,
509+
) -> LspResult<Option<Value>> {
510+
self.init_flag.wait_raised().await;
511+
match params.map(serde_json::from_value) {
512+
Some(Ok(params)) => Ok(Some(
513+
serde_json::to_value(
514+
self
515+
.inner
516+
.read()
517+
.await
518+
.inferred_type(params, &token)
519+
.await?,
520+
)
521+
.map_err(|err| {
522+
error!("Failed to serialize inferred_type response: {:#}", err);
523+
LspError::internal_error()
524+
})?,
525+
)),
526+
Some(Err(err)) => Err(LspError::invalid_params(err.to_string())),
527+
None => Err(LspError::invalid_params("Missing parameters")),
528+
}
529+
}
530+
503531
pub async fn task_definitions(
504532
&self,
505533
_token: CancellationToken,
@@ -2187,6 +2215,63 @@ impl Inner {
21872215
let mut deno_lint_actions = Vec::new();
21882216
let mut deno_test_actions = Vec::new();
21892217
let mut includes_no_cache = false;
2218+
if params.context.only.as_ref().is_none_or(|only| {
2219+
only.iter().any(|kind| {
2220+
INFERRED_TYPE_CODE_ACTION_KIND
2221+
.as_str()
2222+
.starts_with(kind.as_str())
2223+
})
2224+
}) {
2225+
let snapshot = self.snapshot();
2226+
let response = self
2227+
.ts_server
2228+
.provide_inferred_type(&module, params.range.start, snapshot, token)
2229+
.await
2230+
.map_err(|err| {
2231+
if token.is_cancelled() {
2232+
LspError::request_cancelled()
2233+
} else {
2234+
error!(
2235+
"Unable to get inferred type code action from TypeScript: {:#}",
2236+
err
2237+
);
2238+
LspError::internal_error()
2239+
}
2240+
})?;
2241+
if let Some(response) = response {
2242+
deno_actions.push(CodeAction {
2243+
title: "Copy inferred type".to_string(),
2244+
kind: Some(INFERRED_TYPE_CODE_ACTION_KIND.clone()),
2245+
command: Some(Command {
2246+
title: "Copy inferred type".to_string(),
2247+
command: INFERRED_TYPE_COMMAND.to_string(),
2248+
arguments: Some(vec![
2249+
serde_json::to_value(lsp_custom::InferredTypeParams {
2250+
text_document: TextDocumentIdentifier {
2251+
uri: params.text_document.uri.clone(),
2252+
},
2253+
position: params.range.start,
2254+
})
2255+
.map_err(|err| {
2256+
error!(
2257+
"Failed to serialize inferred type command params: {:#}",
2258+
err
2259+
);
2260+
LspError::internal_error()
2261+
})?,
2262+
serde_json::to_value(response).map_err(|err| {
2263+
error!(
2264+
"Failed to serialize inferred type command response: {:#}",
2265+
err
2266+
);
2267+
LspError::internal_error()
2268+
})?,
2269+
]),
2270+
}),
2271+
..Default::default()
2272+
});
2273+
}
2274+
}
21902275
if self.config.specifier_enabled_for_test(&module.specifier)
21912276
&& let Some(Ok(parsed_source)) = &module
21922277
.open_data
@@ -4684,6 +4769,43 @@ impl Inner {
46844769
self.performance.measure(mark);
46854770
Ok(contents)
46864771
}
4772+
4773+
async fn inferred_type(
4774+
&self,
4775+
params: lsp_custom::InferredTypeParams,
4776+
token: &CancellationToken,
4777+
) -> LspResult<Option<lsp_custom::InferredTypeResponse>> {
4778+
let mark = self
4779+
.performance
4780+
.mark_with_args("lsp.inferred_type", &params);
4781+
let Some(document) = self.get_document(
4782+
&params.text_document.uri,
4783+
Enabled::Filter,
4784+
Exists::Enforce,
4785+
Diagnosable::Filter,
4786+
)?
4787+
else {
4788+
return Ok(None);
4789+
};
4790+
let Some(module) = self.get_primary_module(&document)? else {
4791+
return Ok(None);
4792+
};
4793+
let snapshot = self.snapshot();
4794+
let response = self
4795+
.ts_server
4796+
.provide_inferred_type(&module, params.position, snapshot, token)
4797+
.await
4798+
.map_err(|err| {
4799+
if token.is_cancelled() {
4800+
LspError::request_cancelled()
4801+
} else {
4802+
error!("Unable to get inferred type from TypeScript: {:#}", err);
4803+
LspError::internal_error()
4804+
}
4805+
})?;
4806+
self.performance.measure(mark);
4807+
Ok(response)
4808+
}
46874809
}
46884810

46894811
#[cfg(test)]

cli/lsp/lsp_custom.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,25 @@ use deno_core::serde::Serialize;
55
use tower_lsp::lsp_types as lsp;
66

77
pub const PERFORMANCE_REQUEST: &str = "deno/performance";
8+
pub const INFERRED_TYPE_REQUEST: &str = "deno/inferredType";
9+
pub const INFERRED_TYPE_COMMAND: &str = "deno.inferredType";
810
pub const TASK_REQUEST: &str = "deno/taskDefinitions";
911
pub const VIRTUAL_TEXT_DOCUMENT: &str = "deno/virtualTextDocument";
1012

13+
#[derive(Debug, Deserialize, Serialize)]
14+
#[serde(rename_all = "camelCase")]
15+
pub struct InferredTypeParams {
16+
pub text_document: lsp::TextDocumentIdentifier,
17+
pub position: lsp::Position,
18+
}
19+
20+
#[derive(Debug, Deserialize, Serialize)]
21+
#[serde(rename_all = "camelCase")]
22+
pub struct InferredTypeResponse {
23+
pub text: String,
24+
pub range: lsp::Range,
25+
}
26+
1127
#[derive(Debug, Deserialize, Serialize)]
1228
#[serde(rename_all = "camelCase")]
1329
pub struct TaskDefinition {

cli/lsp/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ pub async fn start() -> Result<(), AnyError> {
4949
lsp_custom::PERFORMANCE_REQUEST,
5050
LanguageServer::performance_request,
5151
)
52+
.custom_method(
53+
lsp_custom::INFERRED_TYPE_REQUEST,
54+
LanguageServer::inferred_type,
55+
)
5256
.custom_method(lsp_custom::TASK_REQUEST, LanguageServer::task_definitions)
5357
.custom_method(testing::TEST_RUN_REQUEST, LanguageServer::test_run_request)
5458
.custom_method(

cli/lsp/refactor.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,20 @@ pub static REWRITE_PROPERTY_GENERATEACCESSORS: Lazy<RefactorCodeActionKind> =
130130
}),
131131
});
132132

133+
pub static INFER_FUNCTION_RETURN_TYPE: Lazy<RefactorCodeActionKind> =
134+
Lazy::new(|| RefactorCodeActionKind {
135+
kind: [
136+
lsp::CodeActionKind::REFACTOR_REWRITE.as_str(),
137+
"function",
138+
"returnType",
139+
]
140+
.join(".")
141+
.into(),
142+
matches_callback: Box::new(|tag: &str| {
143+
tag.starts_with("Infer function return type")
144+
}),
145+
});
146+
133147
pub static ALL_KNOWN_REFACTOR_ACTION_KINDS: Lazy<
134148
Vec<&'static RefactorCodeActionKind>,
135149
> = Lazy::new(|| {
@@ -144,6 +158,7 @@ pub static ALL_KNOWN_REFACTOR_ACTION_KINDS: Lazy<
144158
&REWRITE_ARROW_BRACES,
145159
&REWRITE_PARAMETERS_TO_DESTRUCTURED,
146160
&REWRITE_PROPERTY_GENERATEACCESSORS,
161+
&INFER_FUNCTION_RETURN_TYPE,
147162
]
148163
});
149164

cli/lsp/ts_server.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use crate::lsp::diagnostics::ts_json_to_diagnostics;
2626
use crate::lsp::documents::Document;
2727
use crate::lsp::language_server;
2828
use crate::lsp::logging::lsp_warn;
29+
use crate::lsp::lsp_custom;
2930
use crate::lsp::performance::Performance;
3031
use crate::lsp::refactor;
3132
use crate::lsp::tsc::TsJsServer;
@@ -295,6 +296,31 @@ impl TsServer {
295296
}
296297
}
297298

299+
pub async fn provide_inferred_type(
300+
&self,
301+
module: &DocumentModule,
302+
position: lsp::Position,
303+
snapshot: Arc<StateSnapshot>,
304+
token: &CancellationToken,
305+
) -> Result<Option<lsp_custom::InferredTypeResponse>, AnyError> {
306+
match self {
307+
Self::Js(ts_server) => {
308+
let position = module.line_index.offset_tsc(position)?;
309+
let quick_info = ts_server
310+
.get_quick_info(snapshot.clone(), module, position, token)
311+
.await?;
312+
Ok(quick_info.and_then(|quick_info| {
313+
quick_info.display_string(module, &snapshot).map(|text| {
314+
lsp_custom::InferredTypeResponse {
315+
text,
316+
range: quick_info.to_range(module),
317+
}
318+
})
319+
}))
320+
}
321+
}
322+
}
323+
298324
#[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
299325
pub async fn provide_code_actions(
300326
&self,

cli/lsp/tsc.rs

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ use crate::util::path::relative_specifier;
106106
use crate::util::path::to_percent_decoded_str;
107107
use crate::util::v8::convert;
108108

109+
const QUICK_INFO_MAX_LENGTH: u32 = 1_000_000;
110+
109111
static BRACKET_ACCESSOR_RE: Lazy<Regex> =
110112
lazy_regex!(r#"^\[['"](.+)[\['"]\]$"#);
111113
static CODEBLOCK_RE: Lazy<Regex> = lazy_regex!(r"^\s*[~`]{3}"m);
@@ -723,6 +725,7 @@ impl TsJsServer {
723725
.specifier_map
724726
.denormalize(&module.specifier, module.media_type),
725727
position,
728+
QUICK_INFO_MAX_LENGTH,
726729
));
727730
self
728731
.request(
@@ -2091,18 +2094,29 @@ fn display_parts_to_string<'a>(
20912094
}
20922095

20932096
impl QuickInfo {
2097+
pub fn display_string(
2098+
&self,
2099+
module: &DocumentModule,
2100+
snapshot: &StateSnapshot,
2101+
) -> Option<String> {
2102+
self
2103+
.display_parts
2104+
.as_ref()
2105+
.map(|p| display_parts_to_string(p, module, snapshot))
2106+
.filter(|s| !s.is_empty())
2107+
}
2108+
2109+
pub fn to_range(&self, module: &DocumentModule) -> lsp::Range {
2110+
self.text_span.to_range(&module.line_index)
2111+
}
2112+
20942113
pub fn to_hover(
20952114
&self,
20962115
module: &DocumentModule,
20972116
snapshot: &StateSnapshot,
20982117
) -> lsp::Hover {
20992118
let mut value = String::new();
2100-
if let Some(display_string) = self
2101-
.display_parts
2102-
.clone()
2103-
.map(|p| display_parts_to_string(&p, module, snapshot))
2104-
&& !display_string.is_empty()
2105-
{
2119+
if let Some(display_string) = self.display_string(module, snapshot) {
21062120
value.push_str("```tsx\n");
21072121
value.push_str(&display_string);
21082122
value.push_str("\n```\n");
@@ -6418,7 +6432,7 @@ enum TscRequest {
64186432
GetNavigationTree((String,)),
64196433
GetSupportedCodeFixes,
64206434
// https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6214
6421-
GetQuickInfoAtPosition((String, u32)),
6435+
GetQuickInfoAtPosition((String, u32, u32)),
64226436
// https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6257
64236437
GetCodeFixesAtPosition(
64246438
Box<(
@@ -6552,9 +6566,15 @@ impl TscRequest {
65526566
("getNavigationTree", Some(args.to_v8(scope)?))
65536567
}
65546568
TscRequest::GetSupportedCodeFixes => ("$getSupportedCodeFixes", None),
6555-
TscRequest::GetQuickInfoAtPosition((specifier, position)) => (
6569+
TscRequest::GetQuickInfoAtPosition((
6570+
specifier,
6571+
position,
6572+
maximum_length,
6573+
)) => (
65566574
"getQuickInfoAtPosition",
6557-
Some((specifier, Number(position)).to_v8(scope)?),
6575+
Some(
6576+
(specifier, Number(position), Number(maximum_length)).to_v8(scope)?,
6577+
),
65586578
),
65596579
TscRequest::GetCodeFixesAtPosition(args) => (
65606580
"getCodeFixesAtPosition",

0 commit comments

Comments
 (0)