diff --git a/newtests/lsp/completion/params.js b/newtests/lsp/completion/params.js new file mode 100644 index 00000000000..778ebc00dc3 --- /dev/null +++ b/newtests/lsp/completion/params.js @@ -0,0 +1,11 @@ +// @flow + +// Tests different auto complete items use the correct `kind`. +// In VSCode, for example, this includes the autocomplete +// icons used. + +let aFunction = (arg1: number, arg2: string) => null; + +function foo() { + const x = 15; +} diff --git a/newtests/lsp/completion/test.js b/newtests/lsp/completion/test.js index d335ad7ae5b..50d108c7cde 100644 --- a/newtests/lsp/completion/test.js +++ b/newtests/lsp/completion/test.js @@ -176,5 +176,70 @@ export default suite( [...lspIgnoreStatusAndCancellation], ), ]), - ], + test('textDocument/completion', [ + addFile('params.js'), + ideStartAndConnect(), + ideRequestAndWaitUntilResponse('textDocument/completion', { + textDocument: {uri: 'params.js'}, + position: {line: 9, character: 15}, + context: { triggerKind: 1 } + }).verifyAllIDEMessagesInStep( + [ + (() => { + const expectedResponse = { + isIncomplete: false, + items: [ + { + label: "x", + kind: 6, + detail: "number", + inlineDetail: "number", + insertTextFormat: 1 + }, + { + label: "foo", + kind: 3, + detail: "() => void", + inlineDetail: "()", + itemType: "void", + insertTextFormat: 1 + }, + { + label: "exports", + kind: 6, + detail: "{||}", + inlineDetail: "{||}", + insertTextFormat: 1 + }, + { + label: "aFunction", + kind: 3, + detail: "(arg1: number, arg2: string) => null", + inlineDetail: "(arg1: number, arg2: string)", + itemType: "null", + insertTextFormat: 1 + }, + { + label: "this", + kind: 6, + detail: "empty", + inlineDetail: "empty", + insertTextFormat: 1 + }, + { + label: "super", + kind: 6, + detail: "typeof Object.prototype", + inlineDetail: "typeof Object.prototype", + insertTextFormat: 1 + } + ] + }; + return `textDocument/completion${JSON.stringify(expectedResponse)}` + })() + ], + [...lspIgnoreStatusAndCancellation], + ), + ]), + ] ); diff --git a/src/common/flow_lsp_conversions.ml b/src/common/flow_lsp_conversions.ml index af4a8a5ca0b..976f54c0d2e 100644 --- a/src/common/flow_lsp_conversions.ml +++ b/src/common/flow_lsp_conversions.ml @@ -7,7 +7,24 @@ module Ast = Flow_ast +let flow_position_to_lsp (line: int) (char: int): Lsp.position = + let open Lsp in + { + line = line - 1; + character = char; + } + +let lsp_position_to_flow (position: Lsp.position): int * int = + let open Lsp in + let line = position.line + 1 in + let char = position.character + in + (line, char) + let flow_completion_to_lsp + (line: int) + (character: int) + (is_snippet_supported: bool) (item: ServerProt.Response.complete_autocomplete_result) : Lsp.Completion.completionItem = let open Lsp.Completion in @@ -18,17 +35,38 @@ let flow_completion_to_lsp let params = Core_list.map ~f:(fun p -> p.param_name ^ ": " ^ p.param_ty) params in "(" ^ (String.concat ", " params) ^ ")" in - let itemType, inlineDetail, detail = match item.func_details with + let flow_params_to_lsp_snippet name params = + let params = Core_list.mapi ~f:(fun i p -> "${" ^ string_of_int (i +1 ) ^ ":" ^ p.param_name ^ "}") params in + name ^ "(" ^ (String.concat ", " params) ^ ")" + in + let func_snippet item func_details = + let newText = flow_params_to_lsp_snippet item.res_name func_details.param_tys in + let open Lsp in + let start = (flow_position_to_lsp line (character - 1)) in + let end_ = (flow_position_to_lsp line (character + 1)) in + let textEdit: TextEdit.t = { + Lsp.TextEdit.range = { + start; + end_; + }; + Lsp.TextEdit.newText = newText; + } in + [textEdit] + in + let itemType, inlineDetail, detail, insertTextFormat, textEdits = match item.func_details with | Some func_details -> let itemType = Some (trunc 30 func_details.return_ty) in let inlineDetail = Some (trunc 40 (flow_params_to_string func_details.param_tys)) in let detail = Some (trunc80 item.res_ty) in - itemType, inlineDetail, detail + let (insertTextFormat, textEdits) = match is_snippet_supported with + | true -> (Some SnippetFormat, (func_snippet item func_details)) + | false -> (Some PlainText, []) in + itemType, inlineDetail, detail, insertTextFormat, textEdits | None -> let itemType = None in let inlineDetail = Some (trunc80 item.res_ty) in let detail = Some (trunc80 item.res_ty) in - itemType, inlineDetail, detail + itemType, inlineDetail, detail, Some PlainText, [] in { label = item.res_name; @@ -39,9 +77,10 @@ let flow_completion_to_lsp documentation = None; (* This will be filled in by completionItem/resolve. *) sortText = None; filterText = None; + (* deprecated and should not be used *) insertText = None; - insertTextFormat = Some PlainText; - textEdits = []; + insertTextFormat; + textEdits; command = None; data = None; } @@ -70,13 +109,6 @@ let loc_to_lsp_with_default (loc: Loc.t) ~(default_uri: string): Lsp.Location.t in { Lsp.Location.uri; range = loc_to_lsp_range loc; } -let lsp_position_to_flow (position: Lsp.position): int * int = - let open Lsp in - let line = position.line + 1 in - let char = position.character - in - (line, char) - let flow_edit_to_textedit (edit: Loc.t * string): Lsp.TextEdit.t = let loc, text = edit in { Lsp.TextEdit.range = loc_to_lsp_range loc; newText = text } diff --git a/src/server/command_handler/commandHandler.ml b/src/server/command_handler/commandHandler.ml index 10ba0c539c4..d9d4c9e8069 100644 --- a/src/server/command_handler/commandHandler.ml +++ b/src/server/command_handler/commandHandler.ml @@ -899,6 +899,7 @@ let handle_persistent_infer_type ~options ~id ~params ~loc ~metadata ~client ~pr end let handle_persistent_autocomplete_lsp ~options ~id ~params ~loc ~metadata ~client ~profiling ~env = + let is_snippet_supported = Persistent_connection.client_snippet_support client in let open Completion in let (file, line, char) = match loc with | Some loc -> loc @@ -929,7 +930,7 @@ let handle_persistent_autocomplete_lsp ~options ~id ~params ~loc ~metadata ~clie let metadata = with_data ~extra_data metadata in begin match result with | Ok items -> - let items = Core_list.map ~f:Flow_lsp_conversions.flow_completion_to_lsp items in + let items = Core_list.map ~f: (Flow_lsp_conversions.flow_completion_to_lsp line char is_snippet_supported) items in let r = CompletionResult { Lsp.Completion.isIncomplete = false; items; } in let response = ResponseMessage (id, r) in Lwt.return (LspResponse (Ok ((), Some response, metadata))) diff --git a/src/server/persistent_connection/persistent_connection.ml b/src/server/persistent_connection/persistent_connection.ml index 945bf7a70b9..63d056fffa1 100644 --- a/src/server/persistent_connection/persistent_connection.ml +++ b/src/server/persistent_connection/persistent_connection.ml @@ -232,3 +232,9 @@ let get_opened_files (clients: t) : SSet.t = List.fold_left per_client SSet.empty clients let get_id client = client.client_id + +let client_snippet_support (client: single_client) = + let open Lsp.Initialize in + match client.lsp_initialize_params with + | None -> false + | Some params -> params.client_capabilities.textDocument.completion.completionItem.snippetSupport diff --git a/src/server/persistent_connection/persistent_connection.mli b/src/server/persistent_connection/persistent_connection.mli index c67a0539798..d1383fe014c 100644 --- a/src/server/persistent_connection/persistent_connection.mli +++ b/src/server/persistent_connection/persistent_connection.mli @@ -77,3 +77,6 @@ val get_file: single_client -> string -> File_input.t val get_client: Prot.client_id -> single_client option val get_id: single_client -> Prot.client_id + +val client_snippet_support: single_client -> bool +