Skip to content

Commit

Permalink
[flow][paste-provider] Compute results from server-side
Browse files Browse the repository at this point in the history
Summary:
This stack of diffs aims to auto import on paster support similar to this feature of TS: https://code.visualstudio.com/updates/v1_96#_paste-with-imports-for-javascript-and-typescript

This diff implements the core logic on the server side. It closely mirrors the VSCode API, which is split into two phases:

1. Prepare phase: where we compute some metadata for the copied code. In our case, we will compute the imports that need to be inserted. We use scope information to find out all the imported names that would be unbounded after paste. Then we extract information about the import: notably, we try to record the full resolved path of the imported module, so that during paste time, we can re-compute the right import path based on the directory of the pasted file. This is necessary to compute the right relative import for NodeJS resolution algo.
2. Paste phase: it's relatively simple for now: we will just apply the extracted import information as edit, using the existing autofix_import infra. For now, it doesn't account for existing import yet, so duplicate ones might be inserted. Note that this step is completely syntactic, so it can be handled immediately, which ensures that queuing in Flow won't block save.

Changelog: [internal]

Reviewed By: gkz

Differential Revision: D68074700

fbshipit-source-id: 158110dcbc75ab9e3ca7661e6c031396ce2a2f91
  • Loading branch information
SamChou19815 authored and facebook-github-bot committed Jan 14, 2025
1 parent 88e7973 commit 35328df
Show file tree
Hide file tree
Showing 3 changed files with 345 additions and 0 deletions.
121 changes: 121 additions & 0 deletions src/server/command_handler/commandHandler.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1319,6 +1319,96 @@ let auto_close_jsx ~options ~env ~profiling ~params ~client =
(Ok edit, None)
end

let prepare_document_paste
~options
~env
~profiling
~params:(DocumentPaste.PrepareParams { uri; ranges })
~client
~loc_of_aloc =
let text_document = { TextDocumentIdentifier.uri } in
let file_input = file_input_of_text_document_identifier ~client text_document in
match of_file_input ~options ~env file_input with
| Error (Failed _) -> ([], None)
| Error (Skipped reason) ->
let extra_data = json_of_skipped reason in
([], extra_data)
| Ok (file_key, contents) ->
let (check_result, did_hit_cache) =
match
let parse_result =
lazy (Type_contents.parse_contents ~options ~profiling contents file_key)
in
let type_parse_artifacts_cache =
Some (Persistent_connection.type_parse_artifacts_cache client)
in
type_parse_artifacts_with_cache
~options
~profiling
~type_parse_artifacts_cache
env.master_cx
file_key
parse_result
with
| (Ok result, did_hit_cache) -> (Ok result, did_hit_cache)
| (Error _parse_errors, did_hit_cache) ->
(Error "Couldn't parse file in parse_contents", did_hit_cache)
in
(match check_result with
| Error msg ->
let json_props = [("error", Hh_json.JSON_String msg)] in
let json_props = add_cache_hit_data_to_json json_props did_hit_cache in
([], Some (Hh_json.JSON_Object json_props))
| Ok (Parse_artifacts { ast; _ }, Typecheck_artifacts { cx; typed_ast; _ }) ->
let import_items =
Document_paste.prepare_document_paste
cx
~loc_of_aloc
~ast
~typed_ast
~ranges:(List.map (Lsp.lsp_range_to_flow_loc ~source:file_key) ranges)
in
let json_props = add_cache_hit_data_to_json [] did_hit_cache in
(import_items, Some (Hh_json.JSON_Object json_props)))

let provide_document_paste ~options ~reader ~profiling ~params =
let DocumentPaste.(
ProvideParams
{
text_document = { TextDocumentItem.uri; text; _ };
ranges = _;
data_transfer = ImportMetadata { imports };
}) =
params
in
let (edits, extra_data) =
let file_key =
File_key.SourceFile
(Flow_lsp_conversions.lsp_DocumentIdentifier_to_flow_path { TextDocumentIdentifier.uri })
in
match Type_contents.parse_contents ~options ~profiling text file_key |> fst with
| None ->
let json_props = [("error", Hh_json.JSON_String "Failed to parse")] in
([], Some (Hh_json.JSON_Object json_props))
| Some (Parse_artifacts { ast; _ }) ->
let src_dir = Some (File_key.to_string file_key |> Filename.dirname) in
let edits =
Document_paste.provide_document_paste_edits
~layout_options:(Code_action_utils.layout_options options)
~module_system_info:(mk_module_system_info ~options ~reader)
~src_dir
ast
imports
in
(edits, Some (Hh_json.JSON_Object []))
in
let edits =
Base.List.fold_right edits ~init:[] ~f:(fun (loc, newText) acc ->
{ TextEdit.range = Lsp.loc_to_lsp_range loc; newText } :: acc
)
in
({ Lsp.WorkspaceEdit.changes = Lsp.UriMap.singleton uri edits }, extra_data)

let linked_editing_range ~options ~env ~profiling ~params ~client =
let text_document = params.TextDocumentPositionParams.textDocument in
let file_input = file_input_of_text_document_identifier ~client text_document in
Expand Down Expand Up @@ -3389,6 +3479,30 @@ let handle_persistent_auto_close_jsx ~options ~id ~params ~metadata ~client ~pro
Lwt.return
(LspProt.LspFromServer (Some (ResponseMessage (id, AutoCloseJsxResult text_opt))), metadata)

let handle_persistent_prepare_document_paste
~options ~id ~params ~metadata ~client ~profiling ~env ~loc_of_aloc =
let (imports, extra_data) =
prepare_document_paste ~options ~env ~profiling ~params ~client ~loc_of_aloc
in
let metadata = with_data ~extra_data metadata in
Lwt.return
( LspProt.LspFromServer
(Some
(ResponseMessage
(id, PrepareDocumentPasteResult (Lsp.DocumentPaste.ImportMetadata { imports }))
)
),
metadata
)

let handle_persistent_provide_document_paste_edits
~options ~reader ~id ~params ~metadata ~client:_ ~profiling =
let (workspace_edit, extra_data) = provide_document_paste ~options ~reader ~profiling ~params in
let metadata = with_data ~extra_data metadata in
( LspProt.LspFromServer (Some (ResponseMessage (id, ProvideDocumentPasteResult workspace_edit))),
metadata
)

let handle_persistent_linked_editing_range ~options ~id ~params ~metadata ~client ~profiling ~env =
let (result, extra_data) = linked_editing_range ~options ~env ~profiling ~params ~client in
let metadata = with_data ~extra_data metadata in
Expand Down Expand Up @@ -3849,6 +3963,13 @@ let get_persistent_handler ~genv ~client_id ~request:(request, metadata) :
mk_parallelizable_persistent
~options
(handle_persistent_auto_close_jsx ~options ~id ~params ~metadata)
| LspToServer (RequestMessage (id, PrepareDocumentPasteRequest params)) ->
mk_parallelizable_persistent
~options
(handle_persistent_prepare_document_paste ~options ~id ~params ~metadata ~loc_of_aloc)
| LspToServer (RequestMessage (id, ProvideDocumentPasteRequest params)) ->
Handle_persistent_immediately
(handle_persistent_provide_document_paste_edits ~options ~reader ~id ~params ~metadata)
| LspToServer (RequestMessage (id, LinkedEditingRangeRequest params)) ->
mk_parallelizable_persistent
~options
Expand Down
200 changes: 200 additions & 0 deletions src/services/code_action/document_paste.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
(*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)

module Ast = Flow_ast

class imported_def_collector ~scope ~ranges =
object (this)
inherit [Loc.t] Flow_ast_mapper.mapper

val mutable import_def_locs : (string * Loc.t Nel.t) list = []

method import_def_locs = import_def_locs

method private loc_within_range l =
Base.List.exists ranges ~f:(fun range -> Loc.contains range l)

method private collect_relevant_def_loc_of_imported_identifier use_loc =
if this#loc_within_range use_loc then
match Scope_api.With_Loc.def_of_use_opt scope use_loc with
| Some { Scope_api.With_Loc.Def.name = _; locs; kind = _; actual_name } ->
(* All uses within the range, but definition outside of the range is potential
* unbound import after paste candidate. In import_information_extractor, we will
* further filter it down to imports. *)
if not (Nel.exists this#loc_within_range locs) then
import_def_locs <- (actual_name, locs) :: import_def_locs
| None -> ()

method! identifier ((loc, _) as id) =
this#collect_relevant_def_loc_of_imported_identifier loc;
id
end

class import_information_extractor ~cx ~loc_of_aloc ~relevant_imported_defs =
object (_this)
inherit [ALoc.t, ALoc.t * Type.t, ALoc.t, ALoc.t * Type.t] Flow_polymorphic_ast_mapper.mapper

val mutable import_items = []

method import_items = import_items

method on_loc_annot l = l

method on_type_annot l = l

method! import_declaration _loc decl =
let open Ast.Statement.ImportDeclaration in
let { import_kind; source; specifiers; default; comments = _ } = decl in
let lazy_import_source_info =
lazy
(let ((_, source_t), { Ast.StringLiteral.value; _ }) = source in
match TypeUtil.def_loc_of_t source_t |> ALoc.source with
| Some f ->
if Context.file cx = f || File_key.is_lib_file f then
(value, false)
else
(File_key.to_string f, true)
| None -> (value, false)
)
in
let collect ~import_type ~remote ~local_opt =
let ((l, _), { Ast.Identifier.name; comments = _ }) =
Base.Option.value ~default:remote local_opt
in
if
Base.List.exists relevant_imported_defs ~f:(fun (n, locs) ->
name = n && Nel.mem ~equal:Loc.equal (loc_of_aloc l) locs
)
then
let (import_source, import_source_is_resolved) = Lazy.force lazy_import_source_info in
let name_of_id (_, { Ast.Identifier.name; comments = _ }) = name in
let import_item =
{
Lsp.DocumentPaste.remote_name = name_of_id remote;
local_name = Option.map name_of_id local_opt;
import_type;
import_source;
import_source_is_resolved;
}
in
import_items <- import_item :: import_items
in
Base.Option.iter specifiers ~f:(function
| ImportNamedSpecifiers specifiers ->
Base.List.iter specifiers ~f:(fun { kind; local; remote; remote_name_def_loc = _ } ->
let import_type =
match Base.Option.value ~default:import_kind kind with
| ImportType -> Lsp.DocumentPaste.ImportNamedType
| ImportTypeof -> Lsp.DocumentPaste.ImportNamedTypeOf
| ImportValue -> Lsp.DocumentPaste.ImportNamedValue
in
collect ~import_type ~remote ~local_opt:local
)
| ImportNamespaceSpecifier (_, id) ->
(match import_kind with
| ImportType -> ()
| ImportTypeof ->
collect
~import_type:Lsp.DocumentPaste.ImportTypeOfAsNamespace
~remote:id
~local_opt:None
| ImportValue ->
collect
~import_type:Lsp.DocumentPaste.ImportValueAsNamespace
~remote:id
~local_opt:None)
);
Base.Option.iter default ~f:(fun { identifier; remote_default_name_def_loc = _ } ->
let import_type =
match import_kind with
| ImportType -> Lsp.DocumentPaste.ImportNamedType
| ImportTypeof -> Lsp.DocumentPaste.ImportNamedTypeOf
| ImportValue -> Lsp.DocumentPaste.ImportNamedValue
in
collect
~import_type
~remote:(fst identifier, { Ast.Identifier.name = "default"; comments = None })
~local_opt:(Some identifier)
);
decl
end

let prepare_document_paste cx ~loc_of_aloc ~ast ~typed_ast ~ranges =
let relevant_imported_defs =
let collector =
new imported_def_collector
~scope:(Scope_builder.program ~enable_enums:(Context.enable_enums cx) ~with_types:true ast)
~ranges
in
ignore @@ collector#program ast;
collector#import_def_locs
in
let extractor = new import_information_extractor ~cx ~loc_of_aloc ~relevant_imported_defs in
ignore @@ extractor#program typed_ast;
extractor#import_items

let provide_document_paste_edits ~layout_options ~module_system_info ~src_dir ast import_items =
let scope_info = Scope_builder.program ~enable_enums:true ~with_types:true ast in
let module_scope_defs =
Scope_api.With_Loc.toplevel_scopes
|> Base.List.fold ~init:SSet.empty ~f:(fun acc scope_id ->
let { Scope_api.With_Loc.Scope.defs; _ } =
Scope_api.With_Loc.scope scope_info scope_id
in
Base.List.fold ~init:acc ~f:(fun acc s -> SSet.add s acc) (SMap.keys defs)
)
in
let added_imports =
Base.List.filter_map
import_items
~f:(fun
{
Lsp.DocumentPaste.remote_name;
local_name;
import_type;
import_source;
import_source_is_resolved;
}
->
if SSet.mem (Base.Option.value ~default:remote_name local_name) module_scope_defs then
(* If the name is already bound locally, then we won't try to import it here.
* The already bound name might not be the import, but error on the side of not
* introducing duplicate bindings. *)
None
else
match
Lsp_import_edits.from_of_source
~module_system_info
~src_dir
( if import_source_is_resolved then
Export_index.File_key (File_key.SourceFile import_source)
else
Export_index.Builtin import_source
)
with
| None -> None
| Some from ->
(match import_type with
| Lsp.DocumentPaste.ImportNamedType ->
if remote_name = "default" then
Some (from, Autofix_imports.DefaultType (Base.Option.value_exn local_name))
else
Some (from, Autofix_imports.(NamedType [{ remote_name; local_name }]))
| Lsp.DocumentPaste.ImportNamedTypeOf ->
None (* TODO: make Autofix_imports support this kind *)
| Lsp.DocumentPaste.ImportNamedValue ->
if remote_name = "default" then
Some (from, Autofix_imports.Default (Base.Option.value_exn local_name))
else
Some (from, Autofix_imports.(Named [{ remote_name; local_name }]))
| Lsp.DocumentPaste.ImportTypeOfAsNamespace ->
None (* TODO: make Autofix_imports support this kind *)
| Lsp.DocumentPaste.ImportValueAsNamespace ->
Some (from, Autofix_imports.Namespace remote_name))
)
in
Autofix_imports.add_imports ~options:layout_options ~added_imports ast
24 changes: 24 additions & 0 deletions src/services/code_action/document_paste.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
(*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)

module Ast = Flow_ast

val prepare_document_paste :
Context.t ->
loc_of_aloc:(ALoc.t -> Loc.t) ->
ast:(Loc.t, Loc.t) Ast.Program.t ->
typed_ast:(ALoc.t, ALoc.t * Type.t) Ast.Program.t ->
ranges:Loc.t list ->
Lsp.DocumentPaste.import_item list

val provide_document_paste_edits :
layout_options:Js_layout_generator.opts ->
module_system_info:Lsp_module_system_info.t ->
src_dir:string option ->
(Loc.t, Loc.t) Ast.Program.t ->
Lsp.DocumentPaste.import_item list ->
(Loc.t * string) list

0 comments on commit 35328df

Please sign in to comment.