aboutsummaryrefslogtreecommitdiffstats
open Core
open Option.Let_syntax

let join_str_list ~sep = function
  | [] -> ""
  | s :: ss -> List.fold ss ~init:s ~f:(fun acc s -> acc ^ sep ^ s)

type cmd = {
  args : string;
  opts : unit -> string list;
  next : (string -> (string, cmd) Either.t) option;
  call : string list -> bool;
}

let set_opt_cmd opt setting =
  {
    args = "<option value>";
    opts = (fun () -> Settings.allowed_values setting);
    next = None;
    call =
      (fun args ->
        match Settings.set_to setting args with
        | None -> true
        | Some msg ->
            printf "Failed to set option %s: %s\n%!" opt msg;
            false);
  }

let set_cmd =
  {
    args = "<option name> <option value>";
    opts = (fun () -> Map.keys Settings.settings);
    next =
      Some
        (fun opt ->
          match Map.find Settings.settings opt with
          | Some setting -> Second (set_opt_cmd opt setting)
          | None -> First (sprintf "Unknown option '%s'" opt));
    call =
      (fun _ ->
        printf "Missing option argument value\n%!";
        false);
  }

let settings_cmd =
  {
    args = "";
    opts = (fun () -> []);
    next = None;
    call =
      (function
      | [] ->
          Settings.print ();
          true
      | _ ->
          printf "Expected no arguments\n%!";
          false);
  }

let run_cmd =
  {
    args = "<filename>";
    opts = (fun () -> []);
    next = None;
    call =
      (function
      | [ filename ] -> Run.eval_file filename
      | _ ->
          printf "Expected one argument (the filename)\n%!";
          false);
  }

let quit_cmd =
  { args = ""; opts = (fun () -> []); next = None; call = (fun _ -> exit 0) }

let commands =
  Map.of_alist_exn
    (module String)
    [
      ("quit", quit_cmd);
      ("set", set_cmd);
      ("settings", settings_cmd);
      ("run", run_cmd);
    ]

let root_cmd =
  {
    args = "<command>";
    opts = (fun () -> Map.keys commands);
    next =
      Some
        (fun cmd_name ->
          match Map.find commands cmd_name with
          | Some cmd -> Second cmd
          | None ->
              First
                (sprintf "Unknown command '%s' (expected one of {%s})" cmd_name
                   (Map.keys commands |> join_str_list ~sep:", ")));
    call =
      (fun _ ->
        printf "Missing command!\n%!";
        false);
  }

let is_space = Char.( = ) ' '
let strip_space = String.strip ~drop:is_space
let lstrip_space = String.lstrip ~drop:is_space

let clean_str_list ss =
  ss |> List.map ~f:strip_space
  |> List.filter ~f:(fun s -> not (String.is_empty s))

let words s = s |> String.split ~on:' ' |> clean_str_list
let unwords ss = clean_str_list ss |> join_str_list ~sep:" "

let rec call cmd args =
  match args with
  | [] -> cmd.call []
  | arg0 :: argn -> (
      match cmd.next with
      | None -> cmd.call args
      | Some next -> (
          match next arg0 with
          | First msg ->
              printf "%s\n%!" msg;
              false
          | Second cmd' -> call cmd' argn))

let try_lsplit2_space s =
  match String.lsplit2 s ~on:' ' with Some (l, r) -> (l, r) | None -> (s, "")

let lsplit2_space' s =
  let%bind l, r = String.lsplit2 s ~on:' ' in
  (* s = l ^ " " ^ r *)
  let r' = lstrip_space r in
  let space = " " ^ String.chop_suffix_exn r ~suffix:r' in
  (* s = l ^ space ^ r' *)
  return (l, space, r')

let rec completions cmd args =
  (* cmd|<TAB> -> options
     cmd|abc<TAB> -> options with prefix 'abc'
     cmd|abc .*<TAB> -> subcommand 'abc' options, pass .* *)
  if String.(args = "") then cmd.opts ()
  else
    match lsplit2_space' args with
    | None -> cmd.opts () |> List.filter ~f:(String.is_prefix ~prefix:args)
    | Some (arg0, space, argn) -> (
        match cmd.next with
        | None -> []
        | Some next -> (
            match next arg0 with
            | First _ -> []
            | Second cmd' ->
                completions cmd' argn
                |> List.map ~f:(String.append (arg0 ^ space))))

let rec hints cmd args =
  (* cmd: "" -> " <args>"
     cmd: "<space>+" -> "<args>"
     cmd: "<space>+<subcmd>" -> "<hints for subcmd>"
     cmd: "<space>+<subcmd> .*" -> "<hints for subcmd with .*>" *)
  if String.(args = "") then Some (" " ^ cmd.args)
  else if String.(strip_space args = "") then Some cmd.args
  else
    let args = lstrip_space args in
    let%bind next = cmd.next in
    match lsplit2_space' args with
    | None ->
        let%bind cmd' = next args |> Either.Second.to_option in
        hints cmd' ""
    | Some (arg0, space, argn) ->
        let%bind cmd' = next arg0 |> Either.Second.to_option in
        hints cmd' (space ^ argn)

let invoke cmd = call root_cmd (words cmd)
let complete cmd = completions root_cmd cmd
let hint cmd = hints root_cmd cmd