From b11477d1a86eae7aaaf7eca45d2000cbeea775a9 Mon Sep 17 00:00:00 2001 From: Kento Okura Date: Sun, 2 Aug 2026 18:27:53 +0200 Subject: [PATCH] Completely rewrite completion context analysis The idea is to scan the token stream, pushing and popping delimiters. The complicated aspects arise from the syntax: - In order to correctly classify links like [foo](bar), we need to remember the previously closed group. - Idents like \route-asset, \subtree, \date, \transclude have their own completion behavior and need to be recognized. Context analysis returns a list (or stack) of so-called frames. For example: "\p{#{" -> [{delim = Math_inline; purpose = Math}; {delim = Brace; purpose = Body}] The purpose is for disambiguating, for example, the cases \asset{ and \transclude{. They both use the same delimiter, but their completion behavior is different. The completion handler can use this information to easily tell which completions should be provided and, more importantly, this is extensible by adding more rules. For now the rules just check the top frame, meaning the most recently opened delimiter. I will need to use this in practice to see if this is the most convenient behavior. --- lib/compiler/Expand.ml | 8 +- lib/compiler/Expand.mli | 3 + lib/language_server/Analysis.ml | 107 +-------- lib/language_server/Completion.ml | 264 +++++++--------------- lib/language_server/Completion_context.ml | 160 +++++++++++++ lib/language_server/dune | 1 + test/Testables.ml | 1 - test/lsp.t | 4 +- 8 files changed, 261 insertions(+), 287 deletions(-) create mode 100644 lib/language_server/Completion_context.ml diff --git a/lib/compiler/Expand.ml b/lib/compiler/Expand.ml index fa372c0..e0cb279 100644 --- a/lib/compiler/Expand.ml +++ b/lib/compiler/Expand.ml @@ -302,13 +302,13 @@ let builtin_xml_namespaces = |> Seq.map @@ fun (prefix, xmlns) -> (["xmlns"; prefix], (Syn.Xmlns {prefix; xmlns}, None)) -let builtins = +let math_builtins = Seq.append Tex_builtins.words Tex_builtins.symbols + +let prose_builtins = Seq.concat @@ List.to_seq [ builtin_xml_namespaces; - Tex_builtins.words; - Tex_builtins.symbols; begin let open Builtins.Transclude in List.to_seq @@ -382,6 +382,8 @@ let builtins = end; ] +let builtins = Seq.append math_builtins prose_builtins + let initial_visible_trie : (Syn.resolver_data, Range.t option) Trie.t = Yuujinchou.Trie.of_seq builtins diff --git a/lib/compiler/Expand.mli b/lib/compiler/Expand.mli index 50db06f..eeeb146 100644 --- a/lib/compiler/Expand.mli +++ b/lib/compiler/Expand.mli @@ -9,6 +9,9 @@ module Unit_map = URI.Map val initial_visible_trie : (Syn.resolver_data, Range.t option) Trie.t +val math_builtins : (Trie.path * (Syn.resolver_data * Range.t option)) Seq.t +val prose_builtins : (Trie.path * (Syn.resolver_data * Range.t option)) Seq.t + module Builtins : sig module Transclude : sig val expanded_sym : Symbol.t diff --git a/lib/language_server/Analysis.ml b/lib/language_server/Analysis.ml index 75aac69..9c63847 100644 --- a/lib/language_server/Analysis.ml +++ b/lib/language_server/Analysis.ml @@ -8,6 +8,8 @@ open Forester_compiler open Forester_core +(* I think this module is mostly junk? *) + module Item = struct type t = Path of Trie.path | Addr of string @@ -108,97 +110,12 @@ let rec node_at : type a. | None -> Some n end -let get_enclosing_code_group ~position tree = - let rec go ~position nodes = - match - List.find_opt (fun Range.{range; _} -> contains ~position range) nodes - with - | None -> None - | Some ({value; range} as node) -> ( - match value with - | Code.Group (delim, t) -> begin - match go ~position t with - | None -> Some Range.{value = (delim, t); range} - | Some t -> Some t - end - | _ -> (go ~position) (Code.children node)) - in - match Tree.code tree with - | None -> None - | Some code -> go ~position @@ Tree.Parsed.nodes code - -let get_enclosing_syn_group ~position tree = - let rec go ~position nodes = - match - List.find_opt (fun Range.{range; _} -> contains ~position range) nodes - with - | None -> None - | Some ({value; range} as node) -> ( - match value with - | Syn.Group (delim, children) -> begin - match go ~position children with - | None -> Some Range.{value = (delim, children); range} - | Some t -> Some t - end - | _ -> go ~position (Syn.children node)) - in - match Tree.syn tree with - | None -> None - | Some syn -> go ~position syn.tree.nodes - -let enclosing_group_start ~position - ~(enclosing_group : - position:L.Position.t -> Tree.t -> (delim * 'a) Range.located option) - (tree : Tree.t) = - match enclosing_group ~position tree with - | None -> position - | Some {range; value = _} -> Lsp_shims.lsp_pos_of_range range - -let find_with_prev ~position = - let rec go prev = function - | [] -> None - | x :: xs -> - if contains ~position Range.(x.range) then Some (prev, x) - else go (Some x) xs - in - go None - -module Context = struct - (* Kind of like a zipper where you can only go backwards? *) - type 'a t = Prev of 'a * 'a | Parent of 'a | Top of 'a option -end - -let parent_or_prev_at : type a. - position:L.Position.t -> - children:(a Range.located -> a Range.located list) -> - a Range.located list -> - a Range.located Context.t option = - fun ~position ~children code -> - let rec go ~top nodes = - match find_with_prev ~position nodes with - | None -> if top then Some (Context.Top None) else None - | Some (prev, node) -> ( - match go ~top:false (children node) with - | Some inner -> Some inner - | None -> ( - match prev with - | Some prev -> Some (Context.Prev (prev, node)) - | None -> - Some (if top then Context.Top (Some node) else Context.Parent node))) - in - go ~top:true code - -let parent_or_prev_at_code ~position = - parent_or_prev_at ~position ~children:Code.children - -let parent_or_prev_at_syn ~position = - parent_or_prev_at ~position ~children:Syn.children - let node_at_code ~position = node_at ~position ~children:Code.children let node_at_syn ~position = node_at ~position ~children:Syn.children -let get_visible ~forest ~position code = - Sc.run ~init_visible:Expand.initial_visible_trie @@ fun () -> +let get_visible ?(init_visible = Expand.initial_visible_trie) ~forest ~position + code = + Sc.run ~init_visible @@ fun () -> let open Effect.Deep in match_with (Expand.expand_eff ~forest) @@ -241,17 +158,3 @@ let word_at ~position (doc : Lsp.Text_document.t) = words; None with Found str -> Some str) - -let word_before ~position (doc : Lsp.Text_document.t) = - let L.Position.{line; character} = position in - let line = - List.nth_opt (String.split_on_char '\n' (Lsp.Text_document.text doc)) line - in - match line with - | None -> None - | Some line -> ( - try - let until_cursor = String.sub line 0 character in - let words = List.rev @@ String.split_on_char ' ' until_cursor in - Some (List.hd words) - with _ -> None) diff --git a/lib/language_server/Completion.ml b/lib/language_server/Completion.ml index 2668b50..25a4c4e 100644 --- a/lib/language_server/Completion.ml +++ b/lib/language_server/Completion.ml @@ -16,170 +16,6 @@ open struct let ( let* ) = Option.bind end -type completion = Addrs | New_addr | Assets | Visible | Date [@@deriving show] - -module S = Set.Make (struct - type t = completion - - let compare = compare -end) - -type completion_kind = { - text: string -> completion option; - code: Code.node Range.located Analysis.Context.t -> completion option; - syn: Syn.node Range.located Analysis.Context.t -> completion option; -} - -let subtree_completion : completion_kind = - let text word_before = - if Str.(string_match (regexp {|.*subtree.*|}) word_before 0) then - Some New_addr - else None - in - let code (context : _ Analysis.Context.t) = - match context with - | Prev (Range.{value = Code.Subtree (_, _); _}, _) - | Prev (_, Range.{value = Code.Subtree (_, _); _}) - | Parent {value = Code.Subtree (_, _); _} -> - Some New_addr - | Parent _ | Prev (_, _) | Top _ -> None - in - let syn (context : Syn.node Range.located Analysis.Context.t) = - match context with - | Top (Some {value = Subtree _; _}) -> Some New_addr - | Prev (_, {value = Subtree _; _}) -> Some New_addr - | _ -> None - in - {text; code; syn} - -let asset_completion : completion_kind = - let text word_before = - if Str.(string_match (regexp {|.*route-asset.*|}) word_before 0) then - Some Assets - else None - in - let code (context : _ Analysis.Context.t) = - match context with - | Prev (Range.{value = Code.Ident ["route-asset"]; _}, _) - | Prev (_, Range.{value = Code.Ident ["route-asset"]; _}) -> - Some Assets - | Prev _ | Parent _ | Top _ -> None - in - let syn (context : Syn.node Range.located Analysis.Context.t) = - match context with - | Top (Some {value = Route_asset; _}) -> Some Assets - | Prev (_, {value = Route_asset; _}) -> Some Assets - | _ -> None - in - {text; code; syn} - -let date_completion : completion_kind = - let text word_before = - if Str.(string_match (regexp {|.*date.*|}) word_before 0) then Some Date - else None - in - let code (context : _ Analysis.Context.t) = - match context with - | Prev (Range.{value = Code.Ident ["date"]; _}, _) - | Prev (_, Range.{value = Code.Ident ["date"]; _}) -> - Some Date - | Prev _ | Parent _ | Top _ -> None - in - let syn (_context : Syn.node Range.located Analysis.Context.t) = None in - {text; code; syn} - -let uri_completion : completion_kind = - let text word_before = - if - Str.(string_match (regexp {|.*]($|}) word_before 0) - || Str.(string_match (regexp {|.*\[\[?$|}) word_before 0) - || Str.(string_match (regexp {|.*transclude.*|}) word_before 0) - then Some Addrs - else None - in - let code (_context : _ Analysis.Context.t) = None in - let syn (_context : Syn.node Range.located Analysis.Context.t) = None in - {text; code; syn} - -let new_uri_completion : completion_kind = - let text context = - if Str.(string_match (regexp {|.*subtree\[.*|}) context 0) then - Some New_addr - else None - in - let code context = - Option.map (Fun.const New_addr) (uri_completion.code context) - in - let syn context = - Option.map (Fun.const New_addr) (uri_completion.syn context) - in - {text; code; syn} - -let visible_completion : completion_kind = - let text _ = None in - let code (context : _ Analysis.Context.t) = - match context with Top _ | Parent _ -> Some Visible | Prev (_, _) -> None - in - let syn (context : Syn.node Range.located Analysis.Context.t) = - match context with Top _ | Parent _ -> Some Visible | Prev (_, _) -> None - in - {text; code; syn} - -let completion_types (t : Tree.t) ~position = - let completions : completion_kind list = - [ - uri_completion; - asset_completion; - subtree_completion; - new_uri_completion; - date_completion; - visible_completion; - ] - in - let code_opt = Tree.code t in - let syn_opt = Tree.syn t in - let doc_opt = Tree.document t in - let text_context = Option.bind doc_opt @@ Analysis.word_before ~position in - Logs.debug ~src:Log.Src.lsp (fun m -> - m "text context before cursor: %a" - Format.(pp_print_option pp_print_string) - text_context); - let code_context = - let enclosing_group = Analysis.get_enclosing_code_group in - let position = - Analysis.enclosing_group_start ~enclosing_group ~position t - in - let@ code = Option.bind code_opt in - Analysis.parent_or_prev_at_code ~position @@ Tree.Parsed.nodes code - in - let syn_context = - let enclosing_group = Analysis.get_enclosing_syn_group in - let position = - Analysis.enclosing_group_start ~enclosing_group ~position t - in - let@ syn = Option.bind syn_opt in - Analysis.parent_or_prev_at_syn ~position syn.tree.nodes - in - completions - |> List.fold_left - begin fun acc completion_kind -> - (* let module Kind = (val completion_kind : CompletionKind) in *) - let compl = - List.fold_left - begin fun acc a -> - match a with None -> acc | Some compl -> S.add compl acc - end - S.empty - [ - Option.bind text_context completion_kind.text; - Option.bind code_context completion_kind.code; - Option.bind syn_context completion_kind.syn; - ] - in - S.union compl acc - end - S.empty - let kind : Syn.node -> L.CompletionItemKind.t option = function | Fun (_, _) -> Some Function | Text _ | Verbatim _ -> Some Text @@ -251,6 +87,8 @@ let make ()) | Xmlns _ -> None +let builtin_completions builtins = List.of_seq builtins |> List.filter_map make + (* These are useful completion items that are handled during parsing, not during expansion and are thus not "builtins"*) @@ -347,18 +185,92 @@ let new_addr_completions ~(forest : State.t) : L.CompletionItem.t list = (); ] -let visible_completions ~(forest : State.t) ~(position : L.Position.t) : - Tree.parsed -> L.CompletionItem.t list = - fun nodes -> - Analysis.get_visible ~position ~forest nodes +let math_visible_trie = Trie.of_seq Expand.math_builtins +let prose_visible_trie = Trie.of_seq Expand.prose_builtins + +let visible_completions ~(forest : State.t) ~(position : L.Position.t) + (nodes : Tree.parsed) : L.CompletionItem.t list = + Analysis.get_visible ~init_visible:prose_visible_trie ~position ~forest nodes |> Trie.to_seq |> List.of_seq |> List.filter_map make |> List.append syntax_completions +let math_completions ~(forest : State.t) ~(position : L.Position.t) + (nodes : Tree.parsed) : L.CompletionItem.t list = + Analysis.get_visible ~init_visible:math_visible_trie ~position ~forest nodes + |> Trie.to_seq |> List.of_seq |> List.filter_map make + let date_completions () : L.CompletionItem.t list = let now = Human_datetime.now () in let now_string = Format.asprintf "%a" Human_datetime.pp now in [L.CompletionItem.create ~label:now_string ~insertText:now_string ()] +type rule = { + applies: Completion_context.t -> bool; + items: + forest:State.t -> + position:L.Position.t -> + code:Tree.parsed option -> + L.CompletionItem.t list; +} + +let subtree_address_rule : rule = + { + applies = (fun ctx -> Completion_context.purpose ctx = Subtree_address); + items = (fun ~forest ~position:_ ~code:_ -> new_addr_completions ~forest); + } + +let reference_address_rule : rule = + { + applies = (fun ctx -> Completion_context.purpose ctx = Reference_address); + items = + (fun ~forest ~position:_ ~code:_ -> + addr_completions () @ new_addr_completions ~forest); + } + +let asset_rule : rule = + { + applies = (fun ctx -> Completion_context.purpose ctx = Asset); + items = + (fun ~forest ~position:_ ~code:_ -> + asset_completions ~config:forest.config); + } + +let date_rule : rule = + { + applies = (fun ctx -> Completion_context.purpose ctx = Date); + items = (fun ~forest:_ ~position:_ ~code:_ -> date_completions ()); + } + +let math_rule : rule = + { + applies = Completion_context.in_math; + items = + (fun ~forest ~position ~code -> + match code with + | Some nodes -> math_completions ~forest ~position nodes + | None -> builtin_completions Expand.math_builtins); + } + +let visible_rule : rule = + { + applies = (fun ctx -> Completion_context.purpose ctx = Body); + items = + (fun ~forest ~position ~code -> + match code with + | Some nodes -> visible_completions ~forest ~position nodes + | None -> builtin_completions Expand.prose_builtins @ syntax_completions); + } + +let rules = + [ + subtree_address_rule; + reference_address_rule; + asset_rule; + date_rule; + math_rule; + visible_rule; + ] + let compute ({context; position; textDocument = {uri}; _} : L.CompletionParams.t) : _ = Logs.debug ~src:Log.Src.lsp (fun m -> @@ -369,18 +281,14 @@ let compute | None -> None in let forest, uri = Util.forest_and_uri uri in - let config = forest.config in let* tree = forest.={uri} in - let* code = Tree.code tree in - let completion_types = completion_types ~position tree in + let* document = Tree.document tree in + let ctx = Completion_context.classify ~document ~position in + let code = Option.map Tree.Parsed.nodes (Tree.code tree) in let items = - let@ completion = List.concat_map @~ S.to_list completion_types in - match completion with - | Addrs -> addr_completions () - | New_addr -> new_addr_completions ~forest - | Assets -> asset_completions ~config - | Visible -> visible_completions ~forest ~position @@ Tree.Parsed.nodes code - | Date -> date_completions () + rules + |> List.filter (fun rule -> rule.applies ctx) + |> List.concat_map (fun rule -> rule.items ~forest ~position ~code) in Logs.debug ~src:Log.Src.lsp (fun m -> m "found %d completion items" (List.length items)); diff --git a/lib/language_server/Completion_context.ml b/lib/language_server/Completion_context.ml new file mode 100644 index 0000000..90b5a83 --- /dev/null +++ b/lib/language_server/Completion_context.ml @@ -0,0 +1,160 @@ +(* + * SPDX-FileCopyrightText: 2024 The Forester Project Contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + *) + +open Forester_core +open Forester_parser + +open struct + module L = Lsp.Types +end + +type delim = Brace | Square | Paren | Math_inline | Math_display + +type purpose = + | Body + | Subtree_address + | Reference_address + | Asset + | Date + | Math +[@@deriving show] + +type frame = {delim: delim; purpose: purpose} +type level = {frame: frame option; mutable last_closed: purpose option} +type t = {enclosing: frame list} + +let purpose (t : t) : purpose = + match t.enclosing with f :: _ -> f.purpose | [] -> Body + +let in_math (t : t) : bool = purpose t = Math + +let token_head : Tokens.token -> string option = function + | IDENT s -> Some s + | SUBTREE -> Some "subtree" + | SCOPE -> Some "scope" + | PUT -> Some "put" + | DEFAULT -> Some "put?" + | GET -> Some "get" + | IMPORT -> Some "import" + | EXPORT -> Some "export" + | NAMESPACE -> Some "namespace" + | OPEN -> Some "open" + | DEF -> Some "def" + | ALLOC -> Some "alloc" + | LET -> Some "let" + | FUN -> Some "fun" + | OBJECT -> Some "object" + | PATCH -> Some "patch" + | CALL -> Some "call" + | DATALOG -> Some "datalog" + | XML_ELT_IDENT (_, name) -> Some name + | DECL_XMLNS name -> Some ("xmlns:" ^ name) + | HASH_IDENT _ | TEXT _ | VERBATIM _ | COMMENT _ | WHITESPACE _ | SLASH + | LBRACE | RBRACE | LSQUARE | RSQUARE | LPAREN | RPAREN | HASH_LBRACE + | HASH_HASH_LBRACE | TICK | AT_SIGN | HASH | EOF | DX_ENTAILED | DX_VAR _ -> + None + +let purpose_for ~pending_head ~enclosing_purpose ~prev_sibling_purpose + ~(delim : delim) : purpose = + match delim with + | Math_inline | Math_display -> Math + | Brace -> begin + match pending_head with + | Some "transclude" -> Reference_address + | Some "route-asset" -> Asset + | Some "date" -> Date + | _ -> enclosing_purpose + end + | Square -> begin + match pending_head with + | Some "subtree" -> Subtree_address + | Some "route-asset" -> Asset + | Some "date" -> Date + | None when enclosing_purpose = Body -> Reference_address + | _ -> enclosing_purpose + end + | Paren -> begin + match pending_head with + | Some "route-asset" -> Asset + | Some "date" -> Date + | None + when enclosing_purpose = Body + && prev_sibling_purpose = Some Reference_address -> + Reference_address + | _ -> enclosing_purpose + end + +let dispatch lexbuf = + match Stack.top Lexer.mode_stack with + | Lexer.Main -> Lexer.token lexbuf + | Ident_init -> Lexer.ident_init lexbuf + | Ident_fragments -> Lexer.ident_fragments lexbuf + | Verbatim (herald, buffer) -> Lexer.verbatim herald buffer lexbuf + +let reset_lexer_state () = + Stack.clear Lexer.mode_stack; + Stack.push Lexer.Main Lexer.mode_stack + +let classify ~(document : Lsp.Text_document.t) ~(position : L.Position.t) : t = + let text = Lsp.Text_document.text document in + let source : Grace.Source.t = `String {name = None; content = text} in + let cursor = + (Analysis.byte_index_of_position ~source:(Range.total ~source) position + :> int) + in + let levels = ref [{frame = None; last_closed = None}] in + let pending_head = ref None in + let current_purpose () = + match !levels with + | {frame = Some f; _} :: _ -> f.purpose + | {frame = None; _} :: _ | [] -> Body + in + let prev_sibling_purpose () = + match !levels with {last_closed; _} :: _ -> last_closed | [] -> None + in + let push delim = + let p = + purpose_for ~pending_head:!pending_head + ~enclosing_purpose:(current_purpose ()) + ~prev_sibling_purpose:(prev_sibling_purpose ()) ~delim + in + levels := {frame = Some {delim; purpose = p}; last_closed = None} :: !levels; + pending_head := None + in + let pop () = + match !levels with + | closed :: (parent :: _ as rest) -> + Option.iter (fun f -> parent.last_closed <- Some f.purpose) closed.frame; + levels := rest + | _ -> () + in + let handle_token (tok : Tokens.token) = + match tok with + | LBRACE -> push Brace + | LSQUARE -> push Square + | LPAREN -> push Paren + | HASH_LBRACE -> push Math_inline + | HASH_HASH_LBRACE -> push Math_display + | RBRACE | RSQUARE | RPAREN -> + pop (); + pending_head := None + | tok -> pending_head := token_head tok + in + reset_lexer_state (); + let lexbuf = Lexing.from_string text in + (try + let continue_ = ref true in + while !continue_ do + let toks = dispatch lexbuf in + if Lexing.lexeme_end lexbuf > cursor then continue_ := false + else begin + List.iter handle_token toks; + if List.mem Tokens.EOF toks then continue_ := false + end + done + with _ -> ()); + reset_lexer_state (); + {enclosing = List.filter_map (fun {frame; _} -> frame) !levels} diff --git a/lib/language_server/dune b/lib/language_server/dune index e6ce8f3..bd833e3 100644 --- a/lib/language_server/dune +++ b/lib/language_server/dune @@ -20,6 +20,7 @@ forester.search forester.prelude forester.core + forester.parser forester.compiler forester.frontend forester.server diff --git a/test/Testables.ml b/test/Testables.ml index 1569532..d50e6ce 100644 --- a/test/Testables.ml +++ b/test/Testables.ml @@ -35,4 +35,3 @@ let document = let tree = testable Code.pp_tree ( = ) let content = testable Types.pp_content ( = ) let action = testable Action.pp ( = ) -let completion_type = testable Forester_lsp.Completion.pp_completion ( = ) diff --git a/test/lsp.t b/test/lsp.t index 5f9f383..d01159b 100644 --- a/test/lsp.t +++ b/test/lsp.t @@ -4,8 +4,6 @@ > \title{hello > EOF -The completion behavior should probably be refined to only return math stuff when the cursor is in math mode... - $ nvim --headless --clean -l lsp_driver.lua { column = 0, @@ -13,4 +11,4 @@ The completion behavior should probably be refined to only return math stuff whe message = "parse error", severity = 1 } - { "⋃", ",", ";", "Alpha", "Beta", "Big", "Bigg", "Biggl", "Biggr", "Bigl", "Bigr", "Box", "Bumpeq", "Cap", "Colonapprox", "Coloneq", "Coloneqq", "Colonsim", "Cup", "Del", "Delta", "Diamond", "Downarrow", "Eqcolon", "Eqqcolon", "Eta", "Gamma", "Im", "Intersection", "Iota", "Kappa", "Lambda", "Leftarrow", "Leftrightarrow", "Lleftarrow", "Longleftrightarrow", "Lsh", "Mu", "Nu", "Omega", "Oplus", "Otimes", "Perp", "Phi", "Pi", "Pr", "Psi", "Re", "Rho", "Rightarrow", "Rrightarrow", "Rsh", "Sigma", "Subset", "Supset", "Tau", "Theta", "Union", "Uparrow", "Updownarrow", "Upsi", "Upsilon", "VDash", "Vbar", "Vdash", "Vee", "Vert", "Vvdash", "Wedge", "Xi", "Zeta", "_", "aleph", "align", "alloc", "alpha", "amalg", "angle", "approx", "approxeq", "arccos", "arcsin", "arctan", "arg", "array", "arrayopts", "ast", "asymp", "atop", "author", "author/literal", "backepsilon", "backprime", "backsim", "backsimeq", "backslash", "bar", "barwedge", "because", "begin", "begintoggle", "beta", "beth", "between", "bgcolor", "bghighlight", "bghilight", "big", "bigcap", "bigcirc", "bigcup", "bigg", "biggl", "biggr", "biginterleave", "bigl", "bigodot", "bigoplus", "bigotimes", "bigr", "bigsqcap", "bigsqcup", "bigstar", "bigtriangledown", "bigtriangleup", "biguplus", "bigvee", "bigwedge", "binom", "blacklozenge", "blacksquare", "blacktriangle", "blacktriangledown", "blacktriangleleft", "blacktriangleright", "blockquote", "boldsymbol", "bot", "bottom", "bowtie", "boxdot", "boxed", "boxminus", "boxplus", "boxtimes", "bullet", "bumpeq", "call", "cap", "cdot", "cdots", "cellopts", "check", "chi", "circ", "circeq", "circlearrowleft", "circlearrowright", "circledast", "circledcirc", "circleddash", "closure", "clubsuit", "code", "colalign", "collayout", "collines", "colon", "colonapprox", "coloneq", "coloneqq", "colonsim", "color", "colspan", "cong", "conint", "contourintegral", "contributor", "contributor/literal", "coprod", "coproduct", "cos", "cosh", "cot", "coth", "csc", "cup", "curlyeqprec", "curlyeqsucc", "curlyvee", "curlywedge", "current-tree", "curvearrowbotright", "curvearrowleft", "curvearrowright", "dagger", "darr", "dashleftarrow", "dashrightarrow", "dashv", "datalog", "date", "dblcolon", "ddagger", "ddddot", "dddot", "ddot", "ddots", "def", "deg", "delta", "det", "diamond", "diamondsuit", "digamma", "dim", "displaystyle", "div", "divideontimes", "dlap", "dot", "doteq", "doteqdot", "dotplus", "dots", "doublebarwedge", "doubleintegral", "downarrow", "downdownarrows", "downharpoonleft", "downharpoonright", "downuparrow", "duparr", "ell", "em", "embedsin", "empty", "emptyset", "end", "endtoggle", "epsilon", "eqcirc", "eqcolon", "eqqcolon", "eqslantgtr", "eqslantless", "equalcols", "equalrows", "equiv", "eta", "eth", "execute", "exists", "exp", "export", "fallingdotseq", "fghighlight", "fghilight", "figcaption", "figure", "flat", "forall", "frac", "frame", "frown", "fun", "gamma", "gcd", "ge", "geq", "geqq", "geqslant", "get", "gg", "ggg", "gnapprox", "gneq", "gneqq", "gnsim", "gt", "gtrapprox", "gtrdot", "gtreqless", "gtreqqless", "gtrless", "gtrsim", "gvertneqq", "hat", "hbar", "heartsuit", "hom", "hookleftarrow", "hookrightarrow", "iff", "iiiint", "iiint", "iint", "imath", "impliedby", "implies", "import", "in", "includegraphics", "inf", "infinity", "infty", "int", "integral", "intercal", "intersection", "invamp", "iota", "jmath", "kappa", "ker", "lVert", "lambda", "land", "lang", "langle", "lbrace", "lceil", "ldots", "le", "left", "leftarrow", "leftarrowtail", "leftharpoondown", "leftharpoonup", "leftleftarrows", "leftrightarrow", "leftrightarrows", "leftrightharpoons", "leftrightsquigarrow", "leftsquigarrow", "leftthreetimes", "lefttorightarrow", "leq", "leqq", "leqslant", "lessapprox", "lessdot", "lesseqgtr", "lesseqqgtr", "lessgtr", "lesssim", "let", "lfloor", "lg", "lhd", "li", "lim", "liminf", "limsup", "ll", "llangle", "llap", "llbracket", "lll", "lmoustache", "ln", "lnapprox", "lneq", "lneqq", "lnsim", "log", "longleftarrow", "longrightarrow", "looparrowleft", "looparrowright", "lor", "lozenge", "lparen", "lt", "ltimes", "lvert", "lvertneqq", "map", "mapsto", "mathbb", "mathbf", "mathbin", "mathcal", "mathclap", "mathfr", "mathfrak", "mathit", "mathllap", "mathop", "mathord", "mathpunct", "mathraisebox", "mathrel", "mathrlap", "mathrm", "mathscr", "mathsf", "mathtt", "max", "measuredangle", "medspace", "meta", "mho", "mid", "middle", "min", "minusb", "mod", "models", "mp", "mu", "multimap", "multiscripts", "nLeftarrow", "nLeftrightarrow", "nRightarrow", "nVDash", "nabla", "namespace", "natural", "ncong", "ne", "neArr", "neArrow", "nearr", "nearrow", "neg", "negmedspace", "negspace", "negthickspace", "negthinspace", "neq", "nequiv", "nexists", "ngeq", "ngeqq", "ngeqslant", "ngtr", "ni", "nleftarrow", "nleftrightarrow", "nleq", "nleqq", "nleqslant", "nless", "nmid", "not", "notin", "notni", "nparallel", "nprec", "npreceq", "nrightarrow", "nshortmid", "nshortparallel", "nsim", "nsubset", "nsubseteq", "nsubseteqq", "nsucc", "nsucceq", "nsupset", "nsupseteq", "ntriangleleft", "ntrianglelefteq", "ntriangleright", "ntrianglerighteq", "nu", "number", "nvDash", "nvdash", "nwArr", "nwArrow", "nwarr", "nwarrow", "object", "odash", "odot", "oint", "ol", "omega", "omicron", "ominus", "open", "operatorname", "oplus", "oslash", "otimes", "over", "overbrace", "overleftarrow", "overleftrightarrow", "overline", "overrightarrow", "overset", "p", "padding", "parallel", "parent", "parr", "partial", "patch", "perp", "phantom", "phi", "pi", "pitchfork", "plusb", "pm", "pmod", "pre", "prec", "precapprox", "preccurlyeq", "preceq", "precnapprox", "precnsim", "precsim", "prime", "prod", "product", "propto", "psi", "put", "put?", "qed", "qquad", "quad", "quadrupleintegral", "query", "rVert", "rang", "rangle", "rbrace", "rceil", "ref", "rel/has-author", "rel/has-direct-contributor", "rel/has-tag", "rel/has-taxon", "rel/in-host", "rel/is-article", "rel/is-asset", "rel/is-node", "rel/is-person", "rel/is-reference", "rel/links-to", "rel/transcludes", "rel/transcludes/reflexive-transitive-closure", "rel/transcludes/transitive-closure", "rfloor", "rhd", "rho", "right", "rightarrow", "rightarrowtail", "rightharpoondown", "rightharpoonup", "rightleftarrows", "rightleftharpoons", "rightrightarrows", "rightsquigarrow", "rightthreetimes", "righttoleftarrow", "risingdotseq", "rlap", "rmoustache", "root", "route-asset", "rowalign", "rowlines", "rowopts", "rowspan", "rparen", "rrangle", "rrbracket", "rtimes", "rvert", "scope", "scriptscriptsize", "scriptsize", "seArr", "seArrow", "searr", "searrow", "sec", "setminus", "sharp", "shortmid", "shortparallel", "sigma", "sim", "simeq", "sin", "sinh", "slash", "smallfrown", "smallsetminus", "smallsmile", "smile", "space", "spadesuit", "sphericalangle", "sqcap", "sqcup", "sqrt", "sqsubset", "sqsubseteq", "sqsupset", "sqsupseteq", "square", "sslash", "stackrel", "star", "startverb", "statusline", "strong", "subset", "subseteq", "subseteqq", "subsetneq", "subsetneqq", "substack", "subtree", "succ", "succapprox", "succcurlyeq", "succeq", "succnapprox", "succnsim", "succsim", "sum", "sup", "supset", "supseteq", "supseteqq", "supsetneq", "supsetneqq", "swArr", "swArrow", "swarr", "swarrow", "syndicate-current-tree-as-atom-feed", "syndicate-query-as-json-blob", "tag", "tan", "tanh", "tau", "taxon", "tbinom", "tensor", "tex", "text", "textsize", "textstyle", "tfrac", "therefore", "theta", "thickapprox", "thicksim", "thickspace", "thinspace", "tilde", "times", "timesb", "title", "to", "toggle", "tooltip", "top", "transclude", "transclude/expanded", "transclude/heading", "transclude/metadata", "transclude/numbered", "transclude/toc", "triangle", "triangledown", "triangleleft", "trianglelefteq", "triangleq", "triangleright", "trianglerighteq", "tripleintegral", "twoheadleftarrow", "twoheadrightarrow", "udots", "ul", "ulap", "underbrace", "underleftarrow", "underleftrightarrow", "underline", "underoverset", "underrightarrow", "underset", "union", "unlhd", "unrhd", "uparr", "uparrow", "updarr", "updownarrow", "upharpoonleft", "upharpoonright", "uplus", "upsilon", "upuparrows", "vDash", "varepsilon", "varkappa", "varnothing", "varphi", "varpi", "varpropto", "varrho", "varsigma", "varsubsetneq", "varsubsetneqq", "varsupsetneq", "varsupsetneqq", "vartheta", "vartriangleleft", "vartriangleright", "vdash", "vdots", "vec", "vee", "veebar", "vert", "wedge", "widebar", "widecheck", "widehat", "widetilde", "widevec", "wp", "wr", "xLeftarrow", "xLeftrightarrow", "xRightarrow", "xhookleftarrow", "xhookrightarrow", "xi", "xleftarrow", "xleftrightarrow", "xleftrightharpoons", "xmapsto", "xmlns", "xrightarrow", "xrightleftharpoons", "zeta" } + { "alloc", "author", "author/literal", "blockquote", "call", "code", "contributor", "contributor/literal", "current-tree", "datalog", "date", "def", "em", "execute", "export", "figcaption", "figure", "fun", "get", "import", "let", "li", "meta", "namespace", "number", "object", "ol", "open", "p", "parent", "patch", "pre", "put", "put?", "query", "ref", "rel/has-author", "rel/has-direct-contributor", "rel/has-tag", "rel/has-taxon", "rel/in-host", "rel/is-article", "rel/is-asset", "rel/is-node", "rel/is-person", "rel/is-reference", "rel/links-to", "rel/transcludes", "rel/transcludes/reflexive-transitive-closure", "rel/transcludes/transitive-closure", "route-asset", "scope", "startverb", "strong", "subtree", "syndicate-current-tree-as-atom-feed", "syndicate-query-as-json-blob", "tag", "taxon", "tex", "title", "transclude", "transclude/expanded", "transclude/heading", "transclude/metadata", "transclude/numbered", "transclude/toc", "ul", "xmlns" } -- 2.51.2