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}