TTudor BrindusUse OxCaml
11c9f605创建于 2025年12月12日历史提交
(* Async is opened later after the Blocking submodule *)
open! Core
open! Import
module Unix = Core_unix
module Io = Io

module Streaming = struct
  type _ t = T : 'a Io.t * Io.Input.t Async.Pipe.Reader.t -> _ t

  let of_escaped_strings reader =
    T
      ( Io.of_escaped_string
      , Async.Pipe.map reader ~f:(fun x ->
          match Io.(encode of_escaped_string x) with
          | Error _ -> .
          | Ok x -> x) )
  ;;

  let of_strings_raise_on_newlines reader =
    T
      ( Io.of_human_friendly_string
      , Async.Pipe.map reader ~f:(fun x ->
          match Io.(encode of_human_friendly_string x) with
          | Error `String_contains_newline ->
            raise_s [%message "string unexpectedly contains newline" ~_:x]
          | Ok x -> x) )
  ;;
end

module Pick_from : sig
  type _ t =
    | Map : 'a String.Map.t -> 'a t
    | Assoc : (string * 'a) list -> 'a t
    | Inputs : string list -> string t
    | Command_output : string -> string t
    | Streaming : _ Streaming.t -> string t

  val map : 'a String.Map.t -> 'a t
  val assoc : (string * 'a) list -> 'a t
  val inputs : string list -> string t
  val command_output : string -> string t
  val streaming : _ Streaming.t -> string t

  module Of_stringable : sig
    val map : (module Stringable with type t = 't) -> ('t, 'a, _) Map.t -> 'a t
    val assoc : (module Stringable with type t = 't) -> ('t, 'a) List.Assoc.t -> 'a t
    val inputs : (module Stringable with type t = 't) -> 't List.t -> 't t
  end

  (** A [Pick_from.Encoded.t] takes care to convert client provided keys into
      'fzf-friendly' strings (i.e., not containing any newlines), and maps the
      'fzf-friendly' output from Fzf back into client-provided keys.
  *)
  module Encoded : sig
    type 'a unencoded := 'a t
    type 'a t

    val create : 'a unencoded -> 'a t
    val to_list : _ t -> Io.Input.t list
    val lookup_selection : 'a t -> Io.Output.t -> 'a
  end
end = struct
  type _ t =
    | Map : 'a String.Map.t -> 'a t
    | Assoc : (string * 'a) list -> 'a t
    | Inputs : string list -> string t
    | Command_output : string -> string t
    | Streaming : (_ Streaming.t[@sexp.opaque]) -> string t
  [@@deriving sexp_of]

  let to_list (type a) (t : a t) : string list =
    match t with
    | Map values -> Map.keys values
    | Assoc entries -> List.map ~f:fst entries
    | Inputs l -> l
    | Command_output (_ : string) -> []
    | Streaming (_ : _ Streaming.t) -> []
  ;;

  let inputs x = Inputs x
  let map x = Map x
  let assoc x = Assoc x
  let command_output x = Command_output x
  let streaming s = Streaming s

  module Of_stringable = struct
    let assoc (type t) (module S : Stringable with type t = t) assoc =
      let map =
        List.map assoc ~f:(fun (key, v) -> S.to_string key, v) |> String.Map.of_alist_exn
      in
      Map map
    ;;

    let map s map = Map.to_alist map |> assoc s
    let inputs s inputs = List.map inputs ~f:(fun s -> s, s) |> assoc s

    let%expect_test "assoc" =
      let module T = struct
        module T = struct
          type t =
            | Thing
            | Amabob
          [@@deriving enumerate, sexp]
        end

        include T
        include Sexpable.To_stringable (T)
      end
      in
      print_s [%sexp (assoc (module T) [ Thing, "t"; T.Amabob, "a" ] : string t)];
      [%expect {| (Map ((Amabob a) (Thing t))) |}]
    ;;
  end

  let lookup_selection (type a) (t : a t) (selection : string) : a =
    match t with
    | Map map ->
      (match Map.find map selection with
       | Some x -> x
       | None ->
         raise_s
           [%message
             "Fzf bug: String selected that was not a map key"
               selection
               (map : _ String.Map.t)])
    | Assoc alist ->
      (match List.Assoc.find ~equal:String.equal alist selection with
       | Some x -> x
       | None ->
         raise_s
           [%message
             "Fzf bug: string selected was not in selections"
               selection
               ~selections:(alist : (string * _) list)])
    | Inputs _ -> (selection : a)
    | Command_output (_ : string) -> (selection : a)
    | Streaming (_ : _ Streaming.t) -> (selection : a)
  ;;

  module Encoded = struct
    type 'a pick_from = 'a t

    type 'a t =
      { pick_from : 'a pick_from
      ; encoded : Io.Input.t list
      ; decode_exn : Io.Output.t -> string
      }

    let decode_exn codec selection ~message =
      match Io.decode codec selection with
      | Ok decoded -> decoded
      | Error (`Decoded_with_inconsistent_codec err) ->
        raise_s [%message message (selection : Io.Output.t) (err : Error.t)]
    ;;

    let create (type a) (pick_from : a pick_from) : a t =
      let unencoded = to_list pick_from in
      let encode_all (type err) (codec : err Io.t) : (a t, err) Result.t =
        let encode_results = List.map unencoded ~f:(Io.encode codec) in
        match List.partition_result encode_results with
        | encoded, [] ->
          let decode_exn =
            decode_exn
              codec
              ~message:"Fzf bug: string selected that was not in expected format"
          in
          Ok { pick_from; encoded; decode_exn }
        | (_ : Io.Input.t list), err :: (_ : err list) -> Error err
      in
      match pick_from with
      | Streaming (T (codec, _)) ->
        { pick_from
        ; encoded = []
        ; decode_exn =
            decode_exn
              codec
              ~message:"string selected that was not encoded with the right Fzf.Io format"
        }
      | _ ->
        (match encode_all Io.of_human_friendly_string with
         | Ok t -> t
         | Error `String_contains_newline ->
           (match encode_all Io.of_escaped_string with
            | Ok t -> t
            | Error _ -> .))
    ;;

    let lookup_selection (type a) (t : a t) (selection : Io.Output.t) : a =
      let selection = t.decode_exn selection in
      lookup_selection t.pick_from selection
    ;;

    let to_list t = t.encoded
  end
end

module Tiebreak = struct
  module T = struct
    type t =
      | Length
      | Begin
      | End
      | Index
    [@@deriving compare, enumerate, equal, sexp]
  end

  include T
  include Sexpable.To_stringable (T)

  let to_string = Fn.compose String.lowercase to_string

  let%test_unit "roundtrip" =
    List.iter all ~f:(fun t -> [%test_result: t] ~expect:t (of_string (to_string t)))
  ;;

  let%expect_test "demonstrate to_string" =
    List.iter all ~f:(fun t -> print_endline (to_string t));
    [%expect {|
      length
      begin
      end
      index |}]
  ;;
end

type ('a, 'return) pick_fun =
  ?select1:unit
  -> ?query:string
  -> ?header:string
  -> ?preview:string
  -> ?preview_window:string
  -> ?no_sort:unit
  -> ?reverse_input:unit
  -> ?prompt_at_top:unit
  -> ?with_nth:string
  -> ?nth:string
  -> ?delimiter:string
  -> ?height:int
  -> ?bind:string Nonempty_list.t
  -> ?tiebreak:Tiebreak.t Nonempty_list.t
  -> ?filter:string
  -> ?border:[ `rounded | `sharp | `horizontal ]
  -> ?info:[ `default | `inline | `hidden ]
  -> ?exact_match:unit
  -> ?no_hscroll:unit
  -> 'a Pick_from.t
  -> 'return

module Blocking = struct
  let prog = "fzf"

  let really_write_with_newline fd str =
    let str = str ^ "\n" in
    let rec loop pos =
      let pos = pos + Unix.single_write_substring fd ~pos ~buf:str in
      if String.length str > pos then loop pos
    in
    loop 0
  ;;

  let make_command_option ~key value = sprintf "--%s=%s" key value

  let shuttle_pipe_strings_to_fd_then_close pipe stdin_wr =
    let open Async in
    let shuttle_strings =
      let%map () =
        Async.Pipe.iter pipe ~f:(fun (str : Io.Input.t) ->
          match really_write_with_newline stdin_wr (str :> string) with
          | exception Core_unix.Unix_error _ -> Deferred.unit
          | () -> Deferred.unit)
      in
      Core_unix.close stdin_wr
    in
    don't_wait_for shuttle_strings
  ;;

  let pick
        ?select1
        ?query
        ?header
        ?preview
        ?preview_window
        ?no_sort
        ?reverse_input
        ?prompt_at_top
        ?with_nth
        ?nth
        ?delimiter
        ?height
        ?bind
        ?tiebreak
        ?filter
        ?border
        ?info
        ?exact_match
        ?select_many
        ?no_hscroll
        (entries :
           [ `Streaming of Io.Input.t Async.Pipe.Reader.t
           | `List of Io.Input.t Nonempty_list.t
           | `Command_output of string
           ])
        ~buffer_size
        ~on_result
        ~pid_ivar
    =
    let stdout_rd, stdout_wr = Unix.pipe () in
    (* In the past, this [pipe] was created in the child, and written to there.
       Unix pipes have a limited size buffer, and will block on writes if it is filled.
       Therefore, we need to create the pipe in a way that the parent can feed bytes
       into it while [fzf] is reading said bytes. *)
    let stdin_rd, stdin_wr = Unix.pipe () in
    match Unix.fork () with
    | `In_the_child ->
      Unix.dup2 ~dst:Unix.stdin ~src:stdin_rd ();
      Unix.dup2 ~dst:Unix.stdout ~src:stdout_wr ();
      Unix.close stdin_wr;
      let args =
        ([ Option.map query ~f:(make_command_option ~key:"query")
         ; Option.map header ~f:(make_command_option ~key:"header")
         ; Option.map select1 ~f:(Fn.const "--select-1")
         ; Option.map preview ~f:(make_command_option ~key:"preview")
         ; Option.map preview_window ~f:(make_command_option ~key:"preview-window")
         ; Option.map no_sort ~f:(Fn.const "--no-sort")
         ; Option.map reverse_input ~f:(Fn.const "--tac")
         ; Option.map prompt_at_top ~f:(Fn.const "--reverse")
         ; Option.map with_nth ~f:(make_command_option ~key:"with-nth")
         ; Option.map nth ~f:(make_command_option ~key:"nth")
         ; Option.map delimiter ~f:(make_command_option ~key:"delimiter")
         ; Option.map height ~f:(fun h ->
             make_command_option ~key:"height" (Int.to_string h))
         ; Option.map filter ~f:(make_command_option ~key:"filter")
         ; Option.map border ~f:(fun x ->
             [%sexp_of: [ `rounded | `sharp | `horizontal ]] x
             |> Sexp.to_string
             |> make_command_option ~key:"border")
         ; Option.map info ~f:(fun x ->
             [%sexp_of: [ `default | `inline | `hidden ]] x
             |> Sexp.to_string
             |> make_command_option ~key:"info")
         ; Option.map exact_match ~f:(Fn.const "--exact")
         ; Option.map select_many ~f:(Fn.const "-m")
         ; Option.map
             select_many
             ~f:(Fn.const (make_command_option ~key:"bind" "ctrl-a:toggle-all"))
         ; Option.map tiebreak ~f:(fun tiebreaks ->
             let value =
               Nonempty_list.to_list tiebreaks
               |> List.map ~f:Tiebreak.to_string
               |> String.concat ~sep:","
             in
             make_command_option ~key:"tiebreak" value)
         ; Option.map no_hscroll ~f:(Fn.const "--no-hscroll")
         ; (match entries with
            | `List (_ : Io.Input.t Nonempty_list.t) -> None
            | `Streaming (_ : Io.Input.t Async.Pipe.Reader.t) -> None
            | `Command_output command ->
              Some (make_command_option ~key:"bind" [%string "change:reload:%{command}"]))
         ]
         |> List.filter_opt)
        @ Option.value_map bind ~default:[] ~f:(fun bindings ->
          Nonempty_list.to_list bindings
          |> List.map ~f:(fun binding -> make_command_option ~key:"bind" binding))
      in
      never_returns (Unix.exec ~prog ~argv:(prog :: args) ())
    | `In_the_parent pid ->
      Unix.close stdin_rd;
      (match entries with
       | `List entries ->
         let entries = (entries :> string Nonempty_list.t) in
         Nonempty_list.iter entries ~f:(really_write_with_newline stdin_wr);
         Unix.close stdin_wr
       | `Streaming pipe -> shuttle_pipe_strings_to_fd_then_close pipe stdin_wr
       | `Command_output (_ : string) -> Unix.close stdin_wr);
      Option.iter pid_ivar ~f:(fun ivar -> Async.Ivar.fill_exn ivar pid);
      let (_ : Unix.Exit_or_signal.t) = Unix.waitpid pid in
      Unix.close stdout_wr;
      let buf = Bytes.create buffer_size in
      let count = Unix.read stdout_rd ~buf in
      Unix.close stdout_rd;
      on_result buf count
  ;;

  let entries_and_buffer_size (type a) (pick_from : a Pick_from.t) ~buffer_size =
    let large_enough_for_a_reasonable_string_selectable_by_a_human =
      Byte_units.(bytes_int_exn (of_kilobytes 16.))
    in
    match pick_from with
    | Command_output command ->
      Some
        ( `Command_output command
        , large_enough_for_a_reasonable_string_selectable_by_a_human )
    | Streaming (T ((_ : _ Io.t), pipe)) ->
      Some (`Streaming pipe, large_enough_for_a_reasonable_string_selectable_by_a_human)
    | pick_from ->
      let pick_from = Pick_from.Encoded.create pick_from in
      let%map.Option entries =
        Nonempty_list.of_list (Pick_from.Encoded.to_list pick_from)
      in
      `List entries, buffer_size entries
  ;;

  let pick_one_with_pid_ivar
        (type a)
        ?select1
        ?query
        ?header
        ?preview
        ?preview_window
        ?no_sort
        ?reverse_input
        ?prompt_at_top
        ?with_nth
        ?nth
        ?delimiter
        ?height
        ?bind
        ?tiebreak
        ?filter
        ?border
        ?info
        ?exact_match
        ?no_hscroll
        ~pid_ivar
        (pick_from : a Pick_from.t)
    : a option
    =
    let result =
      let%bind.Option entries, buffer_size =
        entries_and_buffer_size pick_from ~buffer_size:(fun entries ->
          Nonempty_list.map (entries :> string Nonempty_list.t) ~f:String.length
          |> Nonempty_list.reduce ~f:Int.max
          |> succ
          (* +1 for trailing newline *))
      in
      pick
        ?select1
        ?query
        ?header
        ?preview
        ?preview_window
        ?no_sort
        ?reverse_input
        ?prompt_at_top
        ?with_nth
        ?nth
        ?delimiter
        ?height
        ?bind
        ?tiebreak
        ?filter
        ?border
        ?info
        ?exact_match
        ?no_hscroll
        entries
        ~buffer_size
        ~on_result:(fun buf count ->
          if count = 0
          then None
          else (
            (* leave off trailing newline *)
            let output = Bytes.To_string.subo buf ~len:(count - 1) in
            Some (Io.Output.of_string output)))
        ~pid_ivar
    in
    let pick_from = Pick_from.Encoded.create pick_from in
    Option.map result ~f:(fun key -> Pick_from.Encoded.lookup_selection pick_from key)
  ;;

  let pick_one = pick_one_with_pid_ivar ~pid_ivar:None

  let pick_many_with_pid_ivar
        (type a)
        ?select1
        ?query
        ?header
        ?preview
        ?preview_window
        ?no_sort
        ?reverse_input
        ?prompt_at_top
        ?with_nth
        ?nth
        ?delimiter
        ?height
        ?bind
        ?tiebreak
        ?filter
        ?border
        ?info
        ?exact_match
        ?no_hscroll
        ~pid_ivar
        (pick_from : a Pick_from.t)
    : a list option
    =
    let result =
      let%bind.Option entries, buffer_size =
        entries_and_buffer_size pick_from ~buffer_size:(fun entries ->
          let each_entry_has_a_trailing_newline = 1 in
          (entries :> string Nonempty_list.t)
          |> Nonempty_list.map ~f:(fun entry ->
            String.length entry + each_entry_has_a_trailing_newline)
          |> Nonempty_list.reduce ~f:Int.( + ))
      in
      pick
        ?select1
        ?query
        ?header
        ?preview
        ?preview_window
        ?no_sort
        ?reverse_input
        ?prompt_at_top
        ?with_nth
        ?nth
        ?delimiter
        ?height
        ?bind
        ?tiebreak
        ?filter
        ?border
        ?info
        ?exact_match
        ?no_hscroll
        entries
        ~buffer_size
        ~select_many:()
        ~on_result:(fun buf count ->
          if count = 0
          then None
          else
            (* leave off trailing newline *)
            Bytes.To_string.subo buf ~len:(count - 1)
            |> String.split ~on:'\n'
            |> List.map ~f:Io.Output.of_string
            |> Option.some)
        ~pid_ivar
    in
    let pick_from = Pick_from.Encoded.create pick_from in
    Option.map result ~f:(fun keys ->
      List.map keys ~f:(fun key -> Pick_from.Encoded.lookup_selection pick_from key))
  ;;

  let pick_many = pick_many_with_pid_ivar ~pid_ivar:None
end

open Async

let with_abort ~abort ~f =
  let pid_ivar = Ivar.create () in
  let abort_deferred =
    let%map (), pid = Deferred.both abort (Ivar.read pid_ivar) in
    pid
  in
  let abort_choice =
    choice abort_deferred (fun pid ->
      Signal_unix.send_i Signal.int (`Pid pid);
      Or_error.return (Second `Aborted))
  in
  let fzf_deferred =
    let%map.Deferred.Or_error x = f ~pid_ivar:(Some pid_ivar) in
    First x
  in
  let fzf_choice = choice fzf_deferred Fn.id in
  let%bind result = choose [ abort_choice; fzf_choice ] in
  (* Wait on the fzf_deferred so when the returned deferred is determined we know the
     child fzf process has already been reaped. *)
  let%bind.Deferred (_ : _ Either.t Or_error.t) = fzf_deferred in
  return result
;;

let pick_one_with_pid_ivar
      (type a)
      ?select1
      ?query
      ?header
      ?preview
      ?preview_window
      ?no_sort
      ?reverse_input
      ?prompt_at_top
      ?with_nth
      ?nth
      ?delimiter
      ?height
      ?bind
      ?tiebreak
      ?filter
      ?border
      ?info
      ?exact_match
      ?no_hscroll
      ~pid_ivar
      (entries : a Pick_from.t)
  : a option Deferred.Or_error.t
  =
  Deferred.Or_error.try_with
    ~run:`Schedule (* consider [~run:`Now] instead; see: https://wiki/x/ByVWF *)
    ~rest:`Log
    (* consider [`Raise] instead; see: https://wiki/x/Ux4xF *)
    (fun () ->
       In_thread.run (fun () ->
         Blocking.pick_one_with_pid_ivar
           ?select1
           ?query
           ?header
           ?preview
           ?preview_window
           ?no_sort
           ?reverse_input
           ?prompt_at_top
           ?with_nth
           ?nth
           ?delimiter
           ?height
           ?bind
           ?tiebreak
           ?filter
           ?border
           ?info
           ?exact_match
           ?no_hscroll
           ~pid_ivar
           entries))
;;

let pick_one = pick_one_with_pid_ivar ~pid_ivar:None

let pick_one_abort
      (type a)
      ~abort
      ?select1
      ?query
      ?header
      ?preview
      ?preview_window
      ?no_sort
      ?reverse_input
      ?prompt_at_top
      ?with_nth
      ?nth
      ?delimiter
      ?height
      ?bind
      ?tiebreak
      ?filter
      ?border
      ?info
      ?exact_match
      ?no_hscroll
      (entries : a Pick_from.t)
  : (a option, [ `Aborted ]) Either.t Deferred.Or_error.t
  =
  with_abort ~abort ~f:(fun ~pid_ivar ->
    pick_one_with_pid_ivar
      ?select1
      ?query
      ?header
      ?preview
      ?preview_window
      ?no_sort
      ?reverse_input
      ?prompt_at_top
      ?with_nth
      ?nth
      ?delimiter
      ?height
      ?bind
      ?tiebreak
      ?filter
      ?border
      ?info
      ?exact_match
      ?no_hscroll
      ~pid_ivar
      entries)
;;

let pick_many_with_pid_ivar
      (type a)
      ?select1
      ?query
      ?header
      ?preview
      ?preview_window
      ?no_sort
      ?reverse_input
      ?prompt_at_top
      ?with_nth
      ?nth
      ?delimiter
      ?height
      ?bind
      ?tiebreak
      ?filter
      ?border
      ?info
      ?exact_match
      ?no_hscroll
      ~pid_ivar
      (entries : a Pick_from.t)
  : a list option Deferred.Or_error.t
  =
  Deferred.Or_error.try_with
    ~run:`Schedule (* consider [~run:`Now] instead; see: https://wiki/x/ByVWF *)
    ~rest:`Log
    (* consider [`Raise] instead; see: https://wiki/x/Ux4xF *)
    (fun () ->
       In_thread.run (fun () ->
         Blocking.pick_many_with_pid_ivar
           ?select1
           ?query
           ?header
           ?preview
           ?preview_window
           ?no_sort
           ?reverse_input
           ?prompt_at_top
           ?with_nth
           ?nth
           ?delimiter
           ?height
           ?bind
           ?tiebreak
           ?filter
           ?border
           ?info
           ?exact_match
           ?no_hscroll
           ~pid_ivar
           entries))
;;

let pick_many = pick_many_with_pid_ivar ~pid_ivar:None

let pick_many_abort
      (type a)
      ~abort
      ?select1
      ?query
      ?header
      ?preview
      ?preview_window
      ?no_sort
      ?reverse_input
      ?prompt_at_top
      ?with_nth
      ?nth
      ?delimiter
      ?height
      ?bind
      ?tiebreak
      ?filter
      ?border
      ?info
      ?exact_match
      ?no_hscroll
      (entries : a Pick_from.t)
  : (a list option, [ `Aborted ]) Either.t Deferred.Or_error.t
  =
  with_abort ~abort ~f:(fun ~pid_ivar ->
    pick_many_with_pid_ivar
      ?select1
      ?query
      ?header
      ?preview
      ?preview_window
      ?no_sort
      ?reverse_input
      ?prompt_at_top
      ?with_nth
      ?nth
      ?delimiter
      ?height
      ?bind
      ?tiebreak
      ?filter
      ?border
      ?info
      ?exact_match
      ?no_hscroll
      ~pid_ivar
      entries)
;;

let complete_subcommands ~show_help ~path ~part subcommands =
  let preview =
    if show_help
    then (
      (* Always use /proc/self/exe, since the first element in [path] might not resolve to
         the executable. *)
      let exe = Core_unix.readlink "/proc/self/exe" in
      let name, args =
        match path with
        | name :: args -> name, String.concat ~sep:" " args
        | [] -> failwith "complete_subcommands: Unexpected empty list for path"
      in
      let command_prefix = [%string "exec -a %{name} %{exe} %{args}"] in
      Some (sprintf "eval '%s '{}' -help'" command_prefix))
    else None
  in
  let prompt_at_top =
    (* If help is being displayed, it will appear at the top, so also move the prompt up
       there.  That way eyes can more easily bounce between the current selection and the
       help. *)
    Option.some_if (Option.is_some preview) ()
  in
  Blocking.pick_one
    (Inputs (List.map ~f:(String.concat ~sep:" ") subcommands))
    ~query:part
    ?preview
    ?prompt_at_top
  |> Option.map ~f:List.return
;;

let complete ~choices (univ_map : Univ_map.t) ~part =
  Blocking.pick_one ~query:part (Inputs (choices univ_map)) |> Option.to_list
;;

let complete_enumerable (module E : Command.Enumerable_stringable) =
  let choices (_ : Univ_map.t) = List.map E.all ~f:E.to_string in
  complete ~choices
;;

let complete_enumerable_sexpable (module E : Command.Enumerable_sexpable) =
  let choices (_ : Univ_map.t) =
    List.map E.all ~f:(fun t -> Sexp.to_string [%sexp (t : E.t)])
  in
  complete ~choices
;;