Skip to main content

dr_strange_parser/
complete.rs

1//! Completing a half-typed query: what may come next, and what this plane
2//! would make of it.
3//!
4//! An editor's own completion can only offer keywords — it has no idea which
5//! labels a plane holds, let alone that a `Function` is far more often the
6//! source of a `CALLS` than of an `IMPORTS`. This does, because it is handed
7//! the plane's vocabulary ([`Vocab`], the soft-schema catalog) along with the
8//! text: after `MATCH ` the best guess is `(n:Function)` because that is the
9//! label the plane holds most of, and after `MATCH (n:Function) ` it is
10//! `-[:CALLS]->(m:Function)` because that is the edge most often leaving a
11//! `Function`, landing where such edges most often land.
12//!
13//! ## Why not the grammar
14//!
15//! [`crate::parse`] is a `nom` grammar over *whole* statements: a prefix fails
16//! it, and a failure says where it stopped rather than what would have
17//! continued. So this is a second, much smaller reading of the same language —
18//! a tokenizer and a position machine that only ever asks "given what has been
19//! typed, what kind of thing does the caret sit at?". It accepts far more than
20//! the grammar does, deliberately: a half-written query is not yet wrong.
21//!
22//! The two are kept honest by the grammar itself: every suggestion is written
23//! the way [`crate::parse`] reads it, and the tests parse what they complete.
24
25/// What a plane holds, as far as completing a query needs to know: the
26/// soft-schema catalog, which a server has already computed.
27///
28/// Counts are what make one suggestion better than another, so they are part
29/// of the vocabulary rather than an afterthought. An empty `Vocab` is
30/// perfectly usable — completion falls back to the language's own keywords and
31/// to `(n)`, which names no label and so cannot be wrong.
32#[derive(Debug, Clone, Default, PartialEq, Eq)]
33pub struct Vocab {
34    pub labels: Vec<LabelInfo>,
35    pub edges: Vec<EdgeInfo>,
36}
37
38/// One node label: how many nodes carry it, and the properties they hold.
39#[derive(Debug, Clone, Default, PartialEq, Eq)]
40pub struct LabelInfo {
41    pub name: String,
42    pub count: u64,
43    /// Property names, in the order the catalog gives them.
44    pub properties: Vec<String>,
45}
46
47/// One edge type: how many the plane holds, and between which labels.
48#[derive(Debug, Clone, Default, PartialEq, Eq)]
49pub struct EdgeInfo {
50    pub name: String,
51    pub count: u64,
52    pub connections: Vec<Connection>,
53}
54
55/// `src -[:type]-> dst`, and how many such edges there are.
56#[derive(Debug, Clone, Default, PartialEq, Eq)]
57pub struct Connection {
58    pub src: String,
59    pub dst: String,
60    pub count: u64,
61}
62
63/// What the caret sits at — the question a suggestion answers.
64///
65/// Carries the context that made the guess possible, so a caller can say *why*
66/// a list is what it is ("properties of `n`, a `Function`") instead of showing
67/// a bare list.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum Expect {
70    /// The keyword a statement opens with.
71    Statement,
72    /// A whole node pattern — `(n:Label)`. `var` is the name it would bind.
73    Node { var: String },
74    /// A variable name, just inside `(`.
75    NodeVar { var: String },
76    /// A label, after `(n:`.
77    Label,
78    /// The end of a node pattern: `:Label`, or `)`.
79    NodeEnd,
80    /// A relationship leaving the node just closed — or the clause that
81    /// follows it, since a pattern may simply end here. `from` is that node's
82    /// label, `var` the name the next node would bind, and `bound` how many
83    /// the pattern has bound already.
84    Hop {
85        from: Option<String>,
86        var: String,
87        bound: usize,
88    },
89    /// An edge type — anywhere from just after the arrow to just after the
90    /// `:`. `lead` is the punctuation that still has to be written before the
91    /// type: `[:` right after the arrow, `:` right after the bracket, and
92    /// nothing once the colon is there.
93    EdgeType {
94        from: Option<String>,
95        var: String,
96        incoming: bool,
97        lead: &'static str,
98    },
99    /// The end of a relationship: `]`, or the bounds of a `*` range —
100    /// `ranged` when a `*` has been written and is still waiting for them.
101    RelEnd { incoming: bool, ranged: bool },
102    /// The arrow that closes a relationship — `->`, or `-`.
103    Arrow { incoming: bool },
104    /// A predicate, after `WHERE`.
105    Predicate { vars: Vec<String> },
106    /// A returned item, after `RETURN`.
107    Projection { vars: Vec<String> },
108    /// A property of `var`, which is a `label`.
109    Property { var: String, label: Option<String> },
110    /// The right-hand side of a comparison — a value only the author knows.
111    Value,
112    /// The key a `ORDER BY` sorts on.
113    SortKey { vars: Vec<String> },
114    /// What a call takes: `key(` wants a variable, `collect(` a value of one.
115    Argument { vars: Vec<String> },
116    /// A clause that may follow a complete one — `ORDER BY`, `LIMIT`,
117    /// `AS OF`. `done` are the ones already written, which cannot come again.
118    Clause { done: Vec<String> },
119    /// Nothing worth guessing at: inside a string literal, where the words
120    /// typed are data rather than syntax; or after a shape the grammar
121    /// refuses (inline properties in a `MATCH` node), where every suggestion
122    /// would steer further into a query that cannot run.
123    Nothing,
124}
125
126/// What kind of thing a suggestion is, for a caller that shows them
127/// differently.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum Kind {
130    Keyword,
131    Label,
132    EdgeType,
133    Property,
134    Variable,
135    /// A whole shape — `(n:Function)`, `-[:CALLS]->(m:Function)`.
136    Snippet,
137}
138
139/// One thing the caret could become.
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct Suggestion {
142    /// What to show in a list — the token or shape itself.
143    pub text: String,
144    /// What to insert **at the caret**: [`Self::text`] less the partial word
145    /// already typed, plus whatever punctuation finishes the position.
146    pub insert: String,
147    /// What tells this candidate from the next — a count, a destination label.
148    pub detail: Option<String>,
149    pub kind: Kind,
150}
151
152/// What may come next, best first.
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct Completion {
155    /// The best candidate's [`Suggestion::insert`]: what a ghost after the
156    /// caret should read, and what accepting it should type. `None` when there
157    /// is nothing to say.
158    pub best: Option<String>,
159    /// Every candidate, best first.
160    pub suggestions: Vec<Suggestion>,
161    /// What the caret sits at.
162    pub expects: Expect,
163    /// The partial word under the caret, which every suggestion completes.
164    pub word: String,
165}
166
167/// How many candidates a position offers. Enough to choose from, few enough to
168/// read: a plane with three hundred labels has no useful three-hundredth
169/// suggestion.
170const MAX_SUGGESTIONS: usize = 12;
171
172/// What may follow `prefix` — the query text from its start to the caret — in
173/// a plane whose vocabulary is `vocab`.
174///
175/// Never fails and never parses: a prefix is not a statement, and asking
176/// whether it is one would answer the wrong question. Reads only what is
177/// *before* the caret, which is what an editor can hand over cheaply whenever
178/// typing pauses.
179pub fn complete(prefix: &str, vocab: &Vocab) -> Completion {
180    let Some((tokens, word)) = tokenize(prefix) else {
181        // The caret is inside an unterminated string: what is typed there is
182        // data, and completing it would be noise.
183        return Completion {
184            best: None,
185            suggestions: Vec::new(),
186            expects: Expect::Nothing,
187            word: String::new(),
188        };
189    };
190    let (expects, closing) = scan(&tokens);
191    // A relationship's brackets take no spaces — `-[:CALLS ]->` is not a
192    // relationship — so a caret that has left one has nothing to add there.
193    let loose = word.is_empty() && prefix.ends_with(char::is_whitespace);
194    let mut suggestions = suggest(&expects, &word, vocab, loose, closing);
195    suggestions.truncate(MAX_SUGGESTIONS);
196    Completion {
197        best: suggestions.first().map(|s| s.insert.clone()),
198        suggestions,
199        expects,
200        word,
201    }
202}
203
204// ---- tokens -----------------------------------------------------------------
205
206/// One token of a prefix. Words keep their text because a suggestion may need
207/// to read it — a variable's name, a label's spelling.
208#[derive(Debug, Clone, PartialEq)]
209enum Tok<'a> {
210    Word(&'a str),
211    Punct(&'a str),
212    Num,
213    Str,
214}
215
216/// Punctuation that runs together into one token: the arrows a relationship is
217/// written with (`->`, `<-`, `-->`, `--`) and the comparisons (`<=`, `>=`,
218/// `<>`, `!=`), which are the only places two of these characters mean one
219/// thing.
220const ARROWY: &str = "-<>=!";
221
222/// Split a prefix into complete tokens and the partial word under the caret.
223///
224/// `None` when the caret is inside an unterminated string literal or a
225/// backticked name: what is typed there is data (or a name of the author's
226/// own), and completing it would be noise. A string literal is read the way
227/// [`crate::parse`] reads it — a backslash escapes whatever follows, so an
228/// escaped quote does not end the literal — and a backticked name is one word
229/// whatever it contains.
230///
231/// The partial word is whatever the prefix ends *part-way through*: a prefix
232/// ending in a space, or in punctuation, has none — the caret sits at a fresh
233/// position rather than inside a name.
234///
235/// The scan walks characters, never bytes: an identifier may be Unicode
236/// (`函数`, `café`), and a string may hold anything, so every slice below is
237/// taken at a character boundary.
238fn tokenize(prefix: &str) -> Option<(Vec<Tok<'_>>, String)> {
239    let mut toks = Vec::new();
240    let mut partial = None;
241    let mut chars = prefix.char_indices().peekable();
242    // Advance while the character under the cursor satisfies `keep`, and
243    // return the byte offset of the first that does not (or the end).
244    let end_of = |chars: &mut std::iter::Peekable<std::str::CharIndices<'_>>,
245                  keep: &dyn Fn(char) -> bool| {
246        while let Some(&(_, c)) = chars.peek() {
247            if !keep(c) {
248                return;
249            }
250            chars.next();
251        }
252    };
253    let at = |chars: &mut std::iter::Peekable<std::str::CharIndices<'_>>| {
254        chars.peek().map_or(prefix.len(), |&(i, _)| i)
255    };
256    while let Some((start, c)) = chars.next() {
257        if c.is_whitespace() {
258            continue;
259        }
260        if c == '"' || c == '\'' {
261            // A backslash escapes whatever follows, so an escaped quote is
262            // part of the string; an unterminated string ends the scan.
263            loop {
264                match chars.next() {
265                    None => return None,
266                    Some((_, '\\')) => {
267                        chars.next();
268                    }
269                    Some((_, q)) if q == c => break,
270                    Some(_) => {}
271                }
272            }
273            toks.push(Tok::Str);
274        } else if c == '`' {
275            // A backticked name is one word, backticks and all: the grammar
276            // takes it as a plain identifier, and a suggestion that reads it
277            // (a variable's name) writes it back the same way.
278            end_of(&mut chars, &|c| c != '`');
279            // Unterminated: the caret is inside the name.
280            chars.next()?;
281            toks.push(Tok::Word(&prefix[start..at(&mut chars)]));
282        } else if c.is_alphabetic() || c == '_' || c == '$' {
283            end_of(&mut chars, &|c| c.is_alphanumeric() || c == '_');
284            let end = at(&mut chars);
285            let word = &prefix[start..end];
286            if end == prefix.len() {
287                partial = Some(word.to_string()); // the caret is inside it
288            } else {
289                toks.push(Tok::Word(word));
290            }
291        } else if c.is_ascii_digit() {
292            // `10`, `2.5`, `1e9`: the fraction and the exponent belong to the
293            // number, or `2.5` would read as a number, a `.` and a number.
294            end_of(&mut chars, &|c| c.is_ascii_digit());
295            if let Some(&(dot, '.')) = chars.peek()
296                && prefix[dot + 1..].starts_with(|c: char| c.is_ascii_digit())
297            {
298                chars.next();
299                end_of(&mut chars, &|c| c.is_ascii_digit());
300            }
301            if let Some(&(e, 'e' | 'E')) = chars.peek()
302                && prefix[e + 1..]
303                    .trim_start_matches(['+', '-'])
304                    .starts_with(|c: char| c.is_ascii_digit())
305            {
306                chars.next();
307                end_of(&mut chars, &|c| c == '+' || c == '-');
308                end_of(&mut chars, &|c| c.is_ascii_digit());
309            }
310            toks.push(Tok::Num);
311        } else {
312            // A run of arrow characters is one token, and so is `..`;
313            // everything else stands alone.
314            if ARROWY.contains(c) || c == '.' {
315                let run = if c == '.' { "." } else { ARROWY };
316                end_of(&mut chars, &|c| run.contains(c));
317            }
318            toks.push(Tok::Punct(&prefix[start..at(&mut chars)]));
319        }
320    }
321    Some((toks, partial.unwrap_or_default()))
322}
323
324// ---- the position machine -----------------------------------------------------
325
326/// Where a scan has got to. Most states become a public [`Expect`]; the few
327/// that do not are the places a reader would not call a position — just inside
328/// a `(`, part-way through a relationship — and they fold into one when the
329/// scan ends.
330#[derive(Debug, Clone, PartialEq, Eq)]
331enum Pos {
332    Statement,
333    /// After `MATCH` / `CREATE` / a closed relationship: a node pattern.
334    NodeOpen,
335    /// Just inside `(`: a variable, or `:`.
336    NodeVar,
337    /// After `(n`: `:`, or `)`.
338    NodeColon,
339    /// After `(n:`: a label.
340    NodeLabel,
341    /// After `(n:Label`: `)`, or a `{key: $k, p: 1}` map.
342    NodeClose,
343    /// Just inside a node's `{`, and after each `,` in it: a property name —
344    /// `key`, the external key, or one the label's nodes hold.
345    PropKey,
346    /// Anywhere else inside a node's `{…}`: the `:` after a key, or its value,
347    /// which is the author's alone.
348    PropRest,
349    /// After `(…)`: a relationship, or the clause that follows.
350    Hop,
351    /// After `-` or `<-`: `[`.
352    RelOpen,
353    /// Just inside `-[`: `:`, or a relationship variable.
354    RelVar,
355    /// After `-[r`: `:`.
356    RelColon,
357    /// After `-[:`: an edge type.
358    RelType,
359    /// After `-[:TYPE`: `]`, or a `*1..3` range.
360    RelClose,
361    /// After `-[:TYPE]`: the arrow.
362    Arrow,
363    /// After `WHERE`, and after each `AND`.
364    Predicate,
365    /// After `RETURN`, and after each `,`.
366    Projection,
367    /// After a variable in a clause: `.`, an operator, `AS`, `,`.
368    Term,
369    /// Inside a call's brackets: `key(`, `count(`, `collect(`.
370    Call,
371    /// After `n.`: a property name.
372    Property(String),
373    /// After a comparison operator.
374    Value,
375    /// After `ORDER BY`: the key to sort on.
376    Sort,
377    /// After a complete clause: `ORDER BY`, `SKIP`, `LIMIT`, `AS OF`.
378    Clause,
379    /// After a shape the grammar refuses outright — `{` in a `MATCH` node,
380    /// whose predicates live in WHERE. Nothing that follows mends it, so the
381    /// scan stays here: the three readings of the language must agree, and
382    /// the parser's is a hard error.
383    Refused,
384}
385
386/// What a scan learned: where the caret is, and the variables the pattern
387/// bound on the way — which is what turns `n.` into a list of properties.
388struct Scan {
389    pos: Pos,
390    /// `(variable, label)` for every node pattern closed so far.
391    vars: Vec<(String, Option<String>)>,
392    /// The label of the node most recently closed — what a relationship
393    /// leaving it is ranked by.
394    from: Option<String>,
395    /// The node being read: its variable, then its label.
396    var: Option<String>,
397    label: Option<String>,
398    /// The relationship being read is written `<-`.
399    incoming: bool,
400    /// The punctuation still owed before its type — see [`Expect::EdgeType`].
401    /// A lone `<` owes its hyphen as well as the bracket.
402    owed: &'static str,
403    /// A `*` has been written in it, and whether its bounds followed.
404    star: Option<bool>,
405    /// The variable a `.` was written after.
406    term: Option<String>,
407    /// The tail clauses already written — a query has one `LIMIT`.
408    done: Vec<String>,
409    /// A `CREATE`/`MERGE` has been written: its nodes carry a property map,
410    /// where a read pattern's may not.
411    writing: bool,
412}
413
414impl Scan {
415    fn close_node(&mut self) {
416        let label = self.label.take();
417        if let Some(var) = self.var.take() {
418            self.vars.push((var, label.clone()));
419        }
420        self.from = label;
421    }
422
423    fn label_of(&self, var: &str) -> Option<String> {
424        self.vars
425            .iter()
426            .find(|(v, _)| v == var)
427            .and_then(|(_, l)| l.clone())
428    }
429
430    fn names(&self) -> Vec<String> {
431        self.vars.iter().map(|(v, _)| v.clone()).collect()
432    }
433
434    /// A name for the next node a pattern binds. `n`, then `m`, then `o` — the
435    /// letters every Cypher example in the world uses, in that order — and a
436    /// numbered one past the end of them, so a long pattern still cannot bind
437    /// the same name twice.
438    fn next_var(&self) -> String {
439        const NAMES: &[&str] = &["n", "m", "o", "p", "q", "r", "s", "t"];
440        NAMES
441            .iter()
442            .find(|name| !self.vars.iter().any(|(v, _)| v == *name))
443            .map_or_else(
444                || format!("n{}", self.vars.len()),
445                |name| (*name).to_string(),
446            )
447    }
448}
449
450/// The keywords that end whatever clause came before them, wherever they
451/// appear — which is what lets `MATCH (n) WHERE … CREATE (m)` scan in one
452/// pass.
453fn clause_keyword(word: &str) -> Option<&'static str> {
454    Some(match word.to_ascii_uppercase().as_str() {
455        "WHERE" => "WHERE",
456        "RETURN" => "RETURN",
457        "CREATE" => "CREATE",
458        "MERGE" => "MERGE",
459        "SET" | "REMOVE" | "DELETE" => "SET",
460        "AND" | "OR" | "NOT" => "AND",
461        "BY" => "BY",
462        "DISTINCT" => "RETURN",
463        "SKIP" | "LIMIT" => "COUNT",
464        "ORDER" | "DETACH" | "AS" | "OF" => "TAIL",
465        _ => return None,
466    })
467}
468
469/// Walk the tokens, ending where the caret sits.
470///
471/// Deliberately forgiving: a token that fits no transition leaves the position
472/// alone rather than derailing the scan, because half a query is full of
473/// tokens the grammar has not seen the rest of yet.
474///
475/// The flag says the node under the caret is past the point a label may be
476/// added — it has one, or a property map — so all that closes it is `)`.
477fn scan(tokens: &[Tok<'_>]) -> (Expect, bool) {
478    let mut s = Scan {
479        pos: Pos::Statement,
480        vars: Vec::new(),
481        from: None,
482        var: None,
483        label: None,
484        incoming: false,
485        owed: "[:",
486        star: None,
487        term: None,
488        done: Vec::new(),
489        writing: false,
490    };
491    for tok in tokens {
492        step(&mut s, tok);
493    }
494    let closing = s.pos == Pos::NodeClose;
495    let expects = match &s.pos {
496        Pos::Statement => Expect::Statement,
497        Pos::NodeOpen => Expect::Node { var: s.next_var() },
498        Pos::NodeVar => Expect::NodeVar { var: s.next_var() },
499        Pos::NodeColon | Pos::NodeClose => Expect::NodeEnd,
500        Pos::PropKey => Expect::Property {
501            var: s.var.clone().unwrap_or_default(),
502            label: s.label.clone(),
503        },
504        Pos::PropRest => Expect::Value,
505        Pos::NodeLabel => Expect::Label,
506        Pos::Hop => Expect::Hop {
507            from: s.from.clone(),
508            var: s.next_var(),
509            bound: s.vars.len(),
510        },
511        Pos::RelOpen | Pos::RelVar | Pos::RelColon | Pos::RelType => Expect::EdgeType {
512            from: s.from.clone(),
513            var: s.next_var(),
514            incoming: s.incoming,
515            lead: match s.pos {
516                Pos::RelOpen => s.owed,
517                Pos::RelVar | Pos::RelColon => ":",
518                _ => "",
519            },
520        },
521        Pos::RelClose => Expect::RelEnd {
522            incoming: s.incoming,
523            ranged: s.star == Some(false),
524        },
525        Pos::Arrow => Expect::Arrow {
526            incoming: s.incoming,
527        },
528        Pos::Predicate => Expect::Predicate { vars: s.names() },
529        Pos::Projection => Expect::Projection { vars: s.names() },
530        Pos::Property(var) => Expect::Property {
531            label: s.label_of(var),
532            var: var.clone(),
533        },
534        Pos::Value => Expect::Value,
535        Pos::Sort => Expect::SortKey { vars: s.names() },
536        Pos::Call => Expect::Argument { vars: s.names() },
537        Pos::Term | Pos::Clause => Expect::Clause {
538            done: s.done.clone(),
539        },
540        Pos::Refused => Expect::Nothing,
541    };
542    (expects, closing)
543}
544
545fn step(s: &mut Scan, tok: &Tok<'_>) {
546    if let Tok::Word(w) = tok
547        && let Some(kw) = clause_keyword(w)
548    {
549        // A tail clause comes once: a query has one `LIMIT`, and offering a
550        // second is offering a syntax error.
551        let written = w.to_ascii_uppercase();
552        if let Some((tail, _)) = TAIL
553            .iter()
554            .find(|(t, _)| t.split(' ').next() == Some(written.as_str()))
555        {
556            s.done.push((*tail).to_string());
557        }
558        if s.pos == Pos::Refused {
559            return;
560        }
561        if matches!(kw, "CREATE" | "MERGE") {
562            s.writing = true;
563        }
564        s.pos = match kw {
565            "WHERE" | "AND" | "SET" => Pos::Predicate,
566            "RETURN" => Pos::Projection,
567            "CREATE" | "MERGE" => Pos::NodeOpen,
568            "BY" => Pos::Sort,
569            // `SKIP 10` — a number nobody but the author knows.
570            "COUNT" => Pos::Value,
571            _ => Pos::Clause,
572        };
573        return;
574    }
575    match (&s.pos, tok) {
576        // A statement opens with the keyword that says where its rows come
577        // from. `CALL` names an algorithm before its `ON (n:Label)`, so it
578        // waits where it is until the node arrives.
579        (Pos::Statement, Tok::Word(w)) => {
580            if matches!(
581                w.to_ascii_uppercase().as_str(),
582                "MATCH" | "SEARCH" | "HYBRID" | "BEAM" | "ON"
583            ) {
584                s.pos = Pos::NodeOpen;
585            }
586        }
587
588        // ---- node patterns ---------------------------------------------------
589        (Pos::NodeOpen, Tok::Punct("(")) => s.pos = Pos::NodeVar,
590        (Pos::NodeVar, Tok::Word(w)) => {
591            s.var = Some((*w).to_string());
592            s.pos = Pos::NodeColon;
593        }
594        (Pos::NodeVar | Pos::NodeColon, Tok::Punct(":")) => s.pos = Pos::NodeLabel,
595        (Pos::NodeLabel, Tok::Word(w)) => {
596            s.label = Some((*w).to_string());
597            s.pos = Pos::NodeClose;
598        }
599        (Pos::NodeVar | Pos::NodeColon | Pos::NodeClose, Tok::Punct(")")) => {
600            s.close_node();
601            s.pos = Pos::Hop;
602        }
603        // A write's node carries its key and properties in a map, `{key: $k,
604        // p: 1}`. Read as its own region, so that the `:` after a key is not
605        // taken for the one that opens a label, and the `}` hands back to the
606        // node it sits in. A read pattern's node has no map — the parser
607        // refuses one with a pointer at WHERE — so there the scan stops.
608        (Pos::NodeVar | Pos::NodeColon | Pos::NodeClose, Tok::Punct("{")) => {
609            s.pos = if s.writing {
610                Pos::PropKey
611            } else {
612                Pos::Refused
613            };
614        }
615        (Pos::Refused, _) => {}
616        (Pos::PropKey, Tok::Word(_)) => s.pos = Pos::PropRest,
617        (Pos::PropRest, Tok::Punct(",")) => s.pos = Pos::PropKey,
618        (Pos::PropKey | Pos::PropRest, Tok::Punct("}")) => s.pos = Pos::NodeClose,
619        (Pos::PropRest, _) => {}
620
621        // ---- relationships ----------------------------------------------------
622        (Pos::Hop, Tok::Punct(p)) if p.starts_with('-') || p.starts_with('<') => {
623            s.incoming = p.starts_with('<');
624            s.star = None;
625            // `<` on its own is half an arrow: the hyphen is owed too.
626            s.owed = if *p == "<" { "-[:" } else { "[:" };
627            // `-->` and `--` are a whole relationship in one token: nothing
628            // bracketed follows them.
629            s.pos = if *p == "--" || *p == "-->" || *p == "<--" {
630                Pos::NodeOpen
631            } else {
632                Pos::RelOpen
633            };
634        }
635        (Pos::RelOpen, Tok::Punct("[")) => s.pos = Pos::RelVar,
636        (Pos::RelVar, Tok::Word(_)) => s.pos = Pos::RelColon,
637        (Pos::RelVar | Pos::RelColon, Tok::Punct(":")) => s.pos = Pos::RelType,
638        (Pos::RelType, Tok::Word(_)) => s.pos = Pos::RelClose,
639        (Pos::RelVar | Pos::RelColon | Pos::RelType | Pos::RelClose, Tok::Punct("]")) => {
640            s.pos = Pos::Arrow;
641        }
642        // The pieces of a `*1..3` range leave the position where it was, and
643        // say how much of the range is written.
644        (Pos::RelClose, Tok::Punct("*")) => s.star = Some(false),
645        (Pos::RelClose, Tok::Punct("..") | Tok::Num) => s.star = Some(true),
646        (Pos::Arrow, Tok::Punct(_)) => s.pos = Pos::NodeOpen,
647
648        // ---- clauses -----------------------------------------------------------
649        (Pos::Predicate | Pos::Projection | Pos::Value | Pos::Clause | Pos::Sort, Tok::Word(w)) => {
650            s.term = Some((*w).to_string());
651            s.pos = Pos::Term;
652        }
653        (Pos::Term, Tok::Punct(".")) => {
654            s.pos = Pos::Property(s.term.clone().unwrap_or_default());
655        }
656        (Pos::Property(_), Tok::Word(_)) => s.pos = Pos::Term,
657        // `key(`, `count(`, `collect(` — what follows the bracket is a term of
658        // its own, and the name before it was the call's, not a variable's:
659        // without this, the `.` in `collect(m.file)` would be read as a
660        // property of `collect`.
661        (Pos::Term, Tok::Punct("(")) => s.pos = Pos::Call,
662        (Pos::Call, Tok::Word(w)) => {
663            s.term = Some((*w).to_string());
664            s.pos = Pos::Term;
665        }
666        (Pos::Call, Tok::Punct("*")) => s.pos = Pos::Term,
667        (Pos::Term, Tok::Punct(p)) if is_comparison(p) => s.pos = Pos::Value,
668        (Pos::Term, Tok::Punct(",")) => s.pos = Pos::Projection,
669        (Pos::Value, Tok::Str | Tok::Num) => s.pos = Pos::Clause,
670
671        _ => {}
672    }
673}
674
675fn is_comparison(p: &str) -> bool {
676    matches!(p, "=" | "<>" | "!=" | "<" | "<=" | ">" | ">=")
677}
678
679// ---- suggesting -----------------------------------------------------------------
680
681/// The keywords a statement may open with, reads before writes: far more
682/// queries read.
683const OPENERS: &[(&str, &str)] = &[
684    ("MATCH", "walk a pattern"),
685    ("SEARCH", "a vector or keyword seed"),
686    ("HYBRID", "fused retrieval"),
687    ("CALL", "a graph algorithm"),
688    ("CREATE", "write nodes and edges"),
689    ("MERGE", "upsert by key"),
690];
691
692/// What may follow a complete pattern.
693const AFTER_PATTERN: &[(&str, &str)] =
694    &[("RETURN", "what to return"), ("WHERE", "filter the rows")];
695
696/// What may follow a complete projection.
697const TAIL: &[(&str, &str)] = &[
698    ("ORDER BY", "sort the rows"),
699    ("LIMIT", "cap the rows"),
700    ("SKIP", "drop the first n"),
701    ("AS", "name the column"),
702    ("AS OF", "read a past snapshot"),
703];
704
705/// What a predicate is built from, once its variable is written.
706const PREDICATE_TERMS: &[(&str, &str)] = &[
707    ("NOT", "negate"),
708    ("key(", "the external key"),
709    ("score()", "the retrieval score"),
710];
711
712/// The folds a `RETURN` may project with.
713const FOLDS: &[(&str, &str)] = &[
714    ("count(*)", "how many rows"),
715    ("collect(", "gather into a list"),
716    ("sum(", "total"),
717    ("avg(", "mean"),
718    ("min(", "least"),
719    ("max(", "greatest"),
720];
721
722fn suggest(
723    expects: &Expect,
724    word: &str,
725    vocab: &Vocab,
726    loose: bool,
727    closing: bool,
728) -> Vec<Suggestion> {
729    match expects {
730        Expect::Nothing | Expect::Value => Vec::new(),
731        Expect::Statement => keywords(OPENERS, word),
732        Expect::Node { var } => node_patterns(word, vocab, var),
733        // A variable is the author's to name; there is nothing to complete
734        // once they have started one.
735        Expect::NodeVar { var } => at_boundary(word, var, "a name for this node"),
736        Expect::Label => labels(word, vocab, ")"),
737        // A node that has its label (or its map) takes no second one.
738        Expect::NodeEnd if closing => at_boundary(word, ")", "close the node"),
739        Expect::NodeEnd => node_ends(word, vocab),
740        Expect::Hop { from, var, bound } => hops(word, vocab, from.as_deref(), var, *bound),
741        Expect::EdgeType {
742            from,
743            var,
744            incoming,
745            lead,
746        } => {
747            // A word typed where the bracket has not been opened yet is not
748            // the start of a type — there is nowhere for it to go — so
749            // nothing completes it.
750            if lead.contains('[') && !word.is_empty() {
751                return Vec::new();
752            }
753            edge_types(word, vocab, from.as_deref(), var, *incoming, lead)
754        }
755        Expect::RelEnd { .. } | Expect::Arrow { .. } if loose => Vec::new(),
756        Expect::RelEnd { incoming, ranged } => {
757            let close = if *incoming { "]-" } else { "]->" };
758            // A `*` with no bounds is the one variable-length shape the
759            // compiler refuses, so what follows one is a bound, not a bracket.
760            if *ranged {
761                at_boundary(word, &format!("1..3{close}"), "how many hops to walk")
762            } else {
763                at_boundary(word, close, "close the relationship")
764            }
765        }
766        Expect::Arrow { incoming } => {
767            let arrow = if *incoming { "-" } else { "->" };
768            at_boundary(word, arrow, "close the relationship")
769        }
770        Expect::Predicate { vars } => {
771            let mut out = variables(vars, word);
772            out.extend(keywords(PREDICATE_TERMS, word));
773            out
774        }
775        Expect::Projection { vars } => {
776            // The last variable bound, first: a pattern's rows *are* its
777            // terminal node, and returning an earlier variable's rows is the
778            // one thing the compiler refuses (an earlier one still projects,
779            // `RETURN p.name`).
780            let terminal: Vec<String> = vars.iter().rev().cloned().collect();
781            let mut out = variables(&terminal, word);
782            out.extend(keys_of(&terminal, word));
783            out.extend(keywords(FOLDS, word));
784            out
785        }
786        // A sort is by a value, and the value a node is usually sorted by is
787        // the name it goes under.
788        Expect::SortKey { vars } => {
789            let terminal: Vec<String> = vars.iter().rev().cloned().collect();
790            let mut out = keys_of(&terminal, word);
791            out.extend(variables(&terminal, word));
792            out.extend(keywords(FOLDS, word));
793            out
794        }
795        Expect::Argument { vars } => {
796            let terminal: Vec<String> = vars.iter().rev().cloned().collect();
797            variables(&terminal, word)
798        }
799        Expect::Property { label, .. } => properties(word, vocab, label.as_deref()),
800        Expect::Clause { done } => keywords(TAIL, word)
801            .into_iter()
802            .filter(|s| !done.iter().any(|d| d.eq_ignore_ascii_case(&s.text)))
803            .collect(),
804    }
805}
806
807/// A fixed piece of syntax, offered only where the caret is at a boundary or
808/// part-way through that very text — never appended to a word it does not
809/// continue.
810fn at_boundary(word: &str, text: &str, detail: &str) -> Vec<Suggestion> {
811    if !text.starts_with(word) || text.len() == word.len() {
812        return Vec::new();
813    }
814    vec![Suggestion {
815        insert: text[word.len()..].to_string(),
816        text: text.to_string(),
817        detail: Some(detail.to_string()),
818        kind: Kind::Snippet,
819    }]
820}
821
822/// A keyword list, filtered by what is typed and cased to match it: a query
823/// written in lower case stays in lower case, since the compiler folds case
824/// and `matCH` would be nobody's idea of a completion.
825fn keywords(list: &[(&str, &str)], word: &str) -> Vec<Suggestion> {
826    list.iter()
827        .filter(|(kw, _)| starts_with(kw, word))
828        .map(|(kw, detail)| {
829            let text = fit_case(kw, word);
830            Suggestion {
831                insert: text[word.len()..].to_string(),
832                text,
833                detail: Some((*detail).to_string()),
834                kind: Kind::Keyword,
835            }
836        })
837        .collect()
838}
839
840/// A keyword in the case the query is being written in. Only keywords are
841/// re-cased: a label or a property is data, and its spelling is not ours to
842/// change.
843fn fit_case(keyword: &str, word: &str) -> String {
844    let lower = !word.is_empty() && word.chars().all(|c| !c.is_alphabetic() || c.is_lowercase());
845    if lower {
846        keyword.to_ascii_lowercase()
847    } else {
848        keyword.to_string()
849    }
850}
851
852/// `key(n)` for each bound variable: the external key is what a node is called,
853/// and a table of nodes is nearly always a table of what they are called.
854fn keys_of(vars: &[String], word: &str) -> Vec<Suggestion> {
855    vars.iter()
856        .map(|v| format!("key({v})"))
857        .filter(|k| starts_with(k, word))
858        .map(|k| Suggestion {
859            insert: k[word.len()..].to_string(),
860            text: k,
861            detail: Some("what it is called".to_string()),
862            kind: Kind::Snippet,
863        })
864        .collect()
865}
866
867/// The variables the pattern bound, which is what a predicate or a projection
868/// is written about.
869fn variables(vars: &[String], word: &str) -> Vec<Suggestion> {
870    vars.iter()
871        .filter(|v| starts_with(v, word))
872        .map(|v| Suggestion {
873            insert: v[word.len()..].to_string(),
874            text: v.clone(),
875            detail: Some("a bound node".to_string()),
876            kind: Kind::Variable,
877        })
878        .collect()
879}
880
881/// Labels, most nodes first. `trailer` finishes the position — a label inside
882/// `(n:` is followed by the `)` that closes it.
883fn labels(word: &str, vocab: &Vocab, trailer: &str) -> Vec<Suggestion> {
884    let mut hits: Vec<&LabelInfo> = vocab
885        .labels
886        .iter()
887        .filter(|l| starts_with(&l.name, word))
888        .collect();
889    hits.sort_by_key(|l| (std::cmp::Reverse(l.count), l.name.clone()));
890    hits.iter()
891        .map(|l| Suggestion {
892            insert: format!("{}{trailer}", &l.name[word.len()..]),
893            text: l.name.clone(),
894            detail: Some(nodes(l.count)),
895            kind: Kind::Label,
896        })
897        .collect()
898}
899
900/// After `MATCH `: the whole node pattern, one per label the plane holds. With
901/// no label to name — an empty plane, or one nobody has digested — the only
902/// honest suggestion is `(n)`, which matches everything.
903fn node_patterns(word: &str, vocab: &Vocab, var: &str) -> Vec<Suggestion> {
904    if !word.is_empty() {
905        return Vec::new();
906    }
907    if vocab.labels.is_empty() {
908        return at_boundary(word, &format!("({var})"), "any node");
909    }
910    ranked_labels(vocab)
911        .iter()
912        .map(|l| {
913            let text = format!("({var}:{})", l.name);
914            Suggestion {
915                insert: text.clone(),
916                text,
917                detail: Some(nodes(l.count)),
918                kind: Kind::Snippet,
919            }
920        })
921        .collect()
922}
923
924/// After `(n`: the label that narrows it, or the `)` that ends it.
925fn node_ends(word: &str, vocab: &Vocab) -> Vec<Suggestion> {
926    if !word.is_empty() {
927        return Vec::new();
928    }
929    let mut out: Vec<Suggestion> = ranked_labels(vocab)
930        .iter()
931        .map(|l| {
932            let text = format!(":{})", l.name);
933            Suggestion {
934                insert: text.clone(),
935                text,
936                detail: Some(nodes(l.count)),
937                kind: Kind::Snippet,
938            }
939        })
940        .collect();
941    out.extend(at_boundary(word, ")", "any node"));
942    out
943}
944
945/// Edge types leaving (or, `incoming`, entering) a label, most edges first,
946/// written as the whole rest of the hop: whatever punctuation is still owed
947/// before the type, the type, the bracket that closes it, and the node it
948/// lands on — labelled with where such edges most often land.
949fn edge_types(
950    word: &str,
951    vocab: &Vocab,
952    from: Option<&str>,
953    var: &str,
954    incoming: bool,
955    lead: &str,
956) -> Vec<Suggestion> {
957    // With punctuation still owed, the word before the caret is not part of
958    // the type's name — it is the relationship's own variable, `-[r` — so the
959    // type is written whole after it rather than completed from it.
960    let typed = if lead.is_empty() { word } else { "" };
961    ranked_edges(vocab, from, incoming)
962        .iter()
963        .filter(|(e, _, _)| starts_with(&e.name, typed))
964        .map(|(e, count, dst)| Suggestion {
965            insert: format!(
966                "{lead}{}{}",
967                &e.name[typed.len()..],
968                hop_tail(*dst, var, incoming)
969            ),
970            text: e.name.clone(),
971            detail: Some(edge_detail(*count, *dst)),
972            kind: Kind::EdgeType,
973        })
974        .collect()
975}
976
977/// After a closed node: the hop that leaves it, and the clauses that would end
978/// the pattern instead.
979///
980/// Both directions, ranked together, because plenty of labels are only ever
981/// arrived at — an `UnresolvedRef` is called and calls nothing — and offering
982/// such a node no hop at all is offering it nothing it wants. With no label
983/// to go on there is no direction to tell apart either, so an unlabelled node
984/// gets the outgoing form alone rather than each type twice.
985///
986/// The hop comes first while the pattern is one node long, because one more
987/// hop is the reason to be writing a pattern at all; past that the reverse is
988/// true, since a two-hop pattern is already a question and a third hop is
989/// rarely one.
990fn hops(word: &str, vocab: &Vocab, from: Option<&str>, var: &str, bound: usize) -> Vec<Suggestion> {
991    let mut shapes = Vec::new();
992    if word.is_empty() {
993        let out = ranked_edges(vocab, from, false)
994            .into_iter()
995            .map(|(e, count, dst)| (e, count, dst, false));
996        let back = from
997            .map(|_| ranked_edges(vocab, from, true))
998            .unwrap_or_default()
999            .into_iter()
1000            .map(|(e, count, dst)| (e, count, dst, true));
1001        let mut both: Vec<_> = out.chain(back).collect();
1002        both.sort_by_key(|(e, count, _, incoming)| {
1003            (std::cmp::Reverse(*count), *incoming, e.name.clone())
1004        });
1005        shapes.extend(both.iter().map(|(e, count, dst, incoming)| {
1006            let arrow = if *incoming { "<-" } else { "-" };
1007            let text = format!("{arrow}[:{}{}", e.name, hop_tail(*dst, var, *incoming));
1008            Suggestion {
1009                insert: text.clone(),
1010                text,
1011                detail: Some(edge_detail(*count, *dst)),
1012                kind: Kind::Snippet,
1013            }
1014        }));
1015    }
1016    let mut clauses = keywords(AFTER_PATTERN, word);
1017    if bound > 1 {
1018        clauses.extend(shapes);
1019        return clauses;
1020    }
1021    shapes.extend(clauses);
1022    shapes
1023}
1024
1025/// What closes a hop: the bracket, the arrow, and the node it lands on.
1026fn hop_tail(dst: Option<&str>, var: &str, incoming: bool) -> String {
1027    let close = if incoming { "]-" } else { "]->" };
1028    match dst {
1029        Some(dst) => format!("{close}({var}:{dst})"),
1030        None => format!("{close}({var})"),
1031    }
1032}
1033
1034fn edge_detail(count: u64, dst: Option<&str>) -> String {
1035    match dst {
1036        Some(dst) => format!("{count} → {dst}"),
1037        None => format!("{count} edges"),
1038    }
1039}
1040
1041fn nodes(count: u64) -> String {
1042    format!("{count} node{}", if count == 1 { "" } else { "s" })
1043}
1044
1045fn ranked_labels(vocab: &Vocab) -> Vec<&LabelInfo> {
1046    let mut out: Vec<&LabelInfo> = vocab.labels.iter().collect();
1047    out.sort_by_key(|l| (std::cmp::Reverse(l.count), l.name.clone()));
1048    out
1049}
1050
1051/// One end of a connection: which label a hop starts at, or lands on.
1052type End = fn(&Connection) -> &String;
1053
1054/// Every edge type that can leave (or, `incoming`, enter) `from`, with how
1055/// many such edges there are and the label they most often reach.
1056///
1057/// With no label to go on, every type counts, ranked by how many edges the
1058/// plane holds of it — which is the best a pattern with an unlabelled node can
1059/// be told.
1060fn ranked_edges<'a>(
1061    vocab: &'a Vocab,
1062    from: Option<&str>,
1063    incoming: bool,
1064) -> Vec<(&'a EdgeInfo, u64, Option<&'a str>)> {
1065    let mut out: Vec<(&EdgeInfo, u64, Option<&str>)> = vocab
1066        .edges
1067        .iter()
1068        .filter_map(|e| {
1069            let Some(from) = from else {
1070                return Some((e, e.count, None));
1071            };
1072            // Which end of a connection the label sits at, and which end the
1073            // hop lands on, swap with the arrow's direction.
1074            let (here, there): (End, End) = if incoming {
1075                (|c| &c.dst, |c| &c.src)
1076            } else {
1077                (|c| &c.src, |c| &c.dst)
1078            };
1079            let mut total = 0;
1080            let mut best: Option<(&str, u64)> = None;
1081            for c in e.connections.iter().filter(|c| here(c) == from) {
1082                total += c.count;
1083                if best.is_none_or(|(_, n)| c.count > n) {
1084                    best = Some((there(c).as_str(), c.count));
1085                }
1086            }
1087            (total > 0).then_some((e, total, best.map(|(l, _)| l)))
1088        })
1089        .collect();
1090    out.sort_by_key(|(e, count, _)| (std::cmp::Reverse(*count), e.name.clone()));
1091    out
1092}
1093
1094/// The properties a label's nodes hold. With no label — an unlabelled node —
1095/// every label's properties are fair game, since any of them could be it.
1096fn properties(word: &str, vocab: &Vocab, label: Option<&str>) -> Vec<Suggestion> {
1097    let mut names: Vec<&str> = Vec::new();
1098    for l in &vocab.labels {
1099        if label.is_none_or(|want| want == l.name) {
1100            for p in &l.properties {
1101                if starts_with(p, word) && !names.contains(&p.as_str()) {
1102                    names.push(p);
1103                }
1104            }
1105        }
1106    }
1107    names
1108        .iter()
1109        .map(|p| Suggestion {
1110            insert: p[word.len()..].to_string(),
1111            text: (*p).to_string(),
1112            detail: label.map(|l| format!("of {l}")),
1113            kind: Kind::Property,
1114        })
1115        .collect()
1116}
1117
1118/// Case-insensitive prefix match, with something left to add: a label is
1119/// `Function` however it is being typed, and a word already complete is not a
1120/// suggestion.
1121fn starts_with(candidate: &str, word: &str) -> bool {
1122    candidate.len() > word.len()
1123        && candidate
1124            .get(..word.len())
1125            .is_some_and(|head| head.eq_ignore_ascii_case(word))
1126}