From df2e31dc8bfc3db06f79458edf0e0617c88cebfe Mon Sep 17 00:00:00 2001 From: Max Heiber Date: Wed, 17 May 2023 15:15:54 -0700 Subject: [PATCH] introduce lazy code action resolution (codeAction/resolve) Summary: This diff adds infra for lazily-resolved code actions, and uses the infra for the "flip around comma" code action. This change is a no-op from the user's point of view. The point of these changes are to make a performant "extract into method" refactor feasible. Since "extract into method" uses libhackfmt under the hood when generating edits, it is best to generate these edits lazily. I implement lazy code actions for "flip around comma" first (in this diff) because it's easier to demonstrate lazy code action resolution with a pre-existing refactor and also the integration test is easier to read for a more straightforward refactor. A subsequent diff in this stack also uses the infra here for the "extract into method" refactor. ## LSP background A "lazily-resolved code action" is a code action with this flow (LSP protocol): - client sends [`textDocument/codeAction`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_codeAction) - server replies with a partially-resolved code action with no `edit` nor `command` field and a `data` field with the shape of its choosing - client sends a [`codeAction/resolve`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeAction_resolve) request with the partially-resolved code action including the `data` field ## Goals of the design I designed lazy code action resolution to be: 1. Easy to use: for a particular refactor, all one has to to be lazy is replace `EditOnly (compute_edit args))` with `UnresolvedEdit (lazy (compute_edit args))`. No need to have a custom protocol for each refactoring. See `CodeActionsServiceFlipAroundComma.ml`. 2. Correct: we ensure the logic for finding the title and location of the code action is identical when handling the `textDocument/codeAction` request compared to the `codeAction/Resolve` request. 3. Type-safe: we distinguish at the type level between resolved and unresolved code actions. In addition, code actions have a well-typed `data` field. These are in contrast to the types in the LSP spec, which allow more kinds of invalid states to be represented. ## Design The LSP protocol is such that the `codeAction/resolve` handler has access to the original code action returned from `textDocument/resolve`, including a custom `data` field. We set `data` to be the original parameters to the `codeAction/resolve` request, which ensures we have enough info to resolve the rest of the code action regardless of the specific refactoring we are implementing (see goals 1-3). A specific refactoring implements a single `find` function that is used regardless of whether the request is `textDocument/codeAction` or `codeAction/resolve` (see goals 1 and 2). CodeActionsService then does slightly different things depending on the request. The following is type-safe due to a sparing use of a phantom type param (see goal 3 and lsp.mli): - For `textDocument/codeAction`, CodeActionsService has to transform the code action to remove functional values so it's easy to marshal the code action across our internal-to-the-language-server IPC stuff. This is just the trivial transformation from `UnresolvedEdit lazy_edit` to `UnresolvedEdit ()`. - For `codeAction/resolve`, CodeActionsService finds the (possibly-unresolved) code action matching the code action to resolve based on the request params and title. CodeActionService then converts `UnresolvedEdit lazy_edit` to `EditOnly edit` by forcing the edit. For a `CodeAction.resolved_command_or_action`, `UnresolvedEdit` is uninhabitable, so we guarantee that `codeAction/resolve` returns a resolved code action. Reviewed By: ljw1004 Differential Revision: D45828606 fbshipit-source-id: 8bc1f253141fa00d9bcced5b65f0f7f30b0e7fa1 --- hphp/hack/src/client/clientLsp.ml | 98 ++++++++++++++--- .../src/client/ide_service/clientIdeDaemon.ml | 12 +- .../client/ide_service/clientIdeMessage.ml | 11 ++ hphp/hack/src/hh_single_type_check.ml | 25 ++++- hphp/hack/src/server/codeActionsService.ml | 67 ++++++++++- hphp/hack/src/server/codeActionsService.mli | 8 ++ .../codeActionsServiceExtractVariable.mli | 2 +- .../codeActionsServiceFlipAroundComma.ml | 3 +- .../codeActionsServiceFlipAroundComma.mli | 2 +- .../codeActionsServiceInlineVariable.mli | 13 +++ hphp/hack/src/server/serverCommand.ml | 3 +- hphp/hack/src/server/serverCommandTypes.ml | 12 +- .../src/server/serverCommandTypesUtils.ml | 3 +- hphp/hack/src/server/serverRpc.ml | 8 +- hphp/hack/src/utils/lsp/lsp.ml | 66 ++++++++--- hphp/hack/src/utils/lsp/lsp.mli | 104 ++++++++++++++---- hphp/hack/src/utils/lsp/lsp_fmt.ml | 95 +++++++++++++--- hphp/hack/src/utils/lsp/lsp_fmt.mli | 5 +- hphp/hack/src/utils/lsp/lsp_helpers.ml | 6 + hphp/hack/src/utils/lsp/lsp_helpers.mli | 2 + .../code_action_flip_around_comma.php | 5 + .../initialize_shutdown.expected | 4 +- .../data/lsp_exchanges/nomethod.expected | 4 +- .../data/lsp_exchanges/references.expected | 4 +- .../data/lsp_exchanges/rename.expected | 4 +- hphp/hack/test/integration/test_lsp.py | 98 ++++++++++++++++- 26 files changed, 569 insertions(+), 95 deletions(-) create mode 100644 hphp/hack/src/server/codeActionsServiceInlineVariable.mli create mode 100644 hphp/hack/test/integration/data/lsp_exchanges/code_action_flip_around_comma.php diff --git a/hphp/hack/src/client/clientLsp.ml b/hphp/hack/src/client/clientLsp.ml index 50c2c4207cf0b0..6cd6a2325476b7 100644 --- a/hphp/hack/src/client/clientLsp.ml +++ b/hphp/hack/src/client/clientLsp.ml @@ -3719,17 +3719,41 @@ let do_codeAction_local in Lwt.return (actions, file_path, errors_opt) -let do_codeAction +let do_codeAction_resolve_local + (ide_service : ClientIdeService.t ref) + (env : env) + (tracking_id : string) + (ref_unblocked_time : float ref) + (editor_open_files : Lsp.TextDocumentItem.t UriMap.t) + (params : CodeActionRequest.params) + ~(resolve_title : string) : CodeActionResolve.result Lwt.t = + let text_document = + get_text_document_item + editor_open_files + params.CodeActionRequest.textDocument.TextDocumentIdentifier.uri + in + let document = lsp_document_to_ide text_document in + let range = lsp_range_to_ide params.CodeActionRequest.range in + ide_rpc + ide_service + ~env + ~tracking_id + ~ref_unblocked_time + (ClientIdeMessage.Code_action_resolve { document; range; resolve_title }) + +let do_codeAction_resolve (conn : server_conn) (ref_unblocked_time : float ref) - (params : CodeActionRequest.params) : - CodeAction.command_or_action list Lwt.t = - let filename = + (params : CodeActionRequest.params) + ~(resolve_title : string) : CodeActionResolve.result Lwt.t = + let path = lsp_uri_to_path params.CodeActionRequest.textDocument.TextDocumentIdentifier.uri in let range = lsp_range_to_ide params.CodeActionRequest.range in - let command = ServerCommandTypes.CODE_ACTIONS (filename, range) in + let command = + ServerCommandTypes.CODE_ACTION_RESOLVE { path; range; resolve_title } + in rpc conn ref_unblocked_time ~desc:"code_actions" command let do_signatureHelp @@ -4795,11 +4819,12 @@ let do_initialize (local_config : ServerLocalConfig.t) : Initialize.result = hoverProvider = true; completionProvider = Some - { - resolveProvider = true; - completion_triggerCharacters = - ["$"; ">"; "\\"; ":"; "<"; "["; "'"; "\""; "{"; "#"]; - }; + CompletionOptions. + { + resolveProvider = true; + completion_triggerCharacters = + ["$"; ">"; "\\"; ":"; "<"; "["; "'"; "\""; "{"; "#"]; + }; signatureHelpProvider = Some { sighelp_triggerCharacters = ["("; ","] }; definitionProvider = true; @@ -4809,7 +4834,7 @@ let do_initialize (local_config : ServerLocalConfig.t) : Initialize.result = documentHighlightProvider = true; documentSymbolProvider = true; workspaceSymbolProvider = true; - codeActionProvider = true; + codeActionProvider = Some CodeActionOptions.{ resolveProvider = true }; codeLensProvider = None; documentFormattingProvider = true; documentRangeFormattingProvider = true; @@ -5737,7 +5762,10 @@ let handle_client_message editor_open_files params in - respond_jsonrpc ~powered_by:Serverless_ide id (CodeActionResult result); + respond_jsonrpc + ~powered_by:Serverless_ide + id + (CodeActionResult (result, params)); begin match errors_opt with | None -> () @@ -5746,13 +5774,47 @@ let handle_client_message end; Lwt.return_some { result_count = List.length result; result_extra_telemetry = None } - (* textDocument/codeAction request, when not in serverless IDE mode *) - | (Main_loop menv, None, RequestMessage (id, CodeActionRequest params)) -> + (* codeAction/resolve request *) + | (_, Some ide_service, RequestMessage (id, CodeActionResolveRequest params)) + -> + let CodeActionResolveRequest.{ data = code_action_request_params; title } + = + params + in let%lwt () = cancel_if_stale client timestamp short_timeout in - let%lwt result = do_codeAction menv.conn ref_unblocked_time params in - respond_jsonrpc ~powered_by:Hh_server id (CodeActionResult result); - Lwt.return_some - { result_count = List.length result; result_extra_telemetry = None } + let%lwt result = + do_codeAction_resolve_local + ide_service + env + tracking_id + ref_unblocked_time + editor_open_files + code_action_request_params + ~resolve_title:title + in + respond_jsonrpc + ~powered_by:Serverless_ide + id + (CodeActionResolveResult result); + Lwt.return_some { result_count = 1; result_extra_telemetry = None } + (* codeAction/resolve request, when not in serverless IDE mode *) + | ( Main_loop menv, + None, + RequestMessage (id, CodeActionResolveRequest params) ) -> + let%lwt () = cancel_if_stale client timestamp short_timeout in + let CodeActionResolveRequest.{ data = code_action_request_params; title } + = + params + in + let%lwt result = + do_codeAction_resolve + menv.conn + ref_unblocked_time + code_action_request_params + ~resolve_title:title + in + respond_jsonrpc ~powered_by:Hh_server id (CodeActionResolveResult result); + Lwt.return_some { result_count = 1; result_extra_telemetry = None } (* textDocument/formatting *) | (_, _, RequestMessage (id, DocumentFormattingRequest params)) -> let result = do_documentFormatting editor_open_files params in diff --git a/hphp/hack/src/client/ide_service/clientIdeDaemon.ml b/hphp/hack/src/client/ide_service/clientIdeDaemon.ml index 3c8226f0a38742..11b8e52984b8a5 100644 --- a/hphp/hack/src/client/ide_service/clientIdeDaemon.ml +++ b/hphp/hack/src/client/ide_service/clientIdeDaemon.ml @@ -1450,7 +1450,6 @@ let handle_request in let path = Path.to_string document.file_path in - (* TODO: should be using RelativePath.t, not string *) let results = Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () -> CodeActionsService.go ~ctx ~entry ~path ~range) @@ -1488,6 +1487,17 @@ let handle_request Some (Errors.sort_and_finalize errors) in Lwt.return (Initialized istate, Ok (results, errors_opt)) + (* Code action resolve (refactorings, quickfixes) *) + | (Initialized istate, Code_action_resolve { document; range; resolve_title }) + -> + let (istate, ctx, entry, _) = update_file_ctx istate document in + + let path = Path.to_string document.file_path in + let result = + Provider_utils.respect_but_quarantine_unsaved_changes ~ctx ~f:(fun () -> + CodeActionsService.resolve ~ctx ~entry ~path ~range ~resolve_title) + in + Lwt.return (Initialized istate, Ok result) (* Go to definition *) | (Initialized istate, Definition (document, { line; column })) -> let (istate, ctx, entry, _) = update_file_ctx istate document in diff --git a/hphp/hack/src/client/ide_service/clientIdeMessage.ml b/hphp/hack/src/client/ide_service/clientIdeMessage.ml index 603d502fe6b796..f92c1553c03404 100644 --- a/hphp/hack/src/client/ide_service/clientIdeMessage.ml +++ b/hphp/hack/src/client/ide_service/clientIdeMessage.ml @@ -162,6 +162,12 @@ type _ t = because this is called so frequently by VSCode (when you switch tab, and every time the caret moves) in the hope that this will be a good balance of simple code and decent experience. *) + | Code_action_resolve : { + document: document; + range: Ide_api_types.range; + resolve_title: string; + } + -> Lsp.CodeActionResolve.result t let t_to_string : type a. a t -> string = function | Initialize_from_saved_state _ -> "Initialize_from_saved_state" @@ -204,6 +210,11 @@ let t_to_string : type a. a t -> string = function Printf.sprintf "Signature_help(%s)" (Path.to_string file_path) | Code_action ({ file_path; _ }, _) -> Printf.sprintf "Code_action(%s)" (Path.to_string file_path) + | Code_action_resolve { document = { file_path; _ }; resolve_title; _ } -> + Printf.sprintf + "Code_action_resolve(%s, %s)" + (Path.to_string file_path) + resolve_title | Find_references ({ file_path; _ }, _, _) -> Printf.sprintf "Find_references(%s)" (Path.to_string file_path) | Rename ({ file_path; _ }, _, _, _) -> diff --git a/hphp/hack/src/hh_single_type_check.ml b/hphp/hack/src/hh_single_type_check.ml index e8a919e7ee461c..f180aabd12f7c7 100644 --- a/hphp/hack/src/hh_single_type_check.ml +++ b/hphp/hack/src/hh_single_type_check.ml @@ -2041,12 +2041,24 @@ let handle_mode let (ctx, entry) = Provider_context.add_entry_if_missing ~ctx ~path in let src = Provider_context.read_file_contents_exn entry in let range = find_ide_range src in + let path = Relative_path.to_absolute path in + let resolve = + Lsp.CodeAction.( + function + | Action { title; _ } -> + (* Here (for simplicity) we are stressing CodeActionsService + more than a real client would: if the 'edit' field is provided + then the client should not request resolution but we always resolve.*) + CodeActionsService.resolve + ~ctx + ~entry + ~path + ~range + ~resolve_title:title + | Command _ as c -> c) + in let commands_or_actions = - CodeActionsService.go - ~ctx - ~entry - ~path:(Relative_path.to_absolute path) - ~range + CodeActionsService.go ~ctx ~entry ~path ~range |> List.map ~f:resolve in let hermeticize_paths = Str.global_replace (Str.regexp "\".+?.php\"") "\"FILE.php\"" @@ -2055,7 +2067,8 @@ let handle_mode Format.printf "No commands or actions found\n" else commands_or_actions - |> Lsp_fmt.print_codeActionResult + |> List.map ~f:Lsp_fmt.print_codeActionResolveResult + |> Hh_json.array_ Fn.id |> Hh_json.json_to_string ~sort_keys:true ~pretty:true |> hermeticize_paths |> Format.printf "%s\n" diff --git a/hphp/hack/src/server/codeActionsService.ml b/hphp/hack/src/server/codeActionsService.ml index ee231c0f17032c..d15c7b24cb3b1e 100644 --- a/hphp/hack/src/server/codeActionsService.ml +++ b/hphp/hack/src/server/codeActionsService.ml @@ -96,7 +96,7 @@ let text_edits (classish_starts : Pos.t SMap.t) (quickfix : Pos.t Quickfix.t) : let fix_action path (classish_starts : Pos.t SMap.t) (quickfix : Pos.t Quickfix.t) : - Lsp.CodeAction.command_or_action = + Lsp.CodeAction.resolvable_command_or_action = let open Lsp in let changes = SMap.singleton path (text_edits classish_starts quickfix) in CodeAction.Action @@ -112,7 +112,7 @@ let fix_action let refactor_action path (classish_starts : Pos.t SMap.t) (quickfix : Pos.t Quickfix.t) : - Lsp.CodeAction.command_or_action = + Lsp.CodeAction.resolvable_command_or_action = let open Lsp in let changes = SMap.singleton path (text_edits classish_starts quickfix) in CodeAction.Action @@ -131,7 +131,7 @@ let actions_for_errors (path : string) (classish_starts : Pos.t SMap.t) ~(start_line : int) - ~(start_col : int) : Lsp.CodeAction.command_or_action list = + ~(start_col : int) : Lsp.CodeAction.resolvable_command_or_action list = let errors = Errors.get_error_list ~drop_fixmed:false errors in let errors_here = List.filter errors ~f:(fun e -> @@ -154,11 +154,12 @@ let lsp_range_of_ide_range (ide_range : Ide_api_types.range) : Lsp.range = end_ = lsp_pos_of_ide_pos ide_range.I.ed; } -let go +let find ~(ctx : Provider_context.t) ~(entry : Provider_context.entry) ~(path : string) - ~(range : Ide_api_types.range) : Lsp.CodeAction.command_or_action list = + ~(range : Ide_api_types.range) : + Lsp.CodeAction.resolvable_command_or_action list = let open Ide_api_types in let start_line = range.st.line in let start_col = range.st.column in @@ -199,3 +200,59 @@ let go @ override_method_commands_or_actions @ variable_actions @ CodeActionsServiceFlipAroundComma.find ~range:lsp_range ~path ~entry ctx + +let go + ~(ctx : Provider_context.t) + ~(entry : Provider_context.entry) + ~(path : string) + ~(range : Ide_api_types.range) : Lsp.CodeAction.command_or_action list = + let open Lsp.CodeAction in + let strip : resolvable_command_or_action -> command_or_action = function + | Command _ as c -> c + | Action ({ action; _ } as a) -> + let action = + match action with + | UnresolvedEdit _ -> UnresolvedEdit () + | EditOnly e -> EditOnly e + | CommandOnly c -> CommandOnly c + | BothEditThenCommand ca -> BothEditThenCommand ca + in + Action { a with action } + in + + find ~ctx ~entry ~path ~range |> List.map ~f:strip + +let resolve + ~(ctx : Provider_context.t) + ~(entry : Provider_context.entry) + ~(path : string) + ~(range : Ide_api_types.range) + ~(resolve_title : string) : Lsp.CodeAction.resolved_command_or_action = + let open Lsp.CodeAction in + let resolve_command_or_action : + resolvable_command_or_action -> resolved_command_or_action = function + | Command _ as c -> c + | Action ({ action; _ } as a) -> + let action = + match action with + | UnresolvedEdit lazy_edit -> EditOnly (Lazy.force lazy_edit) + | EditOnly e -> EditOnly e + | CommandOnly c -> CommandOnly c + | BothEditThenCommand ca -> BothEditThenCommand ca + in + Action { a with action } + in + + find ~ctx ~entry ~path ~range + |> List.find ~f:(fun command_or_action -> + let title = Lsp_helpers.title_of_command_or_action command_or_action in + String.equal title resolve_title) + |> Option.value_exn + ~message: + {|Expected the code action requested with codeAction/resolve to be findable. +Note: This error message may be caused by the source text changing between +when the code action menu pops up and when the user selects the code action. +In such cases we may not be able to find a code action at the same location with +the same title, so cannot resolve the code action. +|} + |> resolve_command_or_action diff --git a/hphp/hack/src/server/codeActionsService.mli b/hphp/hack/src/server/codeActionsService.mli index 5a7253904b37c7..b727597c65a002 100644 --- a/hphp/hack/src/server/codeActionsService.mli +++ b/hphp/hack/src/server/codeActionsService.mli @@ -12,3 +12,11 @@ val go : path:string -> range:Ide_api_types.range -> Lsp.CodeAction.result + +val resolve : + ctx:Provider_context.t -> + entry:Provider_context.entry -> + path:string -> + range:Ide_api_types.range -> + resolve_title:string -> + Lsp.CodeActionResolve.result diff --git a/hphp/hack/src/server/codeActionsServiceExtractVariable.mli b/hphp/hack/src/server/codeActionsServiceExtractVariable.mli index 968f69c5e7fa65..ae17613b22e20b 100644 --- a/hphp/hack/src/server/codeActionsServiceExtractVariable.mli +++ b/hphp/hack/src/server/codeActionsServiceExtractVariable.mli @@ -10,4 +10,4 @@ val find : path:string -> entry:Provider_context.entry -> Provider_context.t -> - Lsp.CodeAction.command_or_action list + Lsp.CodeAction.resolvable_command_or_action list diff --git a/hphp/hack/src/server/codeActionsServiceFlipAroundComma.ml b/hphp/hack/src/server/codeActionsServiceFlipAroundComma.ml index 2576f552cce46d..f7d18fe4d52617 100644 --- a/hphp/hack/src/server/codeActionsServiceFlipAroundComma.ml +++ b/hphp/hack/src/server/codeActionsServiceFlipAroundComma.ml @@ -215,7 +215,8 @@ let edit_of_candidate let command_or_action_of_candidate ~path ~source_text candidate = let action = - Lsp.CodeAction.EditOnly (edit_of_candidate ~path ~source_text candidate) + Lsp.CodeAction.UnresolvedEdit + (lazy (edit_of_candidate ~path ~source_text candidate)) in let code_action = { diff --git a/hphp/hack/src/server/codeActionsServiceFlipAroundComma.mli b/hphp/hack/src/server/codeActionsServiceFlipAroundComma.mli index 968f69c5e7fa65..ae17613b22e20b 100644 --- a/hphp/hack/src/server/codeActionsServiceFlipAroundComma.mli +++ b/hphp/hack/src/server/codeActionsServiceFlipAroundComma.mli @@ -10,4 +10,4 @@ val find : path:string -> entry:Provider_context.entry -> Provider_context.t -> - Lsp.CodeAction.command_or_action list + Lsp.CodeAction.resolvable_command_or_action list diff --git a/hphp/hack/src/server/codeActionsServiceInlineVariable.mli b/hphp/hack/src/server/codeActionsServiceInlineVariable.mli new file mode 100644 index 00000000000000..ae17613b22e20b --- /dev/null +++ b/hphp/hack/src/server/codeActionsServiceInlineVariable.mli @@ -0,0 +1,13 @@ +(* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the "hack" directory of this source tree. + * + *) +val find : + range:Lsp.range -> + path:string -> + entry:Provider_context.entry -> + Provider_context.t -> + Lsp.CodeAction.resolvable_command_or_action list diff --git a/hphp/hack/src/server/serverCommand.ml b/hphp/hack/src/server/serverCommand.ml index d54ea6552b9962..86c4496863fc81 100644 --- a/hphp/hack/src/server/serverCommand.ml +++ b/hphp/hack/src/server/serverCommand.ml @@ -73,7 +73,8 @@ let rpc_command_needs_full_check : type a. a t -> bool = | FORMAT _ -> false | DUMP_FULL_FIDELITY_PARSE _ -> false | IDE_AUTOCOMPLETE _ -> false - | CODE_ACTIONS _ -> false + | CODE_ACTION _ -> false + | CODE_ACTION_RESOLVE _ -> false | OUTLINE _ -> false | IDE_IDLE -> false | RAGE -> false diff --git a/hphp/hack/src/server/serverCommandTypes.ml b/hphp/hack/src/server/serverCommandTypes.ml index 1578b712d29784..83121b863c5000 100644 --- a/hphp/hack/src/server/serverCommandTypes.ml +++ b/hphp/hack/src/server/serverCommandTypes.ml @@ -535,7 +535,17 @@ type _ t = | IDE_AUTOCOMPLETE : string * position * bool -> AutocompleteTypes.ide_result t - | CODE_ACTIONS : string * range -> Lsp.CodeAction.command_or_action list t + | CODE_ACTION : { + path: string; + range: range; + } + -> Lsp.CodeAction.command_or_action list t + | CODE_ACTION_RESOLVE : { + path: string; + range: range; + resolve_title: string; + } + -> Lsp.CodeAction.resolved_command_or_action t | DISCONNECT : unit t | OUTLINE : string -> Outline.outline t | IDE_IDLE : unit t diff --git a/hphp/hack/src/server/serverCommandTypesUtils.ml b/hphp/hack/src/server/serverCommandTypesUtils.ml index c138534f4774ce..de658f3de8eb85 100644 --- a/hphp/hack/src/server/serverCommandTypesUtils.ml +++ b/hphp/hack/src/server/serverCommandTypesUtils.ml @@ -52,7 +52,8 @@ let debug_describe_t : type a. a t -> string = function | CLOSE_FILE _ -> "CLOSE_FILE" | EDIT_FILE _ -> "EDIT_FILE" | IDE_AUTOCOMPLETE _ -> "IDE_AUTOCOMPLETE" - | CODE_ACTIONS _ -> "CODE_ACTIONS" + | CODE_ACTION _ -> "CODE_ACTIONS" + | CODE_ACTION_RESOLVE _ -> "CODE_ACTION_RESOLVE" | DISCONNECT -> "DISCONNECT" | OUTLINE _ -> "OUTLINE" | IDE_IDLE -> "IDE_IDLE" diff --git a/hphp/hack/src/server/serverRpc.ml b/hphp/hack/src/server/serverRpc.ml index 5a91489dbf66a7..924f1ef9344326 100644 --- a/hphp/hack/src/server/serverRpc.ml +++ b/hphp/hack/src/server/serverRpc.ml @@ -432,10 +432,16 @@ let handle : type a. genv -> env -> is_stale:bool -> a t -> env * a = ~naming_table:env.naming_table) in (env, results) - | CODE_ACTIONS (path, range) -> + | CODE_ACTION { path; range } -> let (ctx, entry) = single_ctx_path env path in let actions = CodeActionsService.go ~ctx ~entry ~path ~range in (env, actions) + | CODE_ACTION_RESOLVE { path; range; resolve_title } -> + let (ctx, entry) = single_ctx_path env path in + let action = + CodeActionsService.resolve ~ctx ~entry ~path ~range ~resolve_title + in + (env, action) | DISCONNECT -> (ServerFileSync.clear_sync_data env, ()) | OUTLINE path -> ( env, diff --git a/hphp/hack/src/utils/lsp/lsp.ml b/hphp/hack/src/utils/lsp/lsp.ml index 972991a40c7ded..dded4c56e2e2f6 100644 --- a/hphp/hack/src/utils/lsp/lsp.ml +++ b/hphp/hack/src/utils/lsp/lsp.ml @@ -250,6 +250,17 @@ module Initialize = struct | IncrementalSync [@value 2] [@@deriving enum] + module CodeActionOptions = struct + type t = { resolveProvider: bool } + end + + module CompletionOptions = struct + type t = { + resolveProvider: bool; + completion_triggerCharacters: string list; + } + end + type params = { processId: int option; rootPath: string option; @@ -333,7 +344,7 @@ module Initialize = struct and server_capabilities = { textDocumentSync: textDocumentSyncOptions; hoverProvider: bool; - completionProvider: completionOptions option; + completionProvider: CompletionOptions.t option; signatureHelpProvider: signatureHelpOptions option; definitionProvider: bool; typeDefinitionProvider: bool; @@ -342,7 +353,7 @@ module Initialize = struct documentHighlightProvider: bool; documentSymbolProvider: bool; workspaceSymbolProvider: bool; - codeActionProvider: bool; + codeActionProvider: CodeActionOptions.t option; codeLensProvider: codeLensOptions option; documentFormattingProvider: bool; documentRangeFormattingProvider: bool; @@ -354,11 +365,6 @@ module Initialize = struct rageProviderFB: bool; (** Nuclide-specific feature *) } - and completionOptions = { - resolveProvider: bool; - completion_triggerCharacters: string list; - } - and signatureHelpOptions = { sighelp_triggerCharacters: string list } and codeLensOptions = { codelens_resolveProvider: bool } @@ -535,24 +541,38 @@ module Implementation = struct and result = Location.t list end +(* textDocument/codeAction and codeAction/resolve *) module CodeAction = struct - type t = { + type 'a t = { title: string; kind: CodeActionKind.t; diagnostics: PublishDiagnostics.diagnostic list; - action: edit_and_or_command; + action: 'a edit_and_or_command; } - and edit_and_or_command = + and 'a edit_and_or_command = | EditOnly of WorkspaceEdit.t | CommandOnly of Command.t | BothEditThenCommand of (WorkspaceEdit.t * Command.t) + | UnresolvedEdit of 'a + (** UnresolvedEdit is for this flow: + client --textDocument/codeAction --> server --response_with_unresolved_fields--> + client --codeAction/resolve --> server --response_with_all_fields--> client + *) - type result = command_or_action list - - and command_or_action = + type 'a command_or_action_ = | Command of Command.t - | Action of t + | Action of 'a t + + type resolved_marker = | + + type resolved_command_or_action = resolved_marker command_or_action_ + + type resolvable_command_or_action = WorkspaceEdit.t Lazy.t command_or_action_ + + type command_or_action = unit command_or_action_ + + type result = command_or_action list end module CodeActionRequest = struct @@ -568,6 +588,19 @@ module CodeActionRequest = struct } end +module CodeActionResolve = struct + type result = CodeAction.resolved_command_or_action +end + +(** method="codeAction/resolve" *) +module CodeActionResolveRequest = struct + type params = { + data: CodeActionRequest.params; + title: string; + } +end + +(* Completion request, method="textDocument/completion" *) module Completion = struct type completionItemKind = | Text [@value 1] @@ -949,6 +982,7 @@ type lsp_request = | TypeDefinitionRequest of TypeDefinition.params | ImplementationRequest of Implementation.params | CodeActionRequest of CodeActionRequest.params + | CodeActionResolveRequest of CodeActionResolveRequest.params | CompletionRequest of Completion.params | CompletionItemResolveRequest of CompletionItemResolve.params | WorkspaceSymbolRequest of WorkspaceSymbol.params @@ -981,7 +1015,8 @@ type lsp_result = | DefinitionResult of Definition.result | TypeDefinitionResult of TypeDefinition.result | ImplementationResult of Implementation.result - | CodeActionResult of CodeAction.result + | CodeActionResult of CodeAction.result * CodeActionRequest.params + | CodeActionResolveResult of CodeActionResolve.result | CompletionResult of Completion.result | CompletionItemResolveResult of CompletionItemResolve.result | WorkspaceSymbolResult of WorkspaceSymbol.result @@ -1070,6 +1105,7 @@ let lsp_result_to_log_string = function | TypeDefinitionResult _ -> "TypeDefinitionResult" | ImplementationResult _ -> "ImplementationResult" | CodeActionResult _ -> "CodeActionResult" + | CodeActionResolveResult _ -> "CodeActionResolveResult" | CompletionResult _ -> "CompletionResult" | CompletionItemResolveResult _ -> "CompletionItemResolveResult" | WorkspaceSymbolResult _ -> "WorkspaceSymbolResult" diff --git a/hphp/hack/src/utils/lsp/lsp.mli b/hphp/hack/src/utils/lsp/lsp.mli index 8f2c3ff4bb10e9..a4ea9c70f01b26 100644 --- a/hphp/hack/src/utils/lsp/lsp.mli +++ b/hphp/hack/src/utils/lsp/lsp.mli @@ -333,6 +333,17 @@ module Initialize : sig | IncrementalSync [@value 2] [@@deriving enum] + module CodeActionOptions : sig + type t = { resolveProvider: bool } + end + + module CompletionOptions : sig + type t = { + resolveProvider: bool; (** server resolves extra info on demand *) + completion_triggerCharacters: string list; (** wire "triggerCharacters" *) + } + end + type params = { processId: int option; (** pid of parent process *) rootPath: string option; (** deprecated *) @@ -458,7 +469,7 @@ module Initialize : sig and server_capabilities = { textDocumentSync: textDocumentSyncOptions; (** how to sync *) hoverProvider: bool; - completionProvider: completionOptions option; + completionProvider: CompletionOptions.t option; signatureHelpProvider: signatureHelpOptions option; definitionProvider: bool; typeDefinitionProvider: bool; @@ -467,7 +478,7 @@ module Initialize : sig documentHighlightProvider: bool; documentSymbolProvider: bool; (** ie. document outline *) workspaceSymbolProvider: bool; (** ie. find-symbol-in-project *) - codeActionProvider: bool; + codeActionProvider: CodeActionOptions.t option; codeLensProvider: codeLensOptions option; documentFormattingProvider: bool; documentRangeFormattingProvider: bool; @@ -479,14 +490,7 @@ module Initialize : sig rageProviderFB: bool; } - and completionOptions = { - resolveProvider: bool; (** server resolves extra info on demand *) - completion_triggerCharacters: string list; (** wire "triggerCharacters" *) - } - - and signatureHelpOptions = { - sighelp_triggerCharacters: string list; (** wire "triggerCharacters" *) - } + and signatureHelpOptions = { sighelp_triggerCharacters: string list } and codeLensOptions = { codelens_resolveProvider: bool; (** wire "resolveProvider" *) @@ -697,27 +701,61 @@ end (** A code action represents a change that can be performed in code, e.g. to fix a problem or to refactor code. *) module CodeAction : sig - type t = { + (** + Note: For "textDocument/codeAction" requests we return a `data` field containing + the original request params, then when the client sends "codeAction/resolve" + we read the `data` param to re-calculate the requested code action. This + adding of the "data" field is done in our serialization step, to avoid passing + extra state around and enforce that `data` is all+only the original request params. + See [edit_or_command] for more information on the resolution flow. + *) + type 'resolution_phase t = { title: string; (** A short, human-readable, title for this code action. *) kind: CodeActionKind.t; (** The kind of the code action. Used to filter code actions. *) diagnostics: PublishDiagnostics.diagnostic list; (** The diagnostics that this code action resolves. *) - action: edit_and_or_command; - (** A CodeAction must set either `edit` and/or a `command`. - If both are supplied, the `edit` is applied first, then the `command` is executed. *) - } - - and edit_and_or_command = + action: 'resolution_phase edit_and_or_command; + (* A CodeAction must set either `edit`, a `command` (or neither iff only resolved lazily) + If both are supplied, the `edit` is applied first, then the `command` is executed. + If neither is supplied, the client requests 'edit' be resolved using "codeAction/resolve" + *) + } + + (** + 'resolution_phase is used to follow the protocol in a type-safe and prescribed manner: + LSP protocol: + 1. The client sends server "textDocument/codeAction" + 2. The server can send back an unresolved code action (neither "edit" nor "command" fields) + 3. If the code action is unresolved, the client sends "codeAction/resolve" + + Our implementation flow: + - create a resolvable code action, which includes a lazy value + - if the request is "textDocument/codeAction" replace the lazy value with `()` + - if the request is "codeAction/resolve", we have access to the original + request params via the `data` field (see [t] comments above) and perform + the same calculation as for "textDocument/codeAction" but instead of + stripping out the lazy value we Lazy.force it to produce a resolved code action. + *) + and 'resolution_phase edit_and_or_command = | EditOnly of WorkspaceEdit.t | CommandOnly of Command.t | BothEditThenCommand of (WorkspaceEdit.t * Command.t) + | UnresolvedEdit of 'resolution_phase - type result = command_or_action list - - and command_or_action = + type 'resolution_phase command_or_action_ = | Command of Command.t - | Action of t + | Action of 'resolution_phase t + + type resolved_marker = | + + type resolved_command_or_action = resolved_marker command_or_action_ + + type resolvable_command_or_action = WorkspaceEdit.t Lazy.t command_or_action_ + + type command_or_action = unit command_or_action_ + + type result = command_or_action list end (** Code Action Request, method="textDocument/codeAction" *) @@ -738,6 +776,26 @@ module CodeActionRequest : sig end (** Completion request, method="textDocument/completion" *) +module CodeActionResolve : sig + type result = CodeAction.resolved_command_or_action +end + +(** method="codeAction/resolve" *) +module CodeActionResolveRequest : sig + (** + The client sends a partially-resolved [CodeAction] with an additional [data] field. + We don't bother parsing all the fields from the partially-resolved [CodeAction] because + [data] and [title] are all we need and so we don't have to duplicate the entire + [CodeAction.command_or_action] shape here *) + type params = { + data: CodeActionRequest.params; + title: string; + (** From LSP spec: "A data entry field that is preserved on a code action between + a `textDocument/codeAction` and a `codeAction/resolve` request" + We commit to a single representation for simplicity and type-safety *) + } +end + module Completion : sig (** These numbers should match * https://microsoft.github.io/language-server-protocol/specification#textDocument_completion @@ -1129,6 +1187,7 @@ type lsp_request = | TypeDefinitionRequest of TypeDefinition.params | ImplementationRequest of Implementation.params | CodeActionRequest of CodeActionRequest.params + | CodeActionResolveRequest of CodeActionResolveRequest.params | CompletionRequest of Completion.params | CompletionItemResolveRequest of CompletionItemResolve.params | WorkspaceSymbolRequest of WorkspaceSymbol.params @@ -1161,7 +1220,8 @@ type lsp_result = | DefinitionResult of Definition.result | TypeDefinitionResult of TypeDefinition.result | ImplementationResult of Implementation.result - | CodeActionResult of CodeAction.result + | CodeActionResult of CodeAction.result * CodeActionRequest.params + | CodeActionResolveResult of CodeActionResolve.result | CompletionResult of Completion.result | CompletionItemResolveResult of CompletionItemResolve.result | WorkspaceSymbolResult of WorkspaceSymbol.result diff --git a/hphp/hack/src/utils/lsp/lsp_fmt.ml b/hphp/hack/src/utils/lsp/lsp_fmt.ml index f7c666ab95a12b..664b004eafb6ff 100644 --- a/hphp/hack/src/utils/lsp/lsp_fmt.ml +++ b/hphp/hack/src/utils/lsp/lsp_fmt.ml @@ -514,30 +514,61 @@ let parse_kinds jsons : CodeActionKind.t list = List.map ~f:parse_kind jsons |> List.filter_opt let parse_codeActionRequest (j : json option) : CodeActionRequest.params = - CodeActionRequest.( - let parse_context c : CodeActionRequest.codeActionContext = + let parse_context c : CodeActionRequest.codeActionContext = + CodeActionRequest. { diagnostics = Jget.array_exn c "diagnostics" |> List.map ~f:parse_diagnostic; only = Jget.array_opt c "only" |> Option.map ~f:parse_kinds; } - in + in + CodeActionRequest. { textDocument = Jget.obj_exn j "textDocument" |> parse_textDocumentIdentifier; range = Jget.obj_exn j "range" |> parse_range_exn; context = Jget.obj_exn j "context" |> parse_context; - }) + } + +let parse_codeActionResolveRequest (j : json option) : + CodeActionResolveRequest.params = + let data = Jget.obj_exn j "data" |> parse_codeActionRequest in + let title = Jget.string_exn j "title" in + CodeActionResolveRequest.{ data; title } (************************************************************************) -let print_codeAction (c : CodeAction.t) : json = +let print_codeAction + (c : 'a CodeAction.t) + ~(unresolved_to_code_action_request : 'a -> CodeActionRequest.params) : json + = CodeAction.( - let (edit, command) = + let (edit, command, data) = match c.action with - | EditOnly e -> (Some e, None) - | CommandOnly c -> (None, Some c) - | BothEditThenCommand (e, c) -> (Some e, Some c) + | EditOnly e -> (Some e, None, None) + | CommandOnly c -> (None, Some c, None) + | BothEditThenCommand (e, c) -> (Some e, Some c, None) + | UnresolvedEdit e -> + (None, None, Some (unresolved_to_code_action_request e)) + in + let print_params CodeActionRequest.{ textDocument; range; context } = + Hh_json.JSON_Object + [ + ( "textDocument", + Hh_json.JSON_Object + [ + ( "uri", + JSON_String + (string_of_uri textDocument.TextDocumentIdentifier.uri) ); + ] ); + ("range", print_range range); + ( "context", + Hh_json.JSON_Object + [ + ( "diagnostics", + print_diagnostic_list context.CodeActionRequest.diagnostics ); + ] ); + ] in Jprint.object_opt [ @@ -546,16 +577,32 @@ let print_codeAction (c : CodeAction.t) : json = ("diagnostics", Some (print_diagnostic_list c.diagnostics)); ("edit", Option.map edit ~f:print_documentRename); ("command", Option.map command ~f:print_command); + ("data", Option.map data ~f:print_params); ]) -let print_codeActionResult (c : CodeAction.result) : json = +let print_codeActionResult + (c : CodeAction.result) (p : CodeActionRequest.params) : json = CodeAction.( let print_command_or_action = function | Command c -> print_command c - | Action c -> print_codeAction c + | Action c -> + print_codeAction c ~unresolved_to_code_action_request:(Fn.const p) in JSON_Array (List.map c ~f:print_command_or_action)) +let print_codeActionResolveResult (c : CodeActionResolve.result) : json = + let open CodeAction in + let print_command_or_action = function + | Command c -> print_command c + | Action c -> + let unresolved_to_code_action_request : + CodeAction.resolved_marker -> CodeActionRequest.params = function + | _ -> . + in + print_codeAction c ~unresolved_to_code_action_request + in + print_command_or_action c + (************************************************************************) let print_logMessage (type_ : MessageType.t) (message : string) : json = @@ -1093,9 +1140,11 @@ let print_initialize (r : Initialize.result) : json = Option.map cap.completionProvider ~f:(fun comp -> JSON_Object [ - ("resolveProvider", JSON_Bool comp.resolveProvider); + ( "resolveProvider", + JSON_Bool comp.CompletionOptions.resolveProvider ); ( "triggerCharacters", - Jprint.string_array comp.completion_triggerCharacters + Jprint.string_array + comp.CompletionOptions.completion_triggerCharacters ); ]) ); ( "signatureHelpProvider", @@ -1115,7 +1164,14 @@ let print_initialize (r : Initialize.result) : json = Some (JSON_Bool cap.documentSymbolProvider) ); ( "workspaceSymbolProvider", Some (JSON_Bool cap.workspaceSymbolProvider) ); - ("codeActionProvider", Some (JSON_Bool cap.codeActionProvider)); + ( "codeActionProvider", + Option.map cap.codeActionProvider ~f:(fun provider -> + JSON_Object + [ + ( "resolveProvider", + JSON_Bool provider.CodeActionOptions.resolveProvider + ); + ]) ); ( "codeLensProvider", Option.map cap.codeLensProvider ~f:(fun codelens -> JSON_Object @@ -1262,6 +1318,8 @@ let get_uri_opt (m : lsp_message) : Lsp.documentUri option = Some p.TextDocumentPositionParams.textDocument.uri | RequestMessage (_, CodeActionRequest p) -> Some p.CodeActionRequest.textDocument.uri + | RequestMessage (_, CodeActionResolveRequest p) -> + Some p.CodeActionResolveRequest.data.CodeActionRequest.textDocument.uri | RequestMessage (_, CompletionRequest p) -> Some p.Completion.loc.TextDocumentPositionParams.textDocument.uri | RequestMessage (_, DocumentSymbolRequest p) -> @@ -1340,6 +1398,7 @@ let request_name_to_string (request : lsp_request) : string = | CodeLensResolveRequest _ -> "codeLens/resolve" | HoverRequest _ -> "textDocument/hover" | CodeActionRequest _ -> "textDocument/codeAction" + | CodeActionResolveRequest _ -> "codeAction/resolve" | CompletionRequest _ -> "textDocument/completion" | CompletionItemResolveRequest _ -> "completionItem/resolve" | DefinitionRequest _ -> "textDocument/definition" @@ -1374,6 +1433,7 @@ let result_name_to_string (result : lsp_result) : string = | CodeLensResolveResult _ -> "codeLens/resolve" | HoverResult _ -> "textDocument/hover" | CodeActionResult _ -> "textDocument/codeAction" + | CodeActionResolveResult _ -> "codeAction/resolve" | CompletionResult _ -> "textDocument/completion" | CompletionItemResolveResult _ -> "completionItem/resolve" | DefinitionResult _ -> "textDocument/definition" @@ -1457,6 +1517,8 @@ let parse_lsp_request (method_ : string) (params : json option) : lsp_request = | "textDocument/hover" -> HoverRequest (parse_hover params) | "textDocument/codeAction" -> CodeActionRequest (parse_codeActionRequest params) + | "codeAction/resolve" -> + CodeActionResolveRequest (parse_codeActionResolveRequest params) | "textDocument/completion" -> CompletionRequest (parse_completion params) | "completionItem/resolve" -> CompletionItemResolveRequest (parse_completionItem params) @@ -1537,6 +1599,7 @@ let parse_lsp_result (request : lsp_request) (result : json) : lsp_result = | CodeLensResolveRequest _ | HoverRequest _ | CodeActionRequest _ + | CodeActionResolveRequest _ | CompletionRequest _ | CompletionItemResolveRequest _ | DefinitionRequest _ @@ -1608,6 +1671,7 @@ let print_lsp_request (id : lsp_id) (request : lsp_request) : json = | ShutdownRequest | HoverRequest _ | CodeActionRequest _ + | CodeActionResolveRequest _ | CodeLensResolveRequest _ | CompletionRequest _ | CompletionItemResolveRequest _ @@ -1651,7 +1715,8 @@ let print_lsp_response (id : lsp_id) (result : lsp_result) : json = | ShutdownResult -> print_shutdown () | CodeLensResolveResult r -> print_codeLensResolve r | HoverResult r -> print_hover r - | CodeActionResult r -> print_codeActionResult r + | CodeActionResult (r, p) -> print_codeActionResult r p + | CodeActionResolveResult r -> print_codeActionResolveResult r | CompletionResult r -> print_completion r | CompletionItemResolveResult r -> print_completionItem r | DefinitionResult r -> print_definition_locations r diff --git a/hphp/hack/src/utils/lsp/lsp_fmt.mli b/hphp/hack/src/utils/lsp/lsp_fmt.mli index 957a9d04c2ad2c..e4e62b4a2ef03c 100644 --- a/hphp/hack/src/utils/lsp/lsp_fmt.mli +++ b/hphp/hack/src/utils/lsp/lsp_fmt.mli @@ -91,7 +91,10 @@ val print_documentRename : Lsp.Rename.result -> Hh_json.json val print_diagnostics : Lsp.PublishDiagnostics.params -> Hh_json.json -val print_codeActionResult : Lsp.CodeAction.result -> Hh_json.json +val print_codeActionResult : + Lsp.CodeAction.result -> Lsp.CodeActionRequest.params -> Hh_json.json + +val print_codeActionResolveResult : Lsp.CodeActionResolve.result -> Hh_json.json val print_logMessage : Lsp.MessageType.t -> string -> Hh_json.json diff --git a/hphp/hack/src/utils/lsp/lsp_helpers.ml b/hphp/hack/src/utils/lsp/lsp_helpers.ml index df3c0eb9808d4a..7ce5ccc6eda940 100644 --- a/hphp/hack/src/utils/lsp/lsp_helpers.ml +++ b/hphp/hack/src/utils/lsp/lsp_helpers.ml @@ -439,3 +439,9 @@ let showMessage_warning (writer : Jsonrpc.writer) = let showMessage_error (writer : Jsonrpc.writer) = showMessage writer MessageType.ErrorMessage + +let title_of_command_or_action = + Lsp.CodeAction.( + function + | Command Command.{ title; _ } -> title + | Action { title; _ } -> title) diff --git a/hphp/hack/src/utils/lsp/lsp_helpers.mli b/hphp/hack/src/utils/lsp/lsp_helpers.mli index 9e0138eea640b6..a51e0ee20f6e80 100644 --- a/hphp/hack/src/utils/lsp/lsp_helpers.mli +++ b/hphp/hack/src/utils/lsp/lsp_helpers.mli @@ -129,3 +129,5 @@ val showMessage_info : Jsonrpc.writer -> string -> unit val showMessage_warning : Jsonrpc.writer -> string -> unit val showMessage_error : Jsonrpc.writer -> string -> unit + +val title_of_command_or_action : 'a Lsp.CodeAction.command_or_action_ -> string diff --git a/hphp/hack/test/integration/data/lsp_exchanges/code_action_flip_around_comma.php b/hphp/hack/test/integration/data/lsp_exchanges/code_action_flip_around_comma.php new file mode 100644 index 00000000000000..44fc7f744ebd5c --- /dev/null +++ b/hphp/hack/test/integration/data/lsp_exchanges/code_action_flip_around_comma.php @@ -0,0 +1,5 @@ + None: + """This test is mainly for testing lazy code action resolution: + - The server returns a code action with neither 'edit' nor 'command' field + - The client must send `codeAction/resolve` + - The server then replies with a complete code action + """ + variables = dict(self.prepare_serverless_ide_environment()) + variables.update(self.setup_php_file("code_action_flip_around_comma.php")) + self.test_driver.stop_hh_server() + + spec = ( + self.initialize_spec( + LspTestSpec("code_action_flip_around_comma"), use_serverless_ide=True + ) + .notification( + method="textDocument/didOpen", + params={ + "textDocument": { + "uri": "${php_file_uri}", + "languageId": "hack", + "version": 1, + "text": "${php_file}", + } + }, + ) + .request( + line=line(), + comment="get actions", + method="textDocument/codeAction", + params={ + "textDocument": {"uri": "${php_file_uri}"}, + "range": { + "start": {"line": 3, "character": 10}, + "end": {"line": 3, "character": 10}, + }, + "context": {"diagnostics": []}, + }, + result=[ + { + "title": "Flip around comma", + "kind": "refactor", + "diagnostics": [], + "data": { + "textDocument": {"uri": "${php_file_uri}"}, + "range": { + "start": {"line": 3, "character": 10}, + "end": {"line": 3, "character": 10}, + }, + "context": {"diagnostics": []}, + }, + } + ], + powered_by="serverless_ide", + ) + .request( + line=line(), + comment="resolve code action", + method="codeAction/resolve", + params={ + "title": "Flip around comma", + "data": { + "textDocument": {"uri": "${php_file_uri}"}, + "range": { + "start": {"line": 3, "character": 10}, + "end": {"line": 3, "character": 10}, + }, + "context": {"diagnostics": []}, + }, + "kind": "refactor", + "diagnostics": [], + }, + result={ + "title": "Flip around comma", + "kind": "refactor", + "diagnostics": [], + "edit": { + "changes": { + "${root_path}/code_action_flip_around_comma.php": [ + { + "range": { + "start": {"line": 3, "character": 6}, + "end": {"line": 3, "character": 19}, + }, + "newText": '"b", "a", "c"', + } + ] + } + }, + }, + powered_by="serverless_ide", + ) + .request(line=line(), method="shutdown", params={}, result=None) + .notification(method="exit", params={}) + ) + self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True) + def test_non_blocking(self) -> None: self.prepare_server_environment() variables = self.setup_php_file("non_blocking.php")