0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 09:31:22 -05:00

feat(lsp): Implement textDocument/signatureHelp (#9330)

Co-authored-by: Kitson Kelly <me@kitsonkelly.com>
This commit is contained in:
Yuki Tanaka 2021-02-16 11:34:09 +09:00 committed by GitHub
parent 3f5265b21e
commit ccd6ee5c23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 382 additions and 13 deletions

View file

@ -16,6 +16,7 @@ use lspower::lsp::ImplementationProviderCapability;
use lspower::lsp::OneOf;
use lspower::lsp::SaveOptions;
use lspower::lsp::ServerCapabilities;
use lspower::lsp::SignatureHelpOptions;
use lspower::lsp::TextDocumentSyncCapability;
use lspower::lsp::TextDocumentSyncKind;
use lspower::lsp::TextDocumentSyncOptions;
@ -69,7 +70,17 @@ pub fn server_capabilities(
work_done_progress: None,
},
}),
signature_help_provider: None,
signature_help_provider: Some(SignatureHelpOptions {
trigger_characters: Some(vec![
",".to_string(),
"(".to_string(),
"<".to_string(),
]),
retrigger_characters: None,
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
}),
declaration_provider: None,
definition_provider: Some(OneOf::Left(true)),
type_definition_provider: None,

View file

@ -1705,6 +1705,68 @@ impl Inner {
}
}
}
async fn signature_help(
&self,
params: SignatureHelpParams,
) -> LspResult<Option<SignatureHelp>> {
if !self.enabled() {
return Ok(None);
}
let mark = self.performance.mark("signature_help");
let specifier = utils::normalize_url(
params.text_document_position_params.text_document.uri,
);
let line_index =
if let Some(line_index) = self.get_line_index_sync(&specifier) {
line_index
} else {
return Err(LspError::invalid_params(format!(
"An unexpected specifier ({}) was provided.",
specifier
)));
};
let options = if let Some(context) = params.context {
tsc::SignatureHelpItemsOptions {
trigger_reason: Some(tsc::SignatureHelpTriggerReason {
kind: context.trigger_kind.into(),
trigger_character: context.trigger_character,
}),
}
} else {
tsc::SignatureHelpItemsOptions {
trigger_reason: None,
}
};
let req = tsc::RequestMethod::GetSignatureHelpItems((
specifier,
line_index.offset_tsc(params.text_document_position_params.position)?,
options,
));
let res =
self
.ts_server
.request(self.snapshot(), req)
.await
.map_err(|err| {
error!("Failed to request to tsserver: {}", err);
LspError::invalid_request()
})?;
let maybe_signature_help_items: Option<tsc::SignatureHelpItems> =
serde_json::from_value(res).map_err(|err| {
error!("Failed to deserialize tsserver response: {}", err);
LspError::internal_error()
})?;
if let Some(signature_help_items) = maybe_signature_help_items {
let signature_help = signature_help_items.into_signature_help();
self.performance.measure(mark);
Ok(Some(signature_help))
} else {
self.performance.measure(mark);
Ok(None)
}
}
}
#[lspower::async_trait]
@ -1840,6 +1902,13 @@ impl lspower::LanguageServer for LanguageServer {
) -> LspResult<Option<Value>> {
self.0.lock().await.request_else(method, params).await
}
async fn signature_help(
&self,
params: SignatureHelpParams,
) -> LspResult<Option<SignatureHelp>> {
self.0.lock().await.signature_help(params).await
}
}
#[derive(Debug, Deserialize, Serialize)]
@ -2538,6 +2607,80 @@ mod tests {
harness.run().await;
}
#[tokio::test]
async fn test_signature_help() {
let mut harness = LspTestHarness::new(vec![
("initialize_request.json", LspResponse::RequestAny),
("initialized_notification.json", LspResponse::None),
(
"signature_help_did_open_notification.json",
LspResponse::None,
),
(
"signature_help_request_01.json",
LspResponse::Request(
1,
json!({
"signatures": [
{
"label": "add(a: number, b: number): number",
"documentation": "Adds two numbers.",
"parameters": [
{
"label": "a: number",
"documentation": "This is a first number."
},
{
"label": "b: number",
"documentation": "This is a second number."
}
]
}
],
"activeSignature": 0,
"activeParameter": 0
}),
),
),
(
"signature_help_did_change_notification.json",
LspResponse::None,
),
(
"signature_help_request_02.json",
LspResponse::Request(
2,
json!({
"signatures": [
{
"label": "add(a: number, b: number): number",
"documentation": "Adds two numbers.",
"parameters": [
{
"label": "a: number",
"documentation": "This is a first number."
},
{
"label": "b: number",
"documentation": "This is a second number."
}
]
}
],
"activeSignature": 0,
"activeParameter": 1
}),
),
),
(
"shutdown_request.json",
LspResponse::Request(3, json!(null)),
),
("exit_notification.json", LspResponse::None),
]);
harness.run().await;
}
#[tokio::test]
async fn test_code_lens_impl_request() {
let mut harness = LspTestHarness::new(vec![

View file

@ -166,16 +166,12 @@ pub async fn get_asset(
}
}
fn display_parts_to_string(
maybe_parts: Option<Vec<SymbolDisplayPart>>,
) -> Option<String> {
maybe_parts.map(|parts| {
parts
.into_iter()
.map(|p| p.text)
.collect::<Vec<String>>()
.join("")
})
fn display_parts_to_string(parts: Vec<SymbolDisplayPart>) -> String {
parts
.into_iter()
.map(|p| p.text)
.collect::<Vec<String>>()
.join("")
}
fn get_tag_body_text(tag: &JSDocTagInfo) -> Option<String> {
@ -433,7 +429,7 @@ impl QuickInfo {
pub fn to_hover(&self, line_index: &LineIndex) -> lsp::Hover {
let mut contents = Vec::<lsp::MarkedString>::new();
if let Some(display_string) =
display_parts_to_string(self.display_parts.clone())
self.display_parts.clone().map(display_parts_to_string)
{
contents.push(lsp::MarkedString::from_language_code(
"typescript".to_string(),
@ -441,7 +437,7 @@ impl QuickInfo {
));
}
if let Some(documentation) =
display_parts_to_string(self.documentation.clone())
self.documentation.clone().map(display_parts_to_string)
{
contents.push(lsp::MarkedString::from_markdown(documentation));
}
@ -946,6 +942,91 @@ impl CompletionEntry {
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignatureHelpItems {
items: Vec<SignatureHelpItem>,
applicable_span: TextSpan,
selected_item_index: u32,
argument_index: u32,
argument_count: u32,
}
impl SignatureHelpItems {
pub fn into_signature_help(self) -> lsp::SignatureHelp {
lsp::SignatureHelp {
signatures: self
.items
.into_iter()
.map(|item| item.into_signature_information())
.collect(),
active_parameter: Some(self.argument_index),
active_signature: Some(self.selected_item_index),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignatureHelpItem {
is_variadic: bool,
prefix_display_parts: Vec<SymbolDisplayPart>,
suffix_display_parts: Vec<SymbolDisplayPart>,
separator_display_parts: Vec<SymbolDisplayPart>,
parameters: Vec<SignatureHelpParameter>,
documentation: Vec<SymbolDisplayPart>,
tags: Vec<JSDocTagInfo>,
}
impl SignatureHelpItem {
pub fn into_signature_information(self) -> lsp::SignatureInformation {
let prefix_text = display_parts_to_string(self.prefix_display_parts);
let params_text = self
.parameters
.iter()
.map(|param| display_parts_to_string(param.display_parts.clone()))
.collect::<Vec<String>>()
.join(", ");
let suffix_text = display_parts_to_string(self.suffix_display_parts);
lsp::SignatureInformation {
label: format!("{}{}{}", prefix_text, params_text, suffix_text),
documentation: Some(lsp::Documentation::String(display_parts_to_string(
self.documentation,
))),
parameters: Some(
self
.parameters
.into_iter()
.map(|param| param.into_parameter_information())
.collect(),
),
active_parameter: None,
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignatureHelpParameter {
name: String,
documentation: Vec<SymbolDisplayPart>,
display_parts: Vec<SymbolDisplayPart>,
is_optional: bool,
}
impl SignatureHelpParameter {
pub fn into_parameter_information(self) -> lsp::ParameterInformation {
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple(display_parts_to_string(
self.display_parts,
)),
documentation: Some(lsp::Documentation::String(display_parts_to_string(
self.documentation,
))),
}
}
}
#[derive(Debug, Clone, Deserialize)]
struct Response {
id: usize,
@ -1361,6 +1442,41 @@ pub struct UserPreferences {
pub provide_refactor_not_applicable_reason: Option<bool>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SignatureHelpItemsOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_reason: Option<SignatureHelpTriggerReason>,
}
#[derive(Debug, Serialize)]
pub enum SignatureHelpTriggerKind {
#[serde(rename = "characterTyped")]
CharacterTyped,
#[serde(rename = "invoked")]
Invoked,
#[serde(rename = "retrigger")]
Retrigger,
}
impl From<lsp::SignatureHelpTriggerKind> for SignatureHelpTriggerKind {
fn from(kind: lsp::SignatureHelpTriggerKind) -> Self {
match kind {
lsp::SignatureHelpTriggerKind::Invoked => Self::Invoked,
lsp::SignatureHelpTriggerKind::TriggerCharacter => Self::CharacterTyped,
lsp::SignatureHelpTriggerKind::ContentChange => Self::Retrigger,
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SignatureHelpTriggerReason {
pub kind: SignatureHelpTriggerKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_character: Option<String>,
}
/// Methods that are supported by the Language Service in the compiler isolate.
#[derive(Debug)]
pub enum RequestMethod {
@ -1390,6 +1506,8 @@ pub enum RequestMethod {
GetQuickInfo((ModuleSpecifier, u32)),
/// Get document references for a specific position.
GetReferences((ModuleSpecifier, u32)),
/// Get signature help items for a specific position.
GetSignatureHelpItems((ModuleSpecifier, u32, SignatureHelpItemsOptions)),
/// Get the diagnostic codes that support some form of code fix.
GetSupportedCodeFixes,
}
@ -1497,6 +1615,15 @@ impl RequestMethod {
"specifier": specifier,
"position": position,
}),
RequestMethod::GetSignatureHelpItems((specifier, position, options)) => {
json!({
"id": id,
"method": "getSignatureHelpItems",
"specifier": specifier,
"position": position,
"options": options,
})
}
RequestMethod::GetSupportedCodeFixes => json!({
"id": id,
"method": "getSupportedCodeFixes",

View file

@ -0,0 +1,25 @@
{
"jsonrpc": "2.0",
"method": "textDocument/didChange",
"params": {
"textDocument": {
"uri": "file:///a/file.ts",
"version": 2
},
"contentChanges": [
{
"range": {
"start": {
"line": 9,
"character": 4
},
"end": {
"line": 9,
"character": 4
}
},
"text": "123, "
}
]
}
}

View file

@ -0,0 +1,12 @@
{
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "/**\n * Adds two numbers.\n * @param a This is a first number.\n * @param b This is a second number.\n */\nfunction add(a: number, b: number) {\n return a + b;\n}\n\nadd("
}
}
}

View file

@ -0,0 +1,19 @@
{
"jsonrpc": "2.0",
"id": 1,
"method": "textDocument/signatureHelp",
"params": {
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": {
"character": 4,
"line": 9
},
"context": {
"triggerKind": 2,
"triggerCharacter": "(",
"isRetrigger": false
}
}
}

View file

@ -0,0 +1,14 @@
{
"id": 2,
"jsonrpc": "2.0",
"method": "textDocument/signatureHelp",
"params": {
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": {
"character": 8,
"line": 9
}
}
}

View file

@ -677,6 +677,16 @@ delete Object.prototype.__proto__;
),
);
}
case "getSignatureHelpItems": {
return respond(
id,
languageService.getSignatureHelpItems(
request.specifier,
request.position,
request.options,
),
);
}
case "getSupportedCodeFixes": {
return respond(
id,

View file

@ -54,6 +54,7 @@ declare global {
| GetNavigationTree
| GetQuickInfoRequest
| GetReferencesRequest
| GetSignatureHelpItemsRequest
| GetSupportedCodeFixes;
interface BaseLanguageServerRequest {
@ -144,6 +145,13 @@ declare global {
position: number;
}
interface GetSignatureHelpItemsRequest extends BaseLanguageServerRequest {
method: "getSignatureHelpItems";
specifier: string;
position: number;
options: ts.SignatureHelpItemsOptions;
}
interface GetSupportedCodeFixes extends BaseLanguageServerRequest {
method: "getSupportedCodeFixes";
}