Files
Synapse/cli/lib/completion.ml
Dasemu 40631362b3 Initial import of LogDarioSeq baseline
Establishes version control for the personal Logseq DB fork before
starting the server-client architecture migration and domain features
described in docs/agent-guide and the approved implementation plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 15:32:15 +02:00

518 lines
16 KiB
OCaml

type opts = { shell : Cli_primitive.shell option }
type parsed = Parsed_completion of opts
type action =
| Completion of {
shell : Cli_primitive.shell;
registry : Command_registry.t option;
}
let join_words words = Vec.string_concat " " words
let unique_preserve values =
let seen = Hashtbl.create 16 in
values
|> Vec.filter (fun value ->
if Hashtbl.mem seen value then false
else (
Hashtbl.add seen value ();
true))
let top_command_names registry =
registry.Command_registry.commands
|> Vec.filter_map (fun command -> Vec.nth_opt command.Command_registry.path 0)
|> Vec.sort_uniq String.compare
let is_prefix prefix path =
let rec loop index =
index = Vec.length prefix
|| (Vec.nth prefix index = Vec.nth path index && loop (index + 1))
in
Vec.length prefix <= Vec.length path && loop 0
let commands_under registry prefix =
registry.Command_registry.commands
|> Vec.filter (fun command ->
is_prefix prefix command.Command_registry.path
&& Vec.length command.Command_registry.path > Vec.length prefix)
let direct_children registry prefix =
let prefix_len = Vec.length prefix in
commands_under registry prefix
|> Vec.filter_map (fun command ->
Vec.nth_opt command.Command_registry.path prefix_len)
|> unique_preserve
let direct_leaf_children registry prefix =
let prefix_len = Vec.length prefix in
commands_under registry prefix
|> Vec.filter (fun command ->
Vec.length command.Command_registry.path = prefix_len + 1)
let nested_child_groups registry prefix =
let prefix_len = Vec.length prefix in
commands_under registry prefix
|> Vec.filter_map (fun command ->
if Vec.length command.Command_registry.path > prefix_len + 1 then
Vec.nth_opt command.Command_registry.path prefix_len
else None)
|> unique_preserve
let is_group registry path = not (Vec.is_empty (commands_under registry path))
let global_option_names =
Vec.of_array
[|
"--help";
"-h";
"--version";
"--config";
"--graph";
"-g";
"--root-dir";
"--timeout-ms";
"--output";
"-o";
"--verbose";
"-v";
"--profile";
|]
let global_value_option_names =
Vec.of_array
[|
"--config";
"--graph";
"-g";
"--root-dir";
"--timeout-ms";
"--output";
"-o";
|]
let command_desc_for_path registry path =
Command_registry.find_by_path path registry
|> Option.map (fun (command : Command_registry.command_meta) -> command.doc)
|> Option.value ~default:""
let zsh_quote_entry name desc = "'" ^ name ^ ":" ^ desc ^ "'"
let zsh_escape_desc doc =
let buffer = Buffer.create (String.length doc) in
String.iter
(fun c ->
match c with
| '\'' -> Buffer.add_string buffer "'\\''"
| '[' -> Buffer.add_string buffer "\\["
| ']' -> Buffer.add_string buffer "\\]"
| c -> Buffer.add_char buffer c)
doc;
Buffer.contents buffer
let zsh_option_action (option : Command_registry.option_meta) value_name =
if Vec.mem "--graph" option.names then "{_logseq_graphs}"
else if option.multi && not (Vec.is_empty option.choices) then
"{_values -s , " ^ value_name ^ " " ^ Vec.string_concat " " option.choices
^ "}"
else if not (Vec.is_empty option.choices) then
"(" ^ Vec.string_concat " " option.choices ^ ")"
else if Vec.mem "--root-dir" option.names then "_files -/"
else if value_name = "path" then "_files"
else if value_name = "bool" then "(true false)"
else ""
let zsh_option_spec (option : Command_registry.option_meta) =
let desc = "[" ^ zsh_escape_desc option.doc ^ "]" in
let value_spec =
match option.arity with
| Command_registry.Flag -> None
| Required_value name ->
Some (":" ^ name ^ ":" ^ zsh_option_action option name)
| Optional_value name ->
Some ("::" ^ name ^ ":" ^ zsh_option_action option name)
in
let name_suffix = if Option.is_some value_spec then "=" else "" in
let arg = Option.value value_spec ~default:"" in
let star = if option.repeatable then "*" else "" in
if Vec.length option.names = 1 then
"'" ^ star ^ Vec.nth option.names 0 ^ name_suffix ^ desc ^ arg ^ "'"
else
let exclusion =
if option.repeatable then "'" ^ star ^ "'"
else "'(" ^ Vec.string_concat " " option.names ^ ")'"
in
let alternatives =
option.names
|> Vec.map (fun name -> name ^ name_suffix)
|> Vec.string_concat ","
in
exclusion ^ "{" ^ alternatives ^ "}'" ^ desc ^ arg ^ "'"
let global_option_named name =
Command_registry.global_options
|> Vec.find_opt (fun (option : Command_registry.option_meta) ->
Vec.mem name option.names)
let zsh_global_specs names =
names |> Vec.filter_map global_option_named |> Vec.map zsh_option_spec
let zsh_spec_lines specs = " " ^ Vec.string_concat " \\\n " specs
let zsh_leaf_function command =
let func_name = "_logseq_" ^ join_words command.Command_registry.path in
let func_name = String.map (function ' ' -> '_' | c -> c) func_name in
let specs =
Vec.append
(zsh_global_specs (Vec.of_array [| "--help"; "--graph"; "--output" |]))
(Vec.map zsh_option_spec command.Command_registry.options)
in
String.concat "\n"
[ func_name ^ "() {"; " _arguments \\"; zsh_spec_lines specs; "}"; "" ]
let zsh_group_specs registry path =
let own_option_specs =
match Command_registry.find_by_path path registry with
| Some command -> Vec.map zsh_option_spec command.Command_registry.options
| None -> Vec.empty
in
Vec.append
(Vec.append
(zsh_global_specs (Vec.of_array [| "--help"; "--graph" |]))
own_option_specs)
(Vec.of_array [| "'(-)1:subcommand:->subcmd'"; "'(-)*::args:->args'" |])
let zsh_subgroup_function registry parent subgroup =
let prefix = Vec.push_back parent subgroup in
let child_entries =
direct_leaf_children registry prefix
|> Vec.map (fun command ->
" "
^ zsh_quote_entry
(Vec.nth command.Command_registry.path (Vec.length prefix))
command.Command_registry.doc)
|> Vec.string_concat "\n"
in
let dispatches =
direct_leaf_children registry prefix
|> Vec.map (fun command ->
let leaf = Vec.nth command.Command_registry.path (Vec.length prefix) in
" " ^ leaf ^ ") _logseq_"
^ Vec.string_concat "_" command.Command_registry.path
^ " ;;")
|> Vec.string_concat "\n"
in
String.concat "\n"
[
"_logseq_" ^ Vec.string_concat "_" prefix ^ "() {";
" local curcontext=\"$curcontext\" state line";
" typeset -A opt_args";
" _arguments -C \\";
zsh_spec_lines (zsh_group_specs registry prefix);
" case $state in";
" subcmd)";
" local -a subcmds";
" subcmds=(";
child_entries;
" )";
" _describe 'subcommand' subcmds";
" ;;";
" args)";
" case $line[1] in";
dispatches;
" esac";
" ;;";
" esac";
"}";
"";
]
let zsh_group_function registry group =
let prefix = Vec.singleton group in
let leaf_entries =
direct_leaf_children registry prefix
|> Vec.map (fun command ->
" "
^ zsh_quote_entry
(Vec.nth command.Command_registry.path 1)
command.Command_registry.doc)
in
let subgroup_entries =
nested_child_groups registry prefix
|> Vec.map (fun name ->
" " ^ zsh_quote_entry name (name ^ " commands"))
in
let dispatches =
direct_leaf_children registry prefix
|> Vec.map (fun command ->
let subcmd = Vec.nth command.Command_registry.path 1 in
" " ^ subcmd ^ ") _logseq_"
^ Vec.string_concat "_" command.Command_registry.path
^ " ;;")
|> fun leaf_dispatches ->
Vec.append leaf_dispatches
(nested_child_groups registry prefix
|> Vec.map (fun name ->
" " ^ name ^ ") _logseq_" ^ group ^ "_" ^ name ^ " ;;"))
in
let subgroup_functions =
nested_child_groups registry prefix
|> Vec.map (zsh_subgroup_function registry prefix)
|> Vec.string_concat "\n"
in
subgroup_functions
^ String.concat "\n"
[
"_logseq_" ^ group ^ "() {";
" local curcontext=\"$curcontext\" state line";
" typeset -A opt_args";
" _arguments -C \\";
zsh_spec_lines (zsh_group_specs registry prefix);
" case $state in";
" subcmd)";
" local -a subcmds";
" subcmds=(";
Vec.string_concat "\n" (Vec.append leaf_entries subgroup_entries);
" )";
" _describe 'subcommand' subcmds";
" ;;";
" args)";
" case $line[1] in";
Vec.string_concat "\n" dispatches;
" esac";
" ;;";
" esac";
"}";
"";
]
let generate_zsh_completion registry =
let top_entries =
top_command_names registry
|> Vec.map (fun top ->
let desc =
let path = Vec.singleton top in
if is_group registry path then top ^ " commands"
else command_desc_for_path registry path
in
" " ^ zsh_quote_entry top desc)
|> Vec.string_concat "\n"
in
let dispatches =
top_command_names registry
|> Vec.map (fun top -> " " ^ top ^ ") _logseq_" ^ top ^ " ;;")
|> Vec.string_concat "\n"
in
let leaf_functions =
registry.Command_registry.commands
|> Vec.filter (fun command ->
(* commands that are also groups (e.g. `query`) are completed by
their group dispatcher, which includes their options *)
not (is_group registry command.Command_registry.path))
|> Vec.map zsh_leaf_function |> Vec.string_concat "\n"
in
let group_functions =
top_command_names registry
|> Vec.filter (fun top -> is_group registry (Vec.singleton top))
|> Vec.map (zsh_group_function registry)
|> Vec.string_concat "\n"
in
String.concat "\n"
[
"#compdef logseq";
"# Auto-generated by `logseq completion zsh` - do not edit manually.";
"";
"_logseq_graphs() {";
" local -a graphs";
" graphs=( ${(f)\"$(logseq graph list --output json 2>/dev/null | node \
-e 'const fs = require(\"fs\"); const input = fs.readFileSync(0, \
\"utf8\"); if (!input.trim()) process.exit(0); const data = \
JSON.parse(input); const graphs = data && data.data && \
data.data.graphs; if (Array.isArray(graphs)) \
console.log(graphs.join(\"\\n\"));')\"} )";
" compadd -a graphs";
"}";
"";
"# --- per-command functions ---";
"";
leaf_functions;
"# --- group dispatchers ---";
"";
group_functions;
"# --- top-level dispatcher ---";
"";
"_logseq() {";
" local curcontext=\"$curcontext\" state line";
" typeset -A opt_args";
" _arguments -C \\";
zsh_spec_lines
(Vec.append
(Vec.map zsh_option_spec Command_registry.global_options)
(Vec.of_array
[| "'(-)1:command:->cmds'"; "'(-)*::args:->args'" |]));
" case $state in";
" cmds)";
" local -a cmds";
" cmds=(";
top_entries;
" )";
" _describe 'command' cmds";
" ;;";
" args)";
" case $line[1] in";
dispatches;
" esac";
" ;;";
" esac";
"}";
"";
"compdef _logseq logseq";
]
let bash_case_pattern names = Vec.string_concat "|" names
let bash_subcommand_cases registry =
top_command_names registry
|> Vec.filter_map (fun top ->
let children = direct_children registry (Vec.singleton top) in
if Vec.is_empty children then None
else
Some
(" " ^ top ^ ") COMPREPLY=( $(compgen -W '"
^ Vec.string_concat " " children
^ "' -- \"$cur\") ) ;;"))
|> Vec.string_concat "\n"
let bash_sub_subcommand_cases registry =
top_command_names registry
|> Vec.concat_map (fun top ->
nested_child_groups registry (Vec.singleton top)
|> Vec.map (fun subgroup ->
let leaves =
direct_children registry (Vec.of_array [| top; subgroup |])
in
" " ^ top ^ ":" ^ subgroup ^ ") COMPREPLY=( $(compgen -W '"
^ Vec.string_concat " " leaves
^ "' -- \"$cur\") ) ;;"))
|> Vec.string_concat "\n"
let generate_bash_completion registry =
let top_commands = Vec.string_concat " " (top_command_names registry) in
String.concat "\n"
[
"# Auto-generated by `logseq completion bash` - do not edit manually.";
"";
"_logseq_is_value_opt() {";
" case \"$1\" in";
" " ^ bash_case_pattern global_value_option_names ^ ")";
" return 0 ;;";
" *) return 1 ;;";
" esac";
"}";
"";
"_logseq_cmd_and_subcmd() {";
" local i skip=0";
" __cmd='' __subcmd='' __subsubcmd=''";
" for (( i = 1; i < COMP_CWORD; i++ )); do";
" local w=\"${COMP_WORDS[i]}\"";
" if (( skip )); then skip=0; continue; fi";
" if [[ \"$w\" == -* ]]; then";
" _logseq_is_value_opt \"$w\" && skip=1";
" continue";
" fi";
" if [[ -z \"$__cmd\" ]]; then __cmd=\"$w\"";
" elif [[ -z \"$__subcmd\" ]]; then __subcmd=\"$w\"";
" elif [[ -z \"$__subsubcmd\" ]]; then __subsubcmd=\"$w\"";
" fi";
" done";
"}";
"";
"_logseq_json_names_bash() {";
" logseq graph list --output json 2>/dev/null | node -e 'const fs = \
require(\"fs\"); const input = fs.readFileSync(0, \"utf8\"); if \
(!input.trim()) process.exit(0); const data = JSON.parse(input); const \
graphs = data && data.data && data.data.graphs; if \
(Array.isArray(graphs)) console.log(graphs.join(\" \"));'";
"}";
"";
"_logseq_opts_for() {";
" printf '%s' \"" ^ Vec.string_concat " " global_option_names ^ "\"";
"}";
"";
"_logseq() {";
" local cur prev";
" cur=\"${COMP_WORDS[COMP_CWORD]}\"";
" prev=\"${COMP_WORDS[COMP_CWORD-1]}\"";
" COMPREPLY=()";
" local __cmd __subcmd __subsubcmd";
" _logseq_cmd_and_subcmd";
" if [[ \"$cur\" == -* ]]; then";
" COMPREPLY=( $(compgen -W \"$(_logseq_opts_for \"$__cmd\" \
\"$__subcmd\" \"$__subsubcmd\")\" -- \"$cur\") )";
" return";
" fi";
" if [[ -z \"$__cmd\" ]]; then";
" COMPREPLY=( $(compgen -W '" ^ top_commands ^ "' -- \"$cur\") )";
" return";
" fi";
" if [[ -z \"$__subcmd\" ]]; then";
" case \"$__cmd\" in";
bash_subcommand_cases registry;
" esac";
" return";
" fi";
" if [[ -z \"$__subsubcmd\" ]]; then";
" case \"$__cmd:$__subcmd\" in";
bash_sub_subcommand_cases registry;
" esac";
" return";
" fi";
"}";
"";
"complete -F _logseq logseq";
]
let generate shell registry =
match shell with
| Cli_primitive.Zsh -> generate_zsh_completion registry
| Bash -> generate_bash_completion registry
let command_id _ = Command_id.Completion
let validate_parsed _ = Ok ()
let build ?registry _config _ = function
| Parsed_completion { shell = Some shell } ->
Ok (Completion { shell; registry })
| Parsed_completion { shell = None } ->
Error
(Error.invalid_options
"completion shell is required; expected zsh or bash")
let execute_with_mode (Completion { shell; registry }) _config mode =
Cli_effect.pure
(Cli_result.ok ~command:Command_id.Completion mode
(Message
(generate shell
(Option.value registry ~default:Command_registry.empty))))
let metadata () =
Vec.of_array
[|
{
Command_registry.id = Command_id.Completion;
path = Command_id.to_path Command_id.Completion;
doc = "Generate shell completion script";
long_doc = None;
examples = Vec.empty;
options = Vec.empty;
category = Command_registry.Utilities;
requires_graph = Command_id.requires_graph Command_id.Completion;
requires_auth = Command_id.requires_auth Command_id.Completion;
write_command = Command_id.is_write Command_id.Completion;
human_table_headers_order = Vec.empty;
};
|]
let execute action config =
let (Output.Mode.Packed mode) = Output_mode.for_config config in
execute_with_mode action config mode