LCOV - code coverage report
Current view: top level - dashql-core/src - completion.rs Coverage Total Hit
Test: DashQL 6e74948 Lines: 97.2 % 1240 1205
Test Date: 2026-11-06 17:46:50

            Line data    Source code
       1              : //! Completion at a script cursor.
       2              : //!
       3              : //! Completion collects candidates from the expected grammar symbols at the cursor (probed through
       4              : //! the LR automaton), the name search indexes of the script and the catalog, and the name scopes
       5              : //! around the cursor. Candidates are scored by tags and the top k are selected by score, ties are ranked
       6              : //! by name.
       7              : 
       8              : // Bison names some symbol kinds in mixed case (S_Op)
       9              : #![allow(non_upper_case_globals)]
      10              : 
      11              : use crate::analyzer::{AnalyzedScript, ColumnResolution, ReferencedTable, ScopeId, ScopeSource, SourceVisibility, TableRole};
      12              : use crate::catalog::*;
      13              : use crate::cursor::{CursorContext, NameComponentType, ScriptCursor, SymbolLocation};
      14              : use crate::hash::HashMap;
      15              : use crate::table_parameters::{TableParameterSource, TABLE_PARAMETERS_ENTRY_ID};
      16              : use crate::utils::topk::TopKHeap;
      17              : use crate::utils::{ci_cmp, ci_find, ci_starts_with};
      18              : use dashql_buffers::analyzer::NameTag;
      19              : use dashql_buffers::completion::{CandidateTag, CompletionStrategy};
      20              : use dashql_buffers::cursor::RelativeSymbolPosition;
      21              : use dashql_buffers::parser::{AttributeKey, NodeType, SymbolSpan};
      22              : use dashql_parser::keywords::Keyword;
      23              : use dashql_parser::parser::context::resolve_text_span;
      24              : use dashql_parser::parser::probe::{self, PrefixSnapshot};
      25              : use dashql_parser::scanner::Symbol;
      26              : use dashql_parser::symbol_kind::*;
      27              : use dashql_parser::SymbolKind;
      28              : use std::cmp::Ordering;
      29              : use std::sync::Arc;
      30              : 
      31              : /// A score value
      32              : pub type Score = u32;
      33              : 
      34              : // Keyword prevalence modifiers: "se" should suggest "select" before "set"
      35              : const KEYWORD_A: Score = 4;
      36              : const KEYWORD_B: Score = 3;
      37              : const KEYWORD_C: Score = 2;
      38              : const KEYWORD_D: Score = 0;
      39              : 
      40              : // Coarse base score of a registered name
      41              : const NAME_TAG_IGNORE: Score = 0;
      42              : const NAME_TAG_UNLIKELY: Score = 10;
      43              : const NAME_TAG_LIKELY: Score = 20;
      44              : 
      45              : // Fine-granular score modifiers
      46              : const SUBSTRING_SCORE_MODIFIER: Score = 30;
      47              : const PREFIX_SCORE_MODIFIER: Score = 5;
      48              : const EXACT_MATCH_SCORE_MODIFIER: Score = 15;
      49              : const KEYWORD_SUBSTRING_BONUS: Score = 20;
      50              : const THROUGH_CATALOG_SCORE_MODIFIER: Score = 2;
      51              : const RESOLVING_TABLE_SCORE_MODIFIER: Score = 5;
      52              : const UNRESOLVED_PEER_SCORE_MODIFIER: Score = 1;
      53              : const DOT_SCHEMA_SCORE_MODIFIER: Score = 2;
      54              : const DOT_TABLE_SCORE_MODIFIER: Score = 2;
      55              : const DOT_COLUMN_SCORE_MODIFIER: Score = 2;
      56              : const IN_NAME_SCOPE_SCORE_MODIFIER: Score = 10;
      57              : const IN_SAME_STATEMENT_SCORE_MODIFIER: Score = 1;
      58              : 
      59              : // Suffix-probe modifiers
      60              : const SUFFIX_INCOMPATIBLE_PENALTY: Score = 15;
      61              : const SUFFIX_DEPTH_ONE_BONUS: Score = 0;
      62              : const SUFFIX_DEPTH_MANY_BONUS: Score = 1;
      63              : 
      64              : // Identity candidate scores
      65              : const IDENTITY_SCORE_TABLE_REF: Score = 50;
      66              : const IDENTITY_SCORE_TABLE_REF_ALIAS: Score = 50;
      67              : const IDENTITY_SCORE_COLUMN_REF: Score = 59;
      68              : 
      69              : /// Longer identity candidates are dropped (e.g. an unterminated delimited identifier)
      70              : const MAX_IDENTITY_LENGTH: usize = 64;
      71              : 
      72              : /// The candidate tags, a bitset of `CandidateTag`
      73              : pub type CandidateTags = u32;
      74              : 
      75      1465843 : fn has(tags: CandidateTags, tag: CandidateTag) -> bool {
      76      1465843 :     tags & tag.0 != 0
      77      1465843 : }
      78              : 
      79              : /// `computeCandidateScore`
      80        73297 : fn compute_candidate_score(tags: CandidateTags) -> Score {
      81        73297 :     let mut score: Score = 0;
      82      1246049 :     let add = |score: &mut Score, tag: CandidateTag, value: Score| {
      83      1246049 :         if has(tags, tag) {
      84        92209 :             *score += value;
      85      1153840 :         }
      86      1246049 :     };
      87        73297 :     add(&mut score, CandidateTag::KEYWORD_D, KEYWORD_D);
      88        73297 :     add(&mut score, CandidateTag::KEYWORD_C, KEYWORD_C);
      89        73297 :     add(&mut score, CandidateTag::KEYWORD_B, KEYWORD_B);
      90        73297 :     add(&mut score, CandidateTag::KEYWORD_A, KEYWORD_A);
      91        73297 :     add(&mut score, CandidateTag::SUBSTRING_MATCH, SUBSTRING_SCORE_MODIFIER);
      92        73297 :     add(&mut score, CandidateTag::PREFIX_MATCH, PREFIX_SCORE_MODIFIER);
      93        73297 :     add(&mut score, CandidateTag::EXACT_MATCH, EXACT_MATCH_SCORE_MODIFIER);
      94        73297 :     if has(tags, CandidateTag::EXPECTED_PARSER_SYMBOL) && has(tags, CandidateTag::SUBSTRING_MATCH) {
      95         3727 :         score += KEYWORD_SUBSTRING_BONUS;
      96        69570 :     }
      97        73297 :     add(&mut score, CandidateTag::THROUGH_CATALOG, THROUGH_CATALOG_SCORE_MODIFIER);
      98        73297 :     add(&mut score, CandidateTag::RESOLVING_TABLE, RESOLVING_TABLE_SCORE_MODIFIER);
      99        73297 :     add(&mut score, CandidateTag::UNRESOLVED_PEER, UNRESOLVED_PEER_SCORE_MODIFIER);
     100        73297 :     add(&mut score, CandidateTag::DOT_RESOLUTION_TABLE, DOT_TABLE_SCORE_MODIFIER);
     101        73297 :     add(&mut score, CandidateTag::DOT_RESOLUTION_SCHEMA, DOT_SCHEMA_SCORE_MODIFIER);
     102        73297 :     add(&mut score, CandidateTag::DOT_RESOLUTION_COLUMN, DOT_COLUMN_SCORE_MODIFIER);
     103        73297 :     add(&mut score, CandidateTag::IN_NAME_SCOPE, IN_NAME_SCOPE_SCORE_MODIFIER);
     104        73297 :     add(&mut score, CandidateTag::IN_SAME_STATEMENT, IN_SAME_STATEMENT_SCORE_MODIFIER);
     105        73297 :     add(&mut score, CandidateTag::SUFFIX_DEPTH_ONE, SUFFIX_DEPTH_ONE_BONUS);
     106        73297 :     add(&mut score, CandidateTag::SUFFIX_DEPTH_MANY, SUFFIX_DEPTH_MANY_BONUS);
     107        73297 :     let penalty = if has(tags, CandidateTag::SUFFIX_INCOMPATIBLE) { SUFFIX_INCOMPATIBLE_PENALTY } else { 0 };
     108        73297 :     score.saturating_sub(penalty)
     109        73297 : }
     110              : 
     111              : /// The base scores of the name tags per strategy
     112          445 : fn name_tag_scores(strategy: CompletionStrategy) -> [(NameTag, Score); 7] {
     113          445 :     match strategy {
     114          123 :         CompletionStrategy::TABLE_REF => [
     115          123 :             (NameTag::NONE, NAME_TAG_IGNORE),
     116          123 :             (NameTag::SCHEMA_NAME, NAME_TAG_LIKELY),
     117          123 :             (NameTag::DATABASE_NAME, NAME_TAG_LIKELY),
     118          123 :             (NameTag::TABLE_NAME, NAME_TAG_LIKELY),
     119          123 :             (NameTag::TABLE_ALIAS, NAME_TAG_UNLIKELY),
     120          123 :             (NameTag::COLUMN_NAME, NAME_TAG_UNLIKELY),
     121          123 :             (NameTag::FUNCTION_NAME, NAME_TAG_UNLIKELY),
     122          123 :         ],
     123          108 :         CompletionStrategy::COLUMN_REF => [
     124          108 :             (NameTag::NONE, NAME_TAG_IGNORE),
     125          108 :             (NameTag::SCHEMA_NAME, NAME_TAG_UNLIKELY),
     126          108 :             (NameTag::DATABASE_NAME, NAME_TAG_UNLIKELY),
     127          108 :             (NameTag::TABLE_NAME, NAME_TAG_UNLIKELY),
     128          108 :             (NameTag::TABLE_ALIAS, NAME_TAG_LIKELY),
     129          108 :             (NameTag::COLUMN_NAME, NAME_TAG_LIKELY),
     130          108 :             (NameTag::FUNCTION_NAME, NAME_TAG_UNLIKELY),
     131          108 :         ],
     132          214 :         _ => [
     133          214 :             (NameTag::NONE, NAME_TAG_IGNORE),
     134          214 :             (NameTag::SCHEMA_NAME, NAME_TAG_LIKELY),
     135          214 :             (NameTag::DATABASE_NAME, NAME_TAG_LIKELY),
     136          214 :             (NameTag::TABLE_NAME, NAME_TAG_LIKELY),
     137          214 :             (NameTag::TABLE_ALIAS, NAME_TAG_LIKELY),
     138          214 :             (NameTag::COLUMN_NAME, NAME_TAG_LIKELY),
     139          214 :             (NameTag::FUNCTION_NAME, NAME_TAG_LIKELY),
     140          214 :         ],
     141              :     }
     142          445 : }
     143              : 
     144              : /// The prevalence of a keyword
     145        72930 : fn keyword_prevalence(keyword: SymbolKind) -> CandidateTag {
     146        72930 :     match keyword {
     147          349 :         S_SELECT | S_FROM | S_WHERE => CandidateTag::KEYWORD_A,
     148          457 :         S_AND | S_GROUP_P | S_ORDER => CandidateTag::KEYWORD_B,
     149              :         S_AS | S_ASC_P | S_BY | S_CASE | S_CAST | S_DESC_P | S_END_P | S_LIKE | S_LIMIT | S_OFFSET | S_OR | S_SET
     150         1394 :         | S_THEN | S_WHEN | S_WITH => CandidateTag::KEYWORD_C,
     151        70730 :         _ => CandidateTag::KEYWORD_D,
     152              :     }
     153        72930 : }
     154              : 
     155              : /// Tokens after which no passive keyword hints are shown
     156           51 : fn suppress_passive_hint(kind: SymbolKind) -> bool {
     157           51 :     matches!(kind, S_SELECT | S_FROM | S_JOIN | S_WHERE | S_ON | S_AND | S_OR | S_BY)
     158           51 : }
     159              : 
     160              : /// The continuation score of a keyword
     161      1093346 : fn keyword_continuation_score(kind: SymbolKind) -> u8 {
     162      1093346 :     match kind {
     163         9169 :         S_BY | S_AS | S_EQUALS_GREATER | S_ON | S_SELECT | S_INTO | S_TABLE | S_SET => 10,
     164              :         S_OUTER_P | S_ASC_P | S_DESC_P | S_NULLS_P | S_FIRST_P | S_LAST_P | S_UNBOUNDED | S_PRECEDING | S_CURRENT_P
     165        25864 :         | S_ROW | S_WITHIN | S_SETS => 6,
     166      1058313 :         _ => 0,
     167              :     }
     168      1093346 : }
     169              : 
     170              : /// Is a scanner symbol kind completable?
     171          467 : pub fn is_symbol_kind_completable(kind: SymbolKind) -> bool {
     172          422 :     !matches!(
     173          467 :         kind,
     174              :         S_SCONST
     175              :             | S_ICONST
     176              :             | S_FCONST
     177              :             | S_BCONST
     178              :             | S_XCONST
     179              :             | S_COMMA
     180              :             | S_LRB
     181              :             | S_RRB
     182              :             | S_LSB
     183              :             | S_RSB
     184              :             | S_SEMICOLON
     185              :             | S_COLON
     186              :             | S_PLUS
     187              :             | S_MINUS
     188              :             | S_STAR
     189              :             | S_DIVIDE
     190              :             | S_MODULO
     191              :             | S_QUESTION_MARK
     192              :             | S_PARAM
     193              :             | S_CIRCUMFLEX
     194              :             | S_LESS_THAN
     195              :             | S_GREATER_THAN
     196              :             | S_EQUALS
     197              :             | S_Op
     198              :             | S_EQUALS_GREATER
     199              :             | S_LESS_EQUALS
     200              :             | S_GREATER_EQUALS
     201              :             | S_NOT_EQUALS
     202              :             | S_DOT_STAR_LA
     203              :     )
     204          467 : }
     205              : 
     206              : /// The catalog object of a candidate
     207              : #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     208              : pub enum CandidateObjectRef {
     209              :     /// A database, by its catalog id (script reference or catalog declaration)
     210              :     Database(QualifiedCatalogObjectId),
     211              :     /// A schema, by its catalog id
     212              :     Schema(QualifiedCatalogObjectId),
     213              :     Table(TableLoc),
     214              :     Column(ColumnLoc),
     215              :     Function(ObjectLoc),
     216              : }
     217              : 
     218              : impl CandidateObjectRef {
     219          134 :     pub fn object_type(&self) -> CatalogObjectType {
     220          134 :         match self {
     221           12 :             CandidateObjectRef::Database(_) => CatalogObjectType::DatabaseReference,
     222           27 :             CandidateObjectRef::Schema(_) => CatalogObjectType::SchemaReference,
     223           56 :             CandidateObjectRef::Table(_) => CatalogObjectType::TableDeclaration,
     224           35 :             CandidateObjectRef::Column(_) => CatalogObjectType::ColumnDeclaration,
     225            4 :             CandidateObjectRef::Function(_) => CatalogObjectType::FunctionDeclaration,
     226              :         }
     227          134 :     }
     228              : }
     229              : 
     230              : /// A catalog object of a candidate
     231              : #[derive(Clone, Debug)]
     232              : pub struct CandidateObject {
     233              :     /// The candidate (index into the candidate buffer)
     234              :     pub candidate: usize,
     235              :     /// The candidate tags of the object
     236              :     pub candidate_tags: CandidateTags,
     237              :     /// The qualified id of the catalog object
     238              :     pub catalog_object_id: QualifiedCatalogObjectId,
     239              :     /// The catalog object
     240              :     pub object: CandidateObjectRef,
     241              :     /// The score, computed from the candidate tags
     242              :     pub score: Score,
     243              :     /// The qualified name
     244              :     pub qualified_name: Vec<Arc<str>>,
     245              :     /// The index of the target name in the qualified name, 0 if the qualified name is empty
     246              :     pub qualified_name_target_idx: usize,
     247              :     /// Whether to prefer the qualified name, set for ambiguous names and aliased tables
     248              :     pub prefer_qualified: bool,
     249              : }
     250              : 
     251              : /// A completion candidate
     252              : #[derive(Clone, Debug)]
     253              : pub struct Candidate {
     254              :     /// The text to insert
     255              :     pub completion_text: Arc<str>,
     256              :     /// Identity candidates are emitted as typed, without quoting
     257              :     pub completion_text_is_verbatim: bool,
     258              :     /// The combined analyzer tags of the name
     259              :     pub coarse_name_tags: u8,
     260              :     /// The combined candidate tags
     261              :     pub candidate_tags: CandidateTags,
     262              :     /// The text range to replace
     263              :     pub target_location: SymbolSpan,
     264              :     /// The text range to replace with a qualified name
     265              :     pub target_location_qualified: SymbolSpan,
     266              :     /// The catalog objects (indices into the candidate objects)
     267              :     pub catalog_objects: Vec<usize>,
     268              :     /// The score, higher is better
     269              :     pub score: Score,
     270              :     /// The keyword symbol of keyword candidates
     271              :     pub keyword_symbol: Option<SymbolKind>,
     272              :     /// The keyword continuation (e.g. "by")
     273              :     pub keyword_continuation: Option<&'static str>,
     274              : }
     275              : 
     276              : impl Candidate {
     277        73544 :     pub(crate) fn new(
     278        73544 :         completion_text: Arc<str>,
     279        73544 :         name_tags: u8,
     280        73544 :         tags: CandidateTags,
     281        73544 :         location: SymbolSpan,
     282        73544 :         qualified: SymbolSpan,
     283        73544 :     ) -> Self {
     284        73544 :         Self {
     285        73544 :             completion_text,
     286        73544 :             completion_text_is_verbatim: false,
     287        73544 :             coarse_name_tags: name_tags,
     288        73544 :             candidate_tags: tags,
     289        73544 :             target_location: location,
     290        73544 :             target_location_qualified: qualified,
     291        73544 :             catalog_objects: Vec::new(),
     292        73544 :             score: 0,
     293        73544 :             keyword_symbol: None,
     294        73544 :             keyword_continuation: None,
     295        73544 :         }
     296        73544 :     }
     297              : }
     298              : 
     299              : /// The rank of candidates, greater is better: higher scores, then smaller names in case-insensitive order, then in
     300              : /// byte order. Names are unique among the candidates of the name indexes, but a keyword or the identity candidate
     301              : /// may spell the same; keywords rank before names, the rest by their tags.
     302       149645 : fn candidate_cmp(l: &Candidate, r: &Candidate) -> Ordering {
     303       149645 :     l.score
     304       149645 :         .cmp(&r.score)
     305       149645 :         .then_with(|| ci_cmp(r.completion_text.as_bytes(), l.completion_text.as_bytes()))
     306       149645 :         .then_with(|| r.completion_text.cmp(&l.completion_text))
     307       149645 :         .then_with(|| l.keyword_symbol.is_some().cmp(&r.keyword_symbol.is_some()))
     308       149645 :         .then_with(|| r.keyword_symbol.cmp(&l.keyword_symbol))
     309       149645 :         .then_with(|| r.candidate_tags.cmp(&l.candidate_tags))
     310       149645 : }
     311              : 
     312              : type CandidateHeap = TopKHeap<Candidate, fn(&Candidate, &Candidate) -> Ordering>;
     313              : 
     314              : /// A completion at a cursor
     315              : pub struct Completion<'c, 'a> {
     316              :     /// The cursor to complete at
     317              :     pub cursor: &'c ScriptCursor<'a>,
     318              :     /// The completion strategy, derived from the cursor context
     319              :     pub strategy: CompletionStrategy,
     320              :     /// Is the target qualified?
     321              :     pub dot_completion: bool,
     322              :     /// Is the cursor at a name definition?
     323              :     pub at_definition: bool,
     324              :     /// Is the cursor between symbols?
     325              :     pub between_symbols: bool,
     326              :     /// Is there a real token after the cursor?
     327              :     pub has_post_cursor_token: bool,
     328              :     /// The symbol being completed
     329              :     pub target_symbol: Option<SymbolLocation>,
     330              :     /// The collected candidates, one per name
     331              :     pub candidates: Vec<Candidate>,
     332              :     /// The catalog objects of the candidates
     333              :     pub candidate_objects: Vec<CandidateObject>,
     334              :     /// The candidate indexes by completion text
     335              :     candidates_by_name: HashMap<Arc<str>, usize>,
     336              :     /// The candidate object indexes by catalog object id
     337              :     candidate_objects_by_id: HashMap<QualifiedCatalogObjectId, usize>,
     338              :     /// The heap that selects the top candidates
     339              :     candidate_heap: CandidateHeap,
     340              :     /// The selected candidates, best first
     341              :     pub top_candidates: Vec<Candidate>,
     342              : }
     343              : 
     344              : /// A dot candidate
     345              : struct DotCandidate {
     346              :     /// The name to complete
     347              :     name: Arc<str>,
     348              :     /// The candidate tags
     349              :     candidate_tags: CandidateTags,
     350              :     /// The analyzer tags of the name
     351              :     name_tags: u8,
     352              :     /// The qualified id of the catalog object
     353              :     object_id: QualifiedCatalogObjectId,
     354              :     /// The catalog object the name refers to
     355              :     object: CandidateObjectRef,
     356              : }
     357              : 
     358              : /// Trim double quotes at both ends
     359          721 : fn trim_double_quotes(s: &[u8]) -> &[u8] {
     360          727 :     let begin = s.iter().position(|&c| c != b'"').unwrap_or(s.len());
     361          721 :     let end = s.iter().rposition(|&c| c != b'"').map(|e| e + 1).unwrap_or(begin).max(begin);
     362          721 :     &s[begin..end]
     363          721 : }
     364              : 
     365              : /// Lossless text of bytes that came from the script text at char boundaries
     366          174 : fn text_arc(bytes: &[u8]) -> Arc<str> {
     367          174 :     Arc::from(String::from_utf8_lossy(bytes).as_ref())
     368          174 : }
     369              : 
     370              : /// The match tags of a name against a prefix, None if the prefix is not a substring
     371        69222 : fn match_tags(name: &[u8], prefix: &[u8]) -> Option<CandidateTags> {
     372        69222 :     let pos = ci_find(name, prefix)?;
     373         3802 :     let mut tags = CandidateTag::SUBSTRING_MATCH.0;
     374         3802 :     if pos == 0 {
     375          898 :         tags |= CandidateTag::PREFIX_MATCH.0;
     376          898 :         if name.len() == prefix.len() {
     377           37 :             tags |= CandidateTag::EXACT_MATCH.0;
     378          861 :         }
     379         2904 :     }
     380         3802 :     Some(tags)
     381        69222 : }
     382              : 
     383              : impl<'c, 'a> Completion<'c, 'a> {
     384          499 :     fn new(cursor: &'c ScriptCursor<'a>, k: usize) -> Self {
     385          499 :         let strategy = match cursor.context {
     386           22 :             CursorContext::TableRef { at_alias: true, .. } => CompletionStrategy::TABLE_REF_ALIAS,
     387          121 :             CursorContext::TableRef { .. } => CompletionStrategy::TABLE_REF,
     388          108 :             CursorContext::ColumnRef { .. } | CursorContext::AliasDot => CompletionStrategy::COLUMN_REF,
     389          248 :             CursorContext::None => CompletionStrategy::DEFAULT,
     390              :         };
     391          499 :         Self {
     392          499 :             cursor,
     393          499 :             strategy,
     394          499 :             dot_completion: false,
     395          499 :             at_definition: false,
     396          499 :             between_symbols: false,
     397          499 :             has_post_cursor_token: false,
     398          499 :             target_symbol: None,
     399          499 :             candidates: Vec::new(),
     400          499 :             candidate_objects: Vec::new(),
     401          499 :             candidates_by_name: HashMap::default(),
     402          499 :             candidate_objects_by_id: HashMap::default(),
     403          499 :             candidate_heap: TopKHeap::new(k, candidate_cmp as fn(&Candidate, &Candidate) -> Ordering),
     404          499 :             top_candidates: Vec::new(),
     405          499 :         }
     406          499 :     }
     407              : 
     408         3446 :     fn symbols(&self) -> &'a [Symbol] {
     409         3446 :         &self.cursor.script.scanned.unwrap().symbols
     410         3446 :     }
     411              : 
     412         1206 :     fn text(&self) -> &'a str {
     413         1206 :         &self.cursor.script.scanned.unwrap().text
     414         1206 :     }
     415              : 
     416         2057 :     fn analyzed(&self) -> Option<&'a Arc<AnalyzedScript>> {
     417         2057 :         self.cursor.script.analyzed
     418         2057 :     }
     419              : 
     420              :     /// The catalog snapshot of the analysis
     421         1052 :     fn catalog(&self) -> &'a CatalogSnapshot {
     422         1052 :         self.cursor.script.catalog()
     423         1052 :     }
     424              : 
     425              :     /// The text at a text range, clamped to the script text
     426         1206 :     fn text_at(&self, offset: u32, length: u32) -> &'a [u8] {
     427         1206 :         let text = self.text().as_bytes();
     428         1206 :         let begin = (offset as usize).min(text.len());
     429         1206 :         let end = (begin + length as usize).min(text.len());
     430         1206 :         &text[begin..end]
     431         1206 :     }
     432              : 
     433              :     /// The location of a symbol as a text-based SymbolSpan
     434          816 :     fn symbol_location(symbol: &Symbol) -> SymbolSpan {
     435          816 :         SymbolSpan::new(symbol.location.offset, symbol.location.length)
     436          816 :     }
     437              : 
     438              :     /// The data of a catalog entry: the analyzed script itself or a catalog entry
     439          728 :     pub fn entry_data(&self, entry: CatalogEntryId) -> Option<&'a CatalogEntryData> {
     440          728 :         if let Some(analyzed) = self.analyzed() {
     441          728 :             if analyzed.entry.catalog_entry_id == entry {
     442          131 :                 return Some(&analyzed.entry);
     443          597 :             }
     444            0 :         }
     445          597 :         self.catalog().entry(entry).map(|e| e.data())
     446          728 :     }
     447              : 
     448          500 :     pub fn table(&self, loc: TableLoc) -> Option<&'a TableDeclaration> {
     449          500 :         if loc.entry == TABLE_PARAMETERS_ENTRY_ID {
     450            8 :             return self.analyzed()?.table(loc);
     451          492 :         }
     452          492 :         self.entry_data(loc.entry)?.table_declarations.get(loc.index as usize)
     453          500 :     }
     454              : 
     455              :     /// What the table parameter of a table reads, None for the tables of the catalog
     456            3 :     pub fn table_parameter_source(&self, loc: TableLoc) -> Option<TableParameterSource> {
     457            3 :         self.analyzed()?.table_parameters.as_ref()?.source(loc)
     458            3 :     }
     459              : 
     460              :     /// Complete the name of a table parameter `:foo` in table position with the names of the scripts and attachments
     461              :     /// of the session that have columns. Returns false if the cursor is not at the name of a table parameter.
     462          471 :     fn complete_table_parameter(&mut self, location: &crate::cursor::LocationInfo) -> bool {
     463          471 :         let symbols = self.symbols();
     464          471 :         let offset = self.cursor.text_offset as u32;
     465          471 :         let ends_at = |s: &Symbol| s.location.offset + s.location.length == offset;
     466          548 :         let is_name = |kind: SymbolKind| kind == S_IDENT || kind == S_SCONST;
     467              :         // The colon and the name after it, if any
     468          471 :         let current = location.current;
     469          471 :         let (colon, name) = if current.symbol.kind == S_COLON && ends_at(&current.symbol) {
     470            1 :             (current.symbol_id, None)
     471          470 :         } else if is_name(current.symbol.kind)
     472          333 :             && current.symbol_id > 0
     473          276 :             && current.relative_pos != RelativeSymbolPosition::AFTER_SYMBOL
     474          263 :             && current.relative_pos != RelativeSymbolPosition::BEFORE_SYMBOL
     475              :         {
     476          263 :             (current.symbol_id - 1, Some(current.symbol))
     477              :         } else {
     478          201 :             match location.previous {
     479          201 :                 Some(p) if p.relative_pos == RelativeSymbolPosition::END_OF_SYMBOL && p.symbol.kind == S_COLON => (p.symbol_id, None),
     480          201 :                 Some(p) if p.relative_pos == RelativeSymbolPosition::END_OF_SYMBOL && is_name(p.symbol.kind) && p.symbol_id > 0 => {
     481           10 :                     (p.symbol_id - 1, Some(p.symbol))
     482              :                 }
     483          197 :                 _ => return false,
     484              :             }
     485              :         };
     486          274 :         let Some(colon_symbol) = symbols.get(colon as usize) else { return false };
     487          274 :         if colon_symbol.kind != S_COLON {
     488          270 :             return false;
     489            4 :         }
     490              :         // The name follows the colon right away
     491            4 :         if let Some(name) = name {
     492            3 :             if colon_symbol.location.offset + colon_symbol.location.length != name.location.offset {
     493            0 :                 return false;
     494            3 :             }
     495            1 :         }
     496              :         // A table position takes a join after the parameter, `where x = :y` and `insert into :t` take none
     497            4 :         let expected = probe::parse_until_after(symbols, colon + 1, S_IDENT);
     498            4 :         if !expected.contains(&S_JOIN) {
     499            1 :             return false;
     500            3 :         }
     501            3 :         let Some(results) = self.analyzed().and_then(|a| a.table_parameters.clone()) else { return true };
     502            3 :         let (prefix, target) = match name {
     503            2 :             Some(name) => {
     504            2 :                 let text = self.text_at(name.location.offset, offset.saturating_sub(name.location.offset));
     505            4 :                 (trim_double_quotes(text).iter().copied().filter(|&c| c != b'\'').collect::<Vec<u8>>(), Self::symbol_location(&name))
     506              :             }
     507            1 :             None => (Vec::new(), SymbolSpan::new(offset, 0)),
     508              :         };
     509            3 :         self.strategy = CompletionStrategy::TABLE_REF;
     510           11 :         for (index, script) in results.entries.iter().enumerate() {
     511           11 :             if script.table.is_none() || script.name.is_empty() {
     512            3 :                 continue;
     513            8 :             }
     514            8 :             let tags = if prefix.is_empty() {
     515            3 :                 CandidateTag::NONE.0
     516              :             } else {
     517            5 :                 match match_tags(script.name.as_bytes(), &prefix) {
     518            4 :                     Some(t) => t,
     519            1 :                     None => continue,
     520              :                 }
     521              :             };
     522            7 :             let candidate = self.candidates.len();
     523            7 :             self.candidates.push(Candidate::new(script.name.clone(), NameTag::TABLE_NAME.0, tags, target, target));
     524            7 :             self.candidates_by_name.insert(script.name.clone(), candidate);
     525            7 :             let loc = TableLoc { entry: TABLE_PARAMETERS_ENTRY_ID, index: index as u32 };
     526            7 :             let object_id = QualifiedCatalogObjectId::table(CatalogTableId::new(TABLE_PARAMETERS_ENTRY_ID, index as u32));
     527            7 :             self.add_object(candidate, tags, object_id, CandidateObjectRef::Table(loc));
     528              :         }
     529            3 :         self.select_top_candidates();
     530            3 :         true
     531          471 :     }
     532              : 
     533         1043 :     fn is_at_eof(&self, id: u32) -> bool {
     534         1043 :         id as usize >= self.symbols().len()
     535         1043 :     }
     536              : 
     537          601 :     fn next_symbol(&self, id: u32) -> u32 {
     538          601 :         (id + 1).min(self.symbols().len() as u32 - 1)
     539          601 :     }
     540              : 
     541              :     /// The id and data of a name index object
     542          216 :     fn index_object(&self, object: CatalogObjectRef) -> Option<(QualifiedCatalogObjectId, CandidateObjectRef)> {
     543          216 :         let data = self.entry_data(object.entry())?;
     544          216 :         let id = data.object_id(object);
     545          216 :         let candidate_object = match object {
     546           13 :             CatalogObjectRef::Database(_) => CandidateObjectRef::Database(id),
     547            3 :             CatalogObjectRef::Schema(_) => CandidateObjectRef::Schema(id),
     548           60 :             CatalogObjectRef::Table(t) => CandidateObjectRef::Table(t),
     549          129 :             CatalogObjectRef::Column(c) => CandidateObjectRef::Column(c),
     550           11 :             CatalogObjectRef::Function(f) => CandidateObjectRef::Function(f),
     551              :         };
     552          216 :         Some((id, candidate_object))
     553          216 :     }
     554              : 
     555              :     /// Add a catalog object to a candidate
     556          325 :     fn add_object(
     557          325 :         &mut self,
     558          325 :         candidate: usize,
     559          325 :         tags: CandidateTags,
     560          325 :         object_id: QualifiedCatalogObjectId,
     561          325 :         object: CandidateObjectRef,
     562          325 :     ) {
     563          325 :         let co = self.candidate_objects.len();
     564          325 :         self.candidate_objects.push(CandidateObject {
     565          325 :             candidate,
     566          325 :             candidate_tags: tags,
     567          325 :             catalog_object_id: object_id,
     568          325 :             object,
     569          325 :             score: 0,
     570          325 :             qualified_name: Vec::new(),
     571          325 :             qualified_name_target_idx: 0,
     572          325 :             prefer_qualified: false,
     573          325 :         });
     574          325 :         self.candidates[candidate].catalog_objects.push(co);
     575          325 :         self.candidate_objects_by_id.insert(object_id, co);
     576          325 :     }
     577              : 
     578              :     /// Compute the completion at a cursor
     579          499 :     pub fn compute(cursor: &'c ScriptCursor<'a>, k: usize) -> Self {
     580          499 :         let mut completion = Completion::new(cursor, k);
     581              :         // Inside comments, only comment tags and the content of `@vis` tags are completed
     582          499 :         if cursor.script.scanned.is_some_and(|scanned| crate::cursor::is_inside_comment(scanned, cursor.text_offset)) {
     583           28 :             crate::completion_vis::complete_in_comment(&mut completion, k);
     584           28 :             return completion;
     585          471 :         }
     586          471 :         let Some(location) = cursor.scanner_location else { return completion };
     587          471 :         completion.target_symbol = Some(location.current);
     588              :         // The name of a table parameter, the scripts of the session
     589          471 :         if completion.complete_table_parameter(&location) {
     590            3 :             return completion;
     591          468 :         }
     592          468 :         let mut previous_symbol = location.previous;
     593              : 
     594              :         // Between symbols, or at the very beginning
     595          468 :         if location.current.relative_pos == RelativeSymbolPosition::AFTER_SYMBOL
     596          423 :             || (location.current.relative_pos == RelativeSymbolPosition::BEFORE_SYMBOL && previous_symbol.is_none())
     597           51 :         {
     598           51 :             completion.between_symbols = true;
     599          419 :         }
     600          468 :         let between_symbols = completion.between_symbols;
     601              : 
     602          468 :         let use_previous_if_at_end =
     603           35 :             |completion: &mut Completion, previous: &mut Option<SymbolLocation>| match *previous {
     604           35 :                 Some(p) if p.relative_pos == RelativeSymbolPosition::END_OF_SYMBOL => {
     605           10 :                     completion.target_symbol = Some(p);
     606           10 :                     *previous = None;
     607           10 :                     true
     608              :                 }
     609           25 :                 _ => false,
     610           35 :             };
     611              : 
     612          468 :         let target = completion.target_symbol.unwrap();
     613          468 :         if !between_symbols && target.is_dot() {
     614           30 :             match target.relative_pos {
     615              :                 RelativeSymbolPosition::AFTER_SYMBOL | RelativeSymbolPosition::END_OF_SYMBOL => {
     616           29 :                     completion.dot_completion = true
     617              :                 }
     618              :                 // Don't complete the dot itself, and not at its begin either (see docs/design/core_quirks.md)
     619              :                 _ => {
     620            1 :                     if target.relative_pos == RelativeSymbolPosition::BEGIN_OF_SYMBOL {
     621            1 :                         use_previous_if_at_end(&mut completion, &mut previous_symbol);
     622            1 :                     }
     623            1 :                     return completion;
     624              :                 }
     625              :             }
     626          438 :         } else if !between_symbols && target.is_trailing_dot() {
     627            8 :             match target.relative_pos {
     628              :                 RelativeSymbolPosition::AFTER_SYMBOL | RelativeSymbolPosition::END_OF_SYMBOL => {
     629            8 :                     completion.dot_completion = true
     630              :                 }
     631              :                 RelativeSymbolPosition::BEGIN_OF_SYMBOL => {
     632            0 :                     if !use_previous_if_at_end(&mut completion, &mut previous_symbol) {
     633            0 :                         return completion;
     634            0 :                     }
     635              :                 }
     636            0 :                 _ => return completion,
     637              :             }
     638          430 :         }
     639              : 
     640              :         // Skip completion for the current symbol, e.g. at a comma
     641          467 :         if !between_symbols && !is_symbol_kind_completable(completion.target_symbol.unwrap().symbol.kind) {
     642           34 :             if !use_previous_if_at_end(&mut completion, &mut previous_symbol) {
     643           25 :                 return completion;
     644            9 :             }
     645            9 :             if !is_symbol_kind_completable(completion.target_symbol.unwrap().symbol.kind) {
     646            0 :                 return completion;
     647            9 :             }
     648          433 :         }
     649          442 :         let target = completion.target_symbol.unwrap();
     650              : 
     651              :         // Find the expected symbols when not dot-completing, keep the snapshot for the suffix probes
     652          442 :         let mut expects_identifier = false;
     653          442 :         let mut expected_at_cursor = probe::ExpectedAtCursor::default();
     654          442 :         if !completion.dot_completion {
     655          405 :             let parse_until = if target.relative_pos == RelativeSymbolPosition::AFTER_SYMBOL
     656           45 :                 && !completion.is_at_eof(target.symbol_id)
     657              :             {
     658           45 :                 completion.next_symbol(target.symbol_id)
     659              :             } else {
     660          360 :                 target.symbol_id
     661              :             };
     662          405 :             expected_at_cursor = probe::parse_until_with_snapshot(completion.symbols(), parse_until);
     663          405 :             expects_identifier = expected_at_cursor.expected.contains(&S_IDENT);
     664           37 :         }
     665              : 
     666              :         // Is the cursor at a definition position?
     667          442 :         if let Some(parsed) = cursor.script.parsed {
     668         1313 :             for &node_id in &cursor.ast_path_to_root {
     669         1304 :                 if matches!(
     670         1313 :                     parsed.nodes[node_id as usize].attribute_key(),
     671              :                     AttributeKey::SQL_CREATE_TABLE_NAME
     672              :                         | AttributeKey::SQL_CREATE_AS_NAME
     673              :                         | AttributeKey::SQL_VIEW_NAME
     674              :                         | AttributeKey::SQL_COLUMN_DEF_NAME
     675              :                         | AttributeKey::SQL_RESULT_TARGET_NAME
     676              :                         | AttributeKey::SQL_CTE_NAME
     677              :                         | AttributeKey::SQL_WINDOW_DEF_NAME
     678              :                         | AttributeKey::SQL_CREATE_FUNCTION_NAME
     679              :                         | AttributeKey::SQL_FUNCTION_PARAM_NAME
     680              :                 ) {
     681            9 :                     completion.at_definition = true;
     682            9 :                     break;
     683         1304 :                 }
     684              :             }
     685            0 :         }
     686              : 
     687              :         // Is the previous symbol an inner dot and the cursor on the successor?
     688          442 :         if previous_symbol.is_some_and(|p| p.is_dot()) && expects_identifier {
     689            1 :             if matches!(
     690           65 :                 target.relative_pos,
     691              :                 RelativeSymbolPosition::END_OF_SYMBOL
     692              :                     | RelativeSymbolPosition::BEGIN_OF_SYMBOL
     693              :                     | RelativeSymbolPosition::MID_OF_SYMBOL
     694           64 :             ) {
     695           64 :                 completion.dot_completion = true;
     696           64 :             }
     697          377 :         }
     698              : 
     699          442 :         completion.has_post_cursor_token = completion.compute_has_post_cursor_token();
     700              : 
     701          442 :         if completion.dot_completion {
     702          101 :             completion.find_candidates_for_name_path();
     703          342 :         } else if between_symbols {
     704              :             // Only expected keywords with an empty target, the passive inline hints
     705           51 :             if !suppress_passive_hint(target.symbol.kind) {
     706           30 :                 completion
     707           30 :                     .add_expected_keywords_as_candidates(&expected_at_cursor.expected, &expected_at_cursor.prefix);
     708           31 :             }
     709              :         } else {
     710          290 :             completion.add_expected_keywords_as_candidates(&expected_at_cursor.expected, &expected_at_cursor.prefix);
     711              :             // An identity candidate with the text of the user
     712          290 :             if expects_identifier {
     713          175 :                 let sym = target.symbol;
     714          175 :                 let mut symbol_text = completion.text_at(sym.location.offset, sym.location.length);
     715          175 :                 if symbol_text.len() > MAX_IDENTITY_LENGTH {
     716            1 :                     symbol_text = &[];
     717          174 :                 }
     718          175 :                 if !symbol_text.is_empty() {
     719          174 :                     let identity_score = if completion.at_definition {
     720            9 :                         Score::MAX
     721              :                     } else {
     722          165 :                         match completion.strategy {
     723           63 :                             CompletionStrategy::TABLE_REF => IDENTITY_SCORE_TABLE_REF,
     724           62 :                             CompletionStrategy::COLUMN_REF => IDENTITY_SCORE_COLUMN_REF,
     725           22 :                             CompletionStrategy::TABLE_REF_ALIAS => IDENTITY_SCORE_TABLE_REF_ALIAS,
     726           18 :                             _ => Score::MAX,
     727              :                         }
     728              :                     };
     729          174 :                     let location = Self::symbol_location(&sym);
     730          174 :                     let mut identity =
     731          174 :                         Candidate::new(text_arc(symbol_text), 0, CandidateTag::IDENTITY.0, location, location);
     732          174 :                     identity.completion_text_is_verbatim = true;
     733          174 :                     identity.score = identity_score;
     734          174 :                     completion.candidate_heap.insert(identity);
     735            1 :                 }
     736          115 :             }
     737              :             // Search the name indexes when expecting an identifier that is not a definition or alias
     738          290 :             if expects_identifier
     739          175 :                 && !completion.at_definition
     740          166 :                 && completion.strategy != CompletionStrategy::TABLE_REF_ALIAS
     741          144 :             {
     742          144 :                 completion.find_candidates_in_indexes();
     743          144 :                 completion.find_candidates_in_scope();
     744          144 :                 completion.promote_tables_and_peers_for_unresolved_columns();
     745          195 :             }
     746              :         }
     747          442 :         if !between_symbols {
     748          391 :             completion.promote_identifiers_in_scope();
     749          393 :         }
     750          442 :         completion.select_top_candidates();
     751          442 :         if !between_symbols {
     752          391 :             completion.qualify_top_candidates();
     753          393 :         }
     754          442 :         completion.derive_keyword_continuations(&expected_at_cursor.prefix);
     755          442 :         completion
     756          499 :     }
     757              : 
     758              :     /// Is there a real token after the feed point?
     759          442 :     fn compute_has_post_cursor_token(&self) -> bool {
     760          442 :         let Some(target) = self.target_symbol else { return false };
     761          442 :         let replace_target = matches!(
     762          442 :             target.relative_pos,
     763              :             RelativeSymbolPosition::BEGIN_OF_SYMBOL
     764              :                 | RelativeSymbolPosition::MID_OF_SYMBOL
     765              :                 | RelativeSymbolPosition::END_OF_SYMBOL
     766              :         );
     767          442 :         let mut post_id = target.symbol_id;
     768          442 :         if (target.relative_pos == RelativeSymbolPosition::AFTER_SYMBOL || replace_target)
     769          436 :             && !self.is_at_eof(target.symbol_id)
     770          436 :         {
     771          436 :             post_id = self.next_symbol(target.symbol_id);
     772          438 :         }
     773          442 :         if self.is_at_eof(post_id) {
     774            0 :             return false;
     775          442 :         }
     776          442 :         self.symbols()[post_id as usize].kind != S_YYEOF
     777          442 :     }
     778              : 
     779              :     /// The feed point and whether feeding replaces the target token
     780          762 :     fn feed_point(&self) -> (u32, bool) {
     781          762 :         let target = self.target_symbol.unwrap();
     782          762 :         let replace_target = matches!(
     783          762 :             target.relative_pos,
     784              :             RelativeSymbolPosition::BEGIN_OF_SYMBOL
     785              :                 | RelativeSymbolPosition::MID_OF_SYMBOL
     786              :                 | RelativeSymbolPosition::END_OF_SYMBOL
     787              :         );
     788          762 :         let mut feed_id = target.symbol_id;
     789          762 :         if target.relative_pos == RelativeSymbolPosition::AFTER_SYMBOL && !self.is_at_eof(target.symbol_id) {
     790           69 :             feed_id = self.next_symbol(target.symbol_id);
     791          693 :         }
     792          762 :         (feed_id, replace_target)
     793          762 :     }
     794              : 
     795              :     /// Add the expected keywords to the heap
     796          320 :     fn add_expected_keywords_as_candidates(&mut self, symbols: &[SymbolKind], prefix: &PrefixSnapshot) {
     797          320 :         let target = self.target_symbol.unwrap();
     798          320 :         let (feed_id, replace_target) = self.feed_point();
     799              : 
     800              :         // The match tags of a keyword against the typed text
     801          320 :         let typed_text: &[u8] = {
     802          320 :             let symbol_ofs = target.symbol.location.offset;
     803          320 :             let symbol_prefix = target.text_offset.max(symbol_ofs) - symbol_ofs;
     804          320 :             trim_double_quotes(self.text_at(symbol_ofs, symbol_prefix))
     805              :         };
     806        72930 :         let keyword_tags = |keyword: SymbolKind, text: &str| -> CandidateTags {
     807        72930 :             let mut tags = CandidateTag::EXPECTED_PARSER_SYMBOL.0 | keyword_prevalence(keyword).0;
     808        72930 :             match target.relative_pos {
     809              :                 RelativeSymbolPosition::BEGIN_OF_SYMBOL
     810              :                 | RelativeSymbolPosition::MID_OF_SYMBOL
     811              :                 | RelativeSymbolPosition::END_OF_SYMBOL => {
     812        70328 :                     let typed = typed_text;
     813        70328 :                     if !typed.is_empty() {
     814        69131 :                         if let Some(t) = match_tags(text.as_bytes(), typed) {
     815         3727 :                             tags |= t;
     816        65404 :                         }
     817         1197 :                     }
     818        70328 :                     tags
     819              :                 }
     820         2602 :                 _ => tags,
     821              :             }
     822        72930 :         };
     823              : 
     824          320 :         let target_loc = if self.between_symbols {
     825           30 :             SymbolSpan::new(self.cursor.text_offset as u32, 0)
     826              :         } else {
     827          290 :             Self::symbol_location(&target.symbol)
     828              :         };
     829          320 :         let inside_select = self.cursor.script.parsed.is_some_and(|parsed| {
     830          320 :             self.cursor
     831          320 :                 .ast_path_to_root
     832          320 :                 .iter()
     833          809 :                 .any(|&n| parsed.nodes[n as usize].node_type() == NodeType::OBJECT_SQL_SELECT)
     834          320 :         });
     835              : 
     836              :         // Probe the keywords against the tokens after the cursor
     837          320 :         let mut probes = Vec::new();
     838          320 :         let mut probe_input_idx = Vec::new();
     839          320 :         if self.has_post_cursor_token {
     840           42 :             let mut kinds = Vec::new();
     841        15700 :             for (i, &s) in symbols.iter().enumerate() {
     842        15700 :                 if Keyword::name_of(s).is_some_and(|n| !n.is_empty()) {
     843        15171 :                     kinds.push(s);
     844        15171 :                     probe_input_idx.push(i);
     845        15171 :                 }
     846              :             }
     847           42 :             if !kinds.is_empty() {
     848           42 :                 probes = probe::probe_suffix_batch(self.symbols(), feed_id, prefix, &kinds, replace_target);
     849           42 :             }
     850          278 :         }
     851          320 :         let mut next_probe = 0;
     852              : 
     853        75485 :         for (i, &expected) in symbols.iter().enumerate() {
     854        75485 :             let Some(name) = Keyword::name_of(expected).filter(|n| !n.is_empty()) else { continue };
     855        72930 :             let mut tags = keyword_tags(expected, name);
     856              :             // A leading FROM is no proactive suggestion outside of a SELECT
     857        72930 :             if expected == S_FROM && !inside_select && !has(tags, CandidateTag::SUBSTRING_MATCH) {
     858            1 :                 continue;
     859        72929 :             }
     860        72929 :             if next_probe < probes.len() && probe_input_idx[next_probe] == i {
     861        15171 :                 let probe = probes[next_probe];
     862        15171 :                 next_probe += 1;
     863        15171 :                 if !probe.k1_compatible {
     864         1576 :                     tags |= CandidateTag::SUFFIX_INCOMPATIBLE.0;
     865        13595 :                 } else if probe.reached_eof || probe.depth_consumed >= 2 {
     866        12856 :                     tags |= CandidateTag::SUFFIX_DEPTH_MANY.0;
     867        13586 :                 } else if probe.depth_consumed == 1 {
     868          739 :                     tags |= CandidateTag::SUFFIX_DEPTH_ONE.0;
     869          739 :                 }
     870        57758 :             }
     871        72929 :             let mut candidate = Candidate::new(Arc::from(name), 0, tags, target_loc, target_loc);
     872        72929 :             candidate.score = compute_candidate_score(tags);
     873        72929 :             candidate.keyword_symbol = Some(expected);
     874        72929 :             self.candidate_heap.insert(candidate);
     875              :         }
     876          320 :     }
     877              : 
     878              :     /// The text typed before the cursor in the target symbol, without double quotes
     879          395 :     fn read_target_prefix(&self) -> &'a [u8] {
     880          395 :         let Some(target) = self.target_symbol else { return &[] };
     881          395 :         let symbol_ofs = target.symbol.location.offset;
     882          395 :         let symbol_len = target.symbol.location.length;
     883          395 :         let cursor = self.cursor.text_offset as u32;
     884          395 :         let safe_cursor = cursor.max(symbol_ofs).min(symbol_ofs + symbol_len);
     885          395 :         let symbol_text = self.text_at(symbol_ofs, symbol_len);
     886          395 :         let prefix_len = ((safe_cursor.max(symbol_ofs) - symbol_ofs) as usize).min(symbol_text.len());
     887          395 :         trim_double_quotes(&symbol_text[..prefix_len])
     888          395 :     }
     889              : 
     890              :     /// Find candidates in a name search index
     891          251 :     fn find_candidates_in_index(&mut self, index: &NameSearchIndex, through_catalog: bool) {
     892          251 :         let target = self.target_symbol.unwrap();
     893          251 :         let symbol_ofs = target.symbol.location.offset;
     894          251 :         let symbol_text = self.text_at(symbol_ofs, target.symbol.location.length);
     895          251 :         let prefix_text = self.read_target_prefix();
     896              :         // Fall back to the full word if the cursor prefix is empty
     897          251 :         let search_text = if prefix_text.is_empty() { trim_double_quotes(symbol_text) } else { prefix_text };
     898              : 
     899          410 :         for name in index.search(search_text) {
     900              :             // Skip the name under the cursor itself
     901          409 :             if !through_catalog
     902          220 :                 && name.occurrences == 1
     903          206 :                 && target.text_offset >= name.location.offset
     904          204 :                 && target.text_offset <= name.location.offset + name.location.length
     905              :             {
     906          166 :                 continue;
     907          243 :             }
     908          243 :             let mut tags = CandidateTag::NAME_INDEX.0;
     909          243 :             if through_catalog {
     910          189 :                 tags |= CandidateTag::THROUGH_CATALOG.0;
     911          190 :             }
     912            0 :             if matches!(
     913          243 :                 target.relative_pos,
     914              :                 RelativeSymbolPosition::BEGIN_OF_SYMBOL
     915              :                     | RelativeSymbolPosition::MID_OF_SYMBOL
     916              :                     | RelativeSymbolPosition::END_OF_SYMBOL
     917              :             ) {
     918          243 :                 tags |= CandidateTag::SUBSTRING_MATCH.0;
     919          243 :                 if ci_starts_with(name.text.as_bytes(), prefix_text) {
     920          175 :                     tags |= CandidateTag::PREFIX_MATCH.0;
     921          175 :                     if prefix_text.len() == name.text.len() {
     922           28 :                         tags |= CandidateTag::EXACT_MATCH.0;
     923          149 :                     }
     924           68 :                 }
     925            0 :             }
     926          243 :             let candidate = match self.candidates_by_name.get(&name.text) {
     927           37 :                 Some(&c) => {
     928           37 :                     let candidate = &mut self.candidates[c];
     929           37 :                     candidate.coarse_name_tags |= name.tags.0;
     930           37 :                     candidate.candidate_tags |= tags;
     931           37 :                     c
     932              :                 }
     933              :                 None => {
     934          206 :                     let location = Self::symbol_location(&target.symbol);
     935          206 :                     self.candidates.push(Candidate::new(name.text.clone(), name.tags.0, tags, location, location));
     936          206 :                     let c = self.candidates.len() - 1;
     937          206 :                     self.candidates_by_name.insert(name.text.clone(), c);
     938          206 :                     c
     939              :                 }
     940              :             };
     941          244 :             for &object in &name.objects {
     942          216 :                 let Some((object_id, candidate_object)) = self.index_object(object) else { continue };
     943          216 :                 if let Some(&co) = self.candidate_objects_by_id.get(&object_id) {
     944           14 :                     self.candidate_objects[co].candidate_tags |= tags;
     945           14 :                     continue;
     946          202 :                 }
     947          202 :                 self.add_object(candidate, tags, object_id, candidate_object);
     948              :             }
     949              :         }
     950          251 :     }
     951              : 
     952              :     /// Find candidates in the indexes of the script and the catalog
     953          144 :     fn find_candidates_in_indexes(&mut self) {
     954          144 :         let Some(analyzed) = self.analyzed() else { return };
     955          144 :         self.find_candidates_in_index(analyzed.name_search_index(), false);
     956          144 :         for (_, entry) in self.catalog().entries_ranked() {
     957              :             // The catalog may still hold an earlier version of the script
     958          108 :             if entry.data().catalog_entry_id == analyzed.entry.catalog_entry_id {
     959            1 :                 continue;
     960          107 :             }
     961          107 :             self.find_candidates_in_index(entry.name_search_index(), true);
     962              :         }
     963          144 :     }
     964              : 
     965              :     /// Add or merge a candidate without catalog objects
     966           40 :     fn add_local_candidate(
     967           40 :         &mut self,
     968           40 :         name: &Arc<str>,
     969           40 :         name_tags: u8,
     970           40 :         mut tags: CandidateTags,
     971           40 :         prefix: &[u8],
     972           40 :         location: SymbolSpan,
     973           40 :         qualified: SymbolSpan,
     974           40 :     ) {
     975           40 :         if !prefix.is_empty() {
     976           18 :             match match_tags(name.as_bytes(), prefix) {
     977           13 :                 Some(t) => tags |= t,
     978            5 :                 None => return,
     979              :             }
     980           22 :         }
     981           35 :         match self.candidates_by_name.get(name) {
     982           11 :             Some(&c) => {
     983           11 :                 let candidate = &mut self.candidates[c];
     984           11 :                 candidate.coarse_name_tags |= name_tags;
     985           11 :                 candidate.candidate_tags |= tags;
     986           11 :                 // A visible local relation shadows catalog objects with the same spelling
     987           11 :                 candidate.catalog_objects.clear();
     988           11 :                 candidate.target_location = location;
     989           11 :                 candidate.target_location_qualified = qualified;
     990           11 :             }
     991           24 :             None => {
     992           24 :                 self.candidates.push(Candidate::new(name.clone(), name_tags, tags, location, qualified));
     993           24 :                 self.candidates_by_name.insert(name.clone(), self.candidates.len() - 1);
     994           24 :             }
     995              :         }
     996           40 :     }
     997              : 
     998              :     /// The output column names of a CTE or derived table source
     999           73 :     fn derived_columns(analyzed: &AnalyzedScript, source: &ScopeSource) -> Vec<Arc<str>> {
    1000           73 :         let (child, aliases) = match &source.table {
    1001           10 :             ReferencedTable::Cte(loc) => {
    1002           10 :                 let cte = analyzed.name_scopes[loc.scope as usize].cte_definitions.at(loc.index as usize);
    1003           10 :                 let Some(child) = cte.child_scope else { return Vec::new() };
    1004           10 :                 let aliases = if source.column_aliases.is_empty() { &cte.column_aliases } else { &source.column_aliases };
    1005           10 :                 (child, aliases)
    1006              :             }
    1007            4 :             ReferencedTable::Subquery(child) => (*child, &source.column_aliases),
    1008           59 :             ReferencedTable::Table(_) | ReferencedTable::Unresolved(_) => return Vec::new(),
    1009              :         };
    1010           14 :         let outputs = &analyzed.name_scopes[child as usize].output_columns;
    1011           14 :         let mut names = Vec::new();
    1012           23 :         for (i, output) in outputs.iter().enumerate() {
    1013           23 :             let name = aliases.get(i).unwrap_or(&output.column_name);
    1014           23 :             if !name.text.is_empty() {
    1015           23 :                 names.push(name.text.clone());
    1016           23 :             }
    1017              :         }
    1018           14 :         names
    1019           73 :     }
    1020              : 
    1021              :     /// Is a scope within another one?
    1022            5 :     fn is_scope_within(analyzed: &AnalyzedScript, scope: u32, ancestor: Option<u32>) -> bool {
    1023            5 :         let mut current = Some(scope);
    1024            9 :         while let Some(c) = current {
    1025            7 :             if Some(c) == ancestor {
    1026            3 :                 return true;
    1027            4 :             }
    1028            4 :             current = analyzed.name_scopes[c as usize].parent_scope;
    1029              :         }
    1030            2 :         false
    1031            5 :     }
    1032              : 
    1033              :     /// Add CTEs and the columns of CTE sources in scope
    1034          144 :     fn find_candidates_in_scope(&mut self) {
    1035          144 :         let cursor = self.cursor;
    1036          144 :         let (Some(target), Some(parsed), Some(analyzed)) = (self.target_symbol, cursor.script.parsed, self.analyzed())
    1037              :         else {
    1038            0 :             return;
    1039              :         };
    1040          144 :         let prefix = self.read_target_prefix();
    1041          144 :         let location = Self::symbol_location(&target.symbol);
    1042          144 :         let tags = CandidateTag::NAME_INDEX.0 | CandidateTag::IN_NAME_SCOPE.0;
    1043              : 
    1044          144 :         if self.strategy == CompletionStrategy::TABLE_REF {
    1045              :             // Non-recursive CTEs are visible after their definition and not within themselves
    1046           63 :             let scanned = &parsed.scanned;
    1047           68 :             for &scope_id in &cursor.name_scopes {
    1048           68 :                 let scope = &analyzed.name_scopes[scope_id as usize];
    1049           68 :                 for (_, cte) in scope.cte_definitions.iter() {
    1050            7 :                     let Some(&cte_node) = scope.cte_definition_nodes.get(&cte.cte_name.text) else { continue };
    1051            7 :                     let cte_text = resolve_text_span(
    1052            7 :                         &scanned.text,
    1053            7 :                         &scanned.symbols,
    1054            7 :                         *parsed.nodes[cte_node as usize].symbol_span(),
    1055              :                     );
    1056            7 :                     if !scope.ctes_recursive && cte_text.offset as usize >= cursor.text_offset {
    1057            1 :                         continue;
    1058            6 :                     }
    1059            6 :                     if !scope.ctes_recursive && Self::is_scope_within(analyzed, cursor.name_scopes[0], cte.child_scope)
    1060              :                     {
    1061            3 :                         continue;
    1062            3 :                     }
    1063            3 :                     self.add_local_candidate(
    1064            3 :                         &cte.cte_name.text,
    1065            3 :                         NameTag::TABLE_NAME.0,
    1066            3 :                         tags,
    1067            3 :                         prefix,
    1068            3 :                         location,
    1069            3 :                         location,
    1070              :                     );
    1071              :                 }
    1072              :             }
    1073           63 :             return;
    1074           81 :         }
    1075           81 :         if self.strategy != CompletionStrategy::COLUMN_REF {
    1076           18 :             return;
    1077           63 :         }
    1078              :         // The columns of CTE and derived table sources, catalog columns come from the name index
    1079           70 :         for &scope_id in &cursor.name_scopes {
    1080           70 :             let scope = &analyzed.name_scopes[scope_id as usize];
    1081           74 :             for source in &scope.sources {
    1082           68 :                 for name in Self::derived_columns(analyzed, source) {
    1083           13 :                     self.add_local_candidate(&name, NameTag::COLUMN_NAME.0, tags, prefix, location, location);
    1084           13 :                 }
    1085              :             }
    1086              :         }
    1087          144 :     }
    1088              : 
    1089              :     /// The name scopes around the cursor with the FROM items that the column refs of the cursor can see in them.
    1090              :     /// The innermost scope comes first, a correlation barrier hides all scopes above it.
    1091          426 :     fn visible_scopes(analyzed: &AnalyzedScript, name_scopes: &[ScopeId]) -> Vec<(ScopeId, SourceVisibility)> {
    1092          426 :         let mut visible = Vec::with_capacity(name_scopes.len());
    1093          426 :         let mut visibility = SourceVisibility::All;
    1094          430 :         for &scope_id in name_scopes {
    1095          321 :             visible.push((scope_id, visibility));
    1096          321 :             let scope = &analyzed.name_scopes[scope_id as usize];
    1097          321 :             if scope.column_correlation_barrier {
    1098            4 :                 break;
    1099          317 :             }
    1100          317 :             visibility = scope.parent_visibility;
    1101              :         }
    1102          426 :         visible
    1103          426 :     }
    1104              : 
    1105              :     /// Promote names in the current name scopes
    1106          391 :     fn promote_identifiers_in_scope(&mut self) {
    1107          391 :         let cursor = self.cursor;
    1108          391 :         let Some(analyzed) = self.analyzed() else { return };
    1109          391 :         let in_insert_source =
    1110          395 :             self.cursor.name_scopes.iter().any(|&s| analyzed.name_scopes[s as usize].column_correlation_barrier);
    1111          408 :         let promote = |completion: &mut Self, object_id: QualifiedCatalogObjectId| {
    1112          241 :             if let Some(&co) = completion.candidate_objects_by_id.get(&object_id) {
    1113          101 :                 let candidate = completion.candidate_objects[co].candidate;
    1114          101 :                 completion.candidate_objects[co].candidate_tags |= CandidateTag::IN_NAME_SCOPE.0;
    1115          101 :                 completion.candidates[candidate].candidate_tags |= CandidateTag::IN_NAME_SCOPE.0;
    1116          144 :             }
    1117          241 :         };
    1118          395 :         for (scope_id, visibility) in Self::visible_scopes(analyzed, &cursor.name_scopes) {
    1119          285 :             let scope = &analyzed.name_scopes[scope_id as usize];
    1120              :             // Columns of resolved table refs in scope
    1121          285 :             for &table_ref_id in &scope.table_references {
    1122          276 :                 let table_ref = &analyzed.table_references[table_ref_id as usize];
    1123          276 :                 if !visibility.sees_node(table_ref.ast_node_id) {
    1124            9 :                     continue;
    1125          267 :                 }
    1126          267 :                 if in_insert_source && table_ref.role == TableRole::Write {
    1127            0 :                     continue;
    1128          267 :                 }
    1129          267 :                 let Some(resolved) = table_ref.inner.as_ref().and_then(|r| r.resolved_table.as_ref()) else { continue };
    1130          125 :                 let table_id = resolved.catalog_table_id.unpack_table_id();
    1131              :                 // Only tables of the catalog, the script itself is not necessarily published
    1132          125 :                 let Some(table) = self.catalog().table_by_id(table_id) else { continue };
    1133          205 :                 for column in &table.table_columns {
    1134          205 :                     promote(self, column.object_id);
    1135          205 :                 }
    1136              :             }
    1137              :             // Resolved column refs in scope
    1138          286 :             for &expr in &scope.expressions {
    1139          148 :                 let Some(column_ref) = analyzed.expressions[expr as usize].column_ref() else { continue };
    1140          148 :                 let ColumnResolution::TableColumn(loc) = column_ref.resolved else { continue };
    1141              :                 // The location refers to the entries the script was analyzed against, which the catalog
    1142              :                 // may have replaced since
    1143           36 :                 let Some(column) = analyzed.table(loc.table).and_then(|t| t.table_columns.get(loc.column as usize))
    1144              :                 else {
    1145            0 :                     continue;
    1146              :                 };
    1147           36 :                 promote(self, column.object_id);
    1148              :             }
    1149              :         }
    1150          391 :     }
    1151              : 
    1152              :     /// Promote tables that resolve unresolved columns, and their columns
    1153          144 :     fn promote_tables_and_peers_for_unresolved_columns(&mut self) {
    1154          144 :         let cursor = self.cursor;
    1155          144 :         let (Some(_), Some(analyzed)) = (cursor.statement_id, self.analyzed()) else {
    1156           14 :             return;
    1157              :         };
    1158          130 :         let mut columns = Vec::new();
    1159          142 :         for &scope_id in &cursor.name_scopes {
    1160          142 :             let scope = &analyzed.name_scopes[scope_id as usize];
    1161          143 :             for &expr in &scope.expressions {
    1162           95 :                 let Some(column_ref) = analyzed.expressions[expr as usize].column_ref() else { continue };
    1163           95 :                 if column_ref.resolved.is_resolved() {
    1164           26 :                     continue;
    1165           69 :                 }
    1166           69 :                 columns.clear();
    1167           69 :                 self.resolve_table_columns_with_catalog(
    1168           69 :                     analyzed,
    1169           69 :                     &column_ref.column_name.column_name.text,
    1170           69 :                     &mut columns,
    1171              :                 );
    1172           72 :                 for &column in &columns {
    1173           23 :                     let Some(table) = self.table(column.table) else { continue };
    1174           23 :                     if let Some(&co) = self.candidate_objects_by_id.get(&table.object_id) {
    1175            2 :                         let candidate = self.candidate_objects[co].candidate;
    1176            2 :                         self.candidate_objects[co].candidate_tags |= CandidateTag::RESOLVING_TABLE.0;
    1177            2 :                         self.candidates[candidate].candidate_tags |= CandidateTag::RESOLVING_TABLE.0;
    1178           21 :                     }
    1179           31 :                     for peer in &table.table_columns {
    1180           31 :                         if let Some(&co) = self.candidate_objects_by_id.get(&peer.object_id) {
    1181           23 :                             let candidate = self.candidate_objects[co].candidate;
    1182           23 :                             self.candidate_objects[co].candidate_tags |= CandidateTag::UNRESOLVED_PEER.0;
    1183           23 :                             self.candidates[candidate].candidate_tags |= CandidateTag::UNRESOLVED_PEER.0;
    1184           24 :                         }
    1185              :                     }
    1186              :                 }
    1187              :             }
    1188              :         }
    1189          144 :     }
    1190              : 
    1191              :     /// The table columns with a name in the catalog entries by rank, then in the script
    1192           69 :     fn resolve_table_columns_with_catalog(&self, analyzed: &Arc<AnalyzedScript>, name: &str, out: &mut Vec<ColumnLoc>) {
    1193           69 :         for (_, entry) in self.catalog().entries_ranked() {
    1194           46 :             if entry.data().catalog_entry_id == analyzed.entry.catalog_entry_id {
    1195            0 :                 continue;
    1196           46 :             }
    1197           46 :             entry.data().resolve_table_columns(name, out);
    1198              :         }
    1199           69 :         analyzed.entry.resolve_table_columns(name, out);
    1200           69 :     }
    1201              : 
    1202              :     /// Tables of a schema in any database, the own script first
    1203           38 :     fn resolve_schema_tables(&self, analyzed: &AnalyzedScript, schema: &str, out: &mut Vec<(TableLoc, bool)>) {
    1204           38 :         let own = &analyzed.entry;
    1205           38 :         let lower = (Arc::<str>::from(schema), Arc::<str>::from(""));
    1206           38 :         for ((s, _), tables) in own.tables_by_unqualified_schema.range(lower..) {
    1207            4 :             if &**s != schema {
    1208            2 :                 break;
    1209            2 :             }
    1210            2 :             out.extend(tables.iter().map(|&t| (own.table_loc(t), false)));
    1211              :         }
    1212           38 :         let lower = (Arc::<str>::from(schema), MIN_LAYER, 0);
    1213           38 :         for ((s, _, entry_id), _) in self.catalog().entries_by_schema().range(lower..) {
    1214           13 :             if &**s != schema {
    1215            9 :                 break;
    1216            4 :             }
    1217            4 :             if *entry_id == own.catalog_entry_id {
    1218            0 :                 continue;
    1219            4 :             }
    1220            4 :             let Some(other) = self.catalog().entry(*entry_id) else { continue };
    1221            4 :             let other = other.data();
    1222            4 :             let lower = (Arc::<str>::from(schema), Arc::<str>::from(""));
    1223            7 :             for ((s, _), tables) in other.tables_by_unqualified_schema.range(lower..) {
    1224            7 :                 if &**s != schema {
    1225            3 :                     break;
    1226            4 :                 }
    1227            6 :                 out.extend(tables.iter().map(|&t| (other.table_loc(t), true)));
    1228              :             }
    1229              :         }
    1230           38 :     }
    1231              : 
    1232              :     /// Tables of a qualified schema, the own script first
    1233           19 :     fn resolve_qualified_schema_tables(
    1234           19 :         &self,
    1235           19 :         analyzed: &AnalyzedScript,
    1236           19 :         database: &str,
    1237           19 :         schema: &str,
    1238           19 :         out: &mut Vec<(TableLoc, bool)>,
    1239           19 :     ) {
    1240           19 :         let own = &analyzed.entry;
    1241           19 :         let key = (Arc::<str>::from(schema), Arc::<str>::from(database));
    1242           19 :         if let Some(tables) = own.tables_by_unqualified_schema.get(&key) {
    1243            1 :             out.extend(tables.iter().map(|&t| (own.table_loc(t), false)));
    1244           18 :         }
    1245           19 :         let lower = (Arc::<str>::from(database), Arc::<str>::from(schema), MIN_LAYER, 0);
    1246           19 :         let upper = (Arc::<str>::from(database), Arc::<str>::from(schema), MAX_LAYER, CatalogEntryId::MAX);
    1247           19 :         for ((_, _, _, entry_id), _) in self.catalog().entries_by_qualified_schema().range(lower..=upper) {
    1248           18 :             if *entry_id == own.catalog_entry_id {
    1249            0 :                 continue;
    1250           18 :             }
    1251           18 :             let Some(other) = self.catalog().entry(*entry_id) else { continue };
    1252           18 :             let other = other.data();
    1253           18 :             if let Some(tables) = other.tables_by_unqualified_schema.get(&key) {
    1254           18 :                 out.extend(tables.iter().map(|&t| (other.table_loc(t), true)));
    1255            0 :             }
    1256              :         }
    1257           19 :     }
    1258              : 
    1259              :     /// Schemas of a database, the own script first
    1260           38 :     fn resolve_database_schemas(
    1261           38 :         &self,
    1262           38 :         analyzed: &AnalyzedScript,
    1263           38 :         database: &str,
    1264           38 :     ) -> Vec<(Arc<str>, QualifiedCatalogObjectId, bool)> {
    1265           38 :         let mut out = Vec::new();
    1266           38 :         let lower = (Arc::<str>::from(database), Arc::<str>::from(""));
    1267           38 :         for ((db, _), &i) in analyzed.entry.schemas_by_qualified_name.range(lower.clone()..) {
    1268            2 :             if &**db != database {
    1269            0 :                 break;
    1270            2 :             }
    1271            2 :             let schema = &analyzed.entry.schema_references[i];
    1272            2 :             out.push((schema.schema_name.clone(), schema.object_id, false));
    1273              :         }
    1274           38 :         for ((db, _), schema) in self.catalog().schemas().range(lower..) {
    1275           32 :             if &**db != database {
    1276            1 :                 break;
    1277           31 :             }
    1278           31 :             out.push((schema.schema_name.clone(), schema.object_id, true));
    1279              :         }
    1280           38 :         out
    1281           38 :     }
    1282              : 
    1283              :     /// Complete a name path after a dot
    1284          101 :     fn find_candidates_for_name_path(&mut self) {
    1285          101 :         let cursor = self.cursor;
    1286          101 :         let Some(analyzed) = self.analyzed() else { return };
    1287          101 :         let scanned = cursor.script.scanned.unwrap();
    1288          101 :         let target = self.target_symbol.unwrap();
    1289              : 
    1290              :         // A recovered `alias.` context without a column ref node: resolve the alias in the scopes
    1291          101 :         if let CursorContext::AliasDot = cursor.context {
    1292            9 :             let Some(location) = cursor.scanner_location else { return };
    1293            9 :             let Some(dot) = ScriptCursor::dot_symbol(&location, &target) else { return };
    1294            9 :             let (alias_begin, alias_end) =
    1295            9 :                 crate::cursor::qualifier_before(&scanned.text, dot.symbol.location.offset as usize);
    1296            9 :             let alias_text = &scanned.text[alias_begin..alias_end];
    1297              :             // The qualifier is sealed, only the text after the dot is replaced
    1298            9 :             let replace_at = SymbolSpan::new(cursor.text_offset as u32, 0);
    1299            9 :             let qualified_at = SymbolSpan::new(alias_begin as u32, (cursor.text_offset - alias_begin) as u32);
    1300            9 :             let tags = CandidateTag::DOT_RESOLUTION_COLUMN.0 | CandidateTag::IN_NAME_SCOPE.0;
    1301            9 :             for &scope_id in &cursor.name_scopes {
    1302            9 :                 let scope = &analyzed.name_scopes[scope_id as usize];
    1303            9 :                 let Some(source) = scope.source(alias_text) else { continue };
    1304            9 :                 match &source.table {
    1305            7 :                     ReferencedTable::Table(loc) => {
    1306            7 :                         let names: Vec<Arc<str>> = self
    1307            7 :                             .table(*loc)
    1308            7 :                             .map(|t| {
    1309            7 :                                 let names = t.table_columns.iter().map(|c| &c.column_name);
    1310            7 :                                 let names = source.column_aliases.iter().chain(names.skip(source.column_aliases.len()));
    1311           14 :                                 names.map(|n| n.text.clone()).collect()
    1312            7 :                             })
    1313            7 :                             .unwrap_or_default();
    1314           14 :                         for name in names {
    1315           14 :                             self.add_local_candidate(
    1316           14 :                                 &name,
    1317           14 :                                 NameTag::COLUMN_NAME.0,
    1318           14 :                                 tags,
    1319           14 :                                 &[],
    1320           14 :                                 replace_at,
    1321           14 :                                 qualified_at,
    1322           14 :                             );
    1323           14 :                         }
    1324              :                     }
    1325              :                     ReferencedTable::Cte(_) | ReferencedTable::Subquery(_) => {
    1326            4 :                         for name in Self::derived_columns(analyzed, source) {
    1327            4 :                             self.add_local_candidate(
    1328            4 :                                 &name,
    1329            4 :                                 NameTag::COLUMN_NAME.0,
    1330            4 :                                 tags,
    1331            4 :                                 &[],
    1332            4 :                                 replace_at,
    1333            4 :                                 qualified_at,
    1334            4 :                             );
    1335            4 :                         }
    1336              :                     }
    1337            0 :                     ReferencedTable::Unresolved(_) => {}
    1338              :                 }
    1339            9 :                 return;
    1340              :             }
    1341            0 :             return;
    1342           92 :         }
    1343              : 
    1344           92 :         let cursor_location = target.text_offset;
    1345           92 :         let mut name_path_loc = SymbolSpan::default();
    1346           92 :         let name_path = cursor.read_cursor_name_path(&mut name_path_loc);
    1347          295 :         let resolve = |span: SymbolSpan| resolve_text_span(&scanned.text, &scanned.symbols, span);
    1348              : 
    1349              :         // Count the names and the sealed names before the cursor
    1350           92 :         let mut name_count = 0;
    1351           92 :         let mut sealed = 0;
    1352           92 :         let mut last_text_prefix: &[u8] = &[];
    1353           92 :         let name_path_ts = resolve(name_path_loc);
    1354           92 :         let mut truncate_at = name_path_ts.offset + name_path_ts.length;
    1355          203 :         while name_count < name_path.len() {
    1356          203 :             let component = &name_path[name_count];
    1357          203 :             let comp_ts = resolve(component.loc);
    1358          203 :             if component.component_type == NameComponentType::TrailingDot {
    1359           29 :                 truncate_at = comp_ts.offset + 1;
    1360           29 :                 break;
    1361          174 :             }
    1362          174 :             if component.component_type != NameComponentType::Name {
    1363            0 :                 truncate_at = comp_ts.offset;
    1364            0 :                 break;
    1365          174 :             }
    1366          174 :             if comp_ts.offset + comp_ts.length < cursor_location {
    1367          111 :                 sealed += 1;
    1368          111 :             } else {
    1369              :                 // The cursor points into a name, complete the text left of the cursor
    1370           63 :                 let last_text = self.text_at(comp_ts.offset, comp_ts.length);
    1371           65 :                 let last_content = last_text.iter().position(|&c| c != b'"').unwrap_or(last_text.len());
    1372           63 :                 let last_content_ofs = comp_ts.offset as usize + last_content;
    1373           63 :                 let prefix_length = (cursor_location as usize).max(last_content_ofs) - last_content_ofs;
    1374           63 :                 let end = (last_content + prefix_length).min(last_text.len());
    1375           63 :                 last_text_prefix = &last_text[last_content..end];
    1376           63 :                 truncate_at = comp_ts.offset;
    1377           63 :                 break;
    1378              :             }
    1379          111 :             name_count += 1;
    1380              :         }
    1381           92 :         let name_path = &name_path[..name_count];
    1382              : 
    1383              :         // The text ranges to replace
    1384           92 :         let script_length = scanned.text.len() as u32;
    1385           92 :         let name_path_end = (name_path_ts.offset + name_path_ts.length).min(script_length);
    1386           92 :         let replace_text_at = SymbolSpan::new(truncate_at, name_path_end.max(truncate_at) - truncate_at);
    1387           92 :         let name_path_text_loc =
    1388           92 :             SymbolSpan::new(name_path_ts.offset, name_path_end.max(name_path_ts.offset) - name_path_ts.offset);
    1389           92 :         if name_path.is_empty() {
    1390            0 :             return;
    1391           92 :         }
    1392              : 
    1393          111 :         let name_text = |i: usize| -> &str { analyzed.name_text(name_path[i].name.unwrap()) };
    1394           92 :         let mut dot_candidates: Vec<DotCandidate> = Vec::new();
    1395           92 :         match cursor.context {
    1396           57 :             CursorContext::TableRef { .. } => match sealed {
    1397              :                 1 => {
    1398              :                     // a._: a is a schema in the default database or a database
    1399           38 :                     let a = name_text(0);
    1400           38 :                     let mut tables = Vec::new();
    1401           38 :                     self.resolve_schema_tables(analyzed, a, &mut tables);
    1402           38 :                     for (loc, through_catalog) in tables {
    1403            8 :                         let Some(table) = self.table(loc) else { continue };
    1404            8 :                         let mut tags = CandidateTag::DOT_RESOLUTION_TABLE.0;
    1405            8 :                         if through_catalog {
    1406            6 :                             tags |= CandidateTag::THROUGH_CATALOG.0;
    1407            6 :                         }
    1408            8 :                         dot_candidates.push(DotCandidate {
    1409            8 :                             name: table.table_name.table_name.text.clone(),
    1410            8 :                             candidate_tags: tags,
    1411            8 :                             name_tags: NameTag::TABLE_NAME.0,
    1412            8 :                             object_id: table.object_id,
    1413            8 :                             object: CandidateObjectRef::Table(loc),
    1414            8 :                         });
    1415              :                     }
    1416           38 :                     for (name, object_id, through_catalog) in self.resolve_database_schemas(analyzed, a) {
    1417           33 :                         let mut tags = CandidateTag::DOT_RESOLUTION_SCHEMA.0;
    1418           33 :                         if through_catalog {
    1419           31 :                             tags |= CandidateTag::THROUGH_CATALOG.0;
    1420           31 :                         }
    1421           33 :                         dot_candidates.push(DotCandidate {
    1422           33 :                             name,
    1423           33 :                             candidate_tags: tags,
    1424           33 :                             name_tags: NameTag::SCHEMA_NAME.0,
    1425           33 :                             object_id,
    1426           33 :                             object: CandidateObjectRef::Schema(object_id),
    1427           33 :                         });
    1428              :                     }
    1429              :                 }
    1430              :                 2 => {
    1431              :                     // a.b._: a is a database, b a schema
    1432           19 :                     let (a, b) = (name_text(0), name_text(1));
    1433           19 :                     let mut tables = Vec::new();
    1434           19 :                     self.resolve_qualified_schema_tables(analyzed, a, b, &mut tables);
    1435           19 :                     for (loc, through_catalog) in tables {
    1436           19 :                         let Some(table) = self.table(loc) else { continue };
    1437           19 :                         let mut tags = CandidateTag::DOT_RESOLUTION_TABLE.0;
    1438           19 :                         if through_catalog {
    1439           18 :                             tags |= CandidateTag::THROUGH_CATALOG.0;
    1440           19 :                         }
    1441           19 :                         dot_candidates.push(DotCandidate {
    1442           19 :                             name: table.table_name.table_name.text.clone(),
    1443           19 :                             candidate_tags: tags,
    1444           19 :                             name_tags: NameTag::TABLE_NAME.0,
    1445           19 :                             object_id: table.object_id,
    1446           19 :                             object: CandidateObjectRef::Table(loc),
    1447           19 :                         });
    1448              :                     }
    1449              :                 }
    1450            0 :                 _ => {}
    1451              :             },
    1452              :             CursorContext::ColumnRef { .. } | CursorContext::AliasDot => {
    1453           35 :                 if sealed == 1 {
    1454              :                     // a._: a might be a table alias
    1455           35 :                     let a = name_text(0);
    1456           36 :                     for (scope_id, visibility) in Self::visible_scopes(analyzed, &cursor.name_scopes) {
    1457           36 :                         let scope = &analyzed.name_scopes[scope_id as usize];
    1458           39 :                         if let Some(source) = scope.sources.iter().find(|s| visibility.sees(s) && s.name.as_deref() == Some(a)) {
    1459           33 :                             match &source.table {
    1460           29 :                                 ReferencedTable::Table(loc) => {
    1461           29 :                                     if let Some(decl) = self.table(*loc) {
    1462           29 :                                         let through_catalog = decl.table_id().origin != analyzed.catalog_entry_id();
    1463           56 :                                         for (i, column) in decl.table_columns.iter().enumerate() {
    1464           56 :                                             let mut tags = CandidateTag::DOT_RESOLUTION_COLUMN.0;
    1465           56 :                                             if through_catalog {
    1466           37 :                                                 tags |= CandidateTag::THROUGH_CATALOG.0;
    1467           48 :                                             }
    1468              :                                             // Column aliases rename the leading columns
    1469           56 :                                             let name = source.column_aliases.get(i).unwrap_or(&column.column_name);
    1470           56 :                                             dot_candidates.push(DotCandidate {
    1471           56 :                                                 name: name.text.clone(),
    1472           56 :                                                 candidate_tags: tags,
    1473           56 :                                                 name_tags: NameTag::COLUMN_NAME.0,
    1474           56 :                                                 object_id: column.object_id,
    1475           56 :                                                 object: CandidateObjectRef::Column(ColumnLoc {
    1476           56 :                                                     table: *loc,
    1477           56 :                                                     column: i as u32,
    1478           56 :                                                 }),
    1479           56 :                                             });
    1480              :                                         }
    1481            0 :                                     }
    1482              :                                 }
    1483              :                                 ReferencedTable::Cte(_) | ReferencedTable::Subquery(_) => {
    1484            3 :                                     let tags = CandidateTag::DOT_RESOLUTION_COLUMN.0 | CandidateTag::IN_NAME_SCOPE.0;
    1485            6 :                                     for name in Self::derived_columns(analyzed, source) {
    1486            6 :                                         self.add_local_candidate(
    1487            6 :                                             &name,
    1488            6 :                                             NameTag::COLUMN_NAME.0,
    1489            6 :                                             tags,
    1490            6 :                                             last_text_prefix,
    1491            6 :                                             replace_text_at,
    1492            6 :                                             name_path_text_loc,
    1493            6 :                                         );
    1494            6 :                                     }
    1495              :                                 }
    1496            1 :                                 ReferencedTable::Unresolved(_) => {}
    1497              :                             }
    1498           33 :                             break;
    1499            3 :                         }
    1500              :                     }
    1501            0 :                 }
    1502              :             }
    1503            0 :             CursorContext::None => {}
    1504              :         }
    1505              : 
    1506          118 :         for mut dot_candidate in dot_candidates {
    1507          116 :             if let Some(&co) = self.candidate_objects_by_id.get(&dot_candidate.object_id) {
    1508              :                 // Update the tags and the replacement target
    1509            0 :                 self.candidate_objects[co].candidate_tags |= dot_candidate.candidate_tags;
    1510            0 :                 let candidate = self.candidate_objects[co].candidate;
    1511            0 :                 self.candidates[candidate].target_location = replace_text_at;
    1512            0 :                 self.candidates[candidate].target_location_qualified = name_path_text_loc;
    1513            0 :                 continue;
    1514          116 :             }
    1515          116 :             if !last_text_prefix.is_empty() {
    1516           68 :                 if let Some(t) = match_tags(dot_candidate.name.as_bytes(), last_text_prefix) {
    1517           58 :                     dot_candidate.candidate_tags |= t;
    1518           58 :                 }
    1519           48 :             }
    1520          116 :             let candidate = match self.candidates_by_name.get(&dot_candidate.name) {
    1521            1 :                 Some(&c) => {
    1522            1 :                     let existing = &mut self.candidates[c];
    1523            1 :                     existing.target_location = replace_text_at;
    1524            1 :                     existing.target_location_qualified = name_path_text_loc;
    1525              :                     // A name can be several things, e.g. a table and a schema
    1526            1 :                     existing.coarse_name_tags |= dot_candidate.name_tags;
    1527            1 :                     existing.candidate_tags |= dot_candidate.candidate_tags;
    1528            1 :                     c
    1529              :                 }
    1530              :                 None => {
    1531          115 :                     self.candidates.push(Candidate::new(
    1532          115 :                         dot_candidate.name.clone(),
    1533          115 :                         dot_candidate.name_tags,
    1534          115 :                         dot_candidate.candidate_tags,
    1535          115 :                         replace_text_at,
    1536          115 :                         name_path_text_loc,
    1537              :                     ));
    1538          115 :                     let c = self.candidates.len() - 1;
    1539          115 :                     self.candidates_by_name.insert(dot_candidate.name.clone(), c);
    1540          115 :                     c
    1541              :                 }
    1542              :             };
    1543          116 :             self.add_object(candidate, dot_candidate.candidate_tags, dot_candidate.object_id, dot_candidate.object);
    1544              :         }
    1545          101 :     }
    1546              : 
    1547              :     /// Score all candidates and select the top k
    1548          445 :     fn select_top_candidates(&mut self) {
    1549          445 :         let scores = name_tag_scores(self.strategy);
    1550              :         // Objects with higher scores first, then in the order they were found
    1551          445 :         let mut object_heap =
    1552          445 :             TopKHeap::new(24, |l: &(usize, Score), r: &(usize, Score)| l.1.cmp(&r.1).then(r.0.cmp(&l.0)));
    1553          491 :         for i in 0..self.candidates.len() {
    1554              :             // The base score is the maximum among the name tags
    1555          352 :             let tags = self.candidates[i].coarse_name_tags;
    1556          352 :             let mut base_score = 0;
    1557         2464 :             for (tag, tag_score) in scores {
    1558         2464 :                 base_score = base_score.max(if tags & tag.0 != 0 { tag_score } else { 0 });
    1559              :             }
    1560              :             // The top catalog objects
    1561          352 :             object_heap.clear();
    1562          352 :             for &co in &self.candidates[i].catalog_objects {
    1563          321 :                 let score = compute_candidate_score(self.candidate_objects[co].candidate_tags);
    1564          321 :                 self.candidate_objects[co].score = score;
    1565          321 :                 object_heap.insert((co, score));
    1566          321 :             }
    1567          352 :             let objects: Vec<(usize, Score)> = object_heap.finish().clone();
    1568          352 :             let candidate = &mut self.candidates[i];
    1569          352 :             candidate.catalog_objects = objects.iter().map(|&(co, _)| co).collect();
    1570              :             // The score of the last, i.e. the lowest, of the top objects (see docs/design/core_quirks.md)
    1571          352 :             let object_score = match objects.last() {
    1572          305 :                 Some(&(_, score)) => score,
    1573           47 :                 None => compute_candidate_score(candidate.candidate_tags),
    1574              :             };
    1575          352 :             candidate.score = base_score + object_score;
    1576          352 :             let candidate = candidate.clone();
    1577          352 :             self.candidate_heap.insert(candidate);
    1578              :         }
    1579          445 :         self.candidate_heap.finish();
    1580          445 :         self.top_candidates = self.candidate_heap.take_entries();
    1581          445 :     }
    1582              : 
    1583              :     /// The qualified name of a table
    1584          249 :     fn qualified_table_name(name: &QualifiedTableName) -> Vec<Arc<str>> {
    1585          249 :         let mut names = Vec::with_capacity(4);
    1586          249 :         if !name.database_name.text.is_empty() {
    1587           50 :             names.push(name.database_name.text.clone());
    1588           50 :             names.push(name.schema_name.text.clone());
    1589           50 :             names.push(name.table_name.text.clone());
    1590          199 :         } else if !name.schema_name.text.is_empty() {
    1591           28 :             names.push(name.schema_name.text.clone());
    1592           28 :             names.push(name.table_name.text.clone());
    1593          171 :         } else if !name.table_name.text.is_empty() {
    1594          171 :             names.push(name.table_name.text.clone());
    1595          171 :         }
    1596          249 :         names
    1597          249 :     }
    1598              : 
    1599              :     /// Derive qualified names of the top candidates
    1600          391 :     fn qualify_top_candidates(&mut self) {
    1601              :         // The column candidates by the table that defines them: (table id, candidate object)
    1602          391 :         let mut columns_by_table: Vec<(QualifiedCatalogObjectId, usize)> = Vec::new();
    1603         5172 :         for top in &self.top_candidates {
    1604         5172 :             let mut column_count_in_scope = 0;
    1605         5172 :             let mut table_count = 0;
    1606         5172 :             for &co in &top.catalog_objects {
    1607          255 :                 let object = &self.candidate_objects[co];
    1608          255 :                 let in_scope = has(object.candidate_tags, CandidateTag::IN_NAME_SCOPE);
    1609          255 :                 match object.object {
    1610          126 :                     CandidateObjectRef::Column(loc) => {
    1611          126 :                         column_count_in_scope += in_scope as usize;
    1612          126 :                         columns_by_table.push((
    1613          126 :                             QualifiedCatalogObjectId::table(object.catalog_object_id.unpack_table_column_id().0),
    1614          126 :                             co,
    1615          126 :                         ));
    1616          126 :                         let table = self.table(loc.table);
    1617          126 :                         let qualified = match table {
    1618          126 :                             Some(table) => {
    1619          126 :                                 let mut names = Self::qualified_table_name(&table.table_name);
    1620          126 :                                 names.push(table.table_columns[loc.column as usize].column_name.text.clone());
    1621          126 :                                 names
    1622              :                             }
    1623            0 :                             None => Vec::new(),
    1624              :                         };
    1625          126 :                         let object = &mut self.candidate_objects[co];
    1626          126 :                         object.qualified_name_target_idx = qualified.len().saturating_sub(1);
    1627          126 :                         object.qualified_name = qualified;
    1628              :                     }
    1629           70 :                     CandidateObjectRef::Table(loc) => {
    1630           70 :                         table_count += 1;
    1631           70 :                         let qualified =
    1632           70 :                             self.table(loc).map(|t| Self::qualified_table_name(&t.table_name)).unwrap_or_default();
    1633           70 :                         let object = &mut self.candidate_objects[co];
    1634           70 :                         object.qualified_name_target_idx = qualified.len().saturating_sub(1);
    1635           70 :                         object.qualified_name = qualified;
    1636              :                     }
    1637           11 :                     CandidateObjectRef::Function(loc) => {
    1638           11 :                         let qualified = self
    1639           11 :                             .entry_data(loc.entry)
    1640           11 :                             .and_then(|d| d.function_declarations.get(loc.index as usize))
    1641           11 :                             .map(|f| {
    1642           11 :                                 let name = &f.function_name;
    1643           11 :                                 let mut names = Vec::with_capacity(3);
    1644           11 :                                 if !name.database_name.text.is_empty() {
    1645            3 :                                     names.push(name.database_name.text.clone());
    1646            3 :                                     names.push(name.schema_name.text.clone());
    1647            3 :                                     names.push(name.function_name.text.clone());
    1648            9 :                                 } else if !name.schema_name.text.is_empty() {
    1649            2 :                                     names.push(name.schema_name.text.clone());
    1650            2 :                                     names.push(name.function_name.text.clone());
    1651            6 :                                 } else if !name.function_name.text.is_empty() {
    1652            6 :                                     names.push(name.function_name.text.clone());
    1653            6 :                                 }
    1654           11 :                                 names
    1655           11 :                             })
    1656           11 :                             .unwrap_or_default();
    1657           11 :                         let object = &mut self.candidate_objects[co];
    1658           11 :                         object.qualified_name_target_idx = qualified.len().saturating_sub(1);
    1659           11 :                         object.qualified_name = qualified;
    1660              :                     }
    1661           48 :                     _ => {}
    1662              :                 }
    1663              :             }
    1664         5172 :             let prefer_qualified_columns = column_count_in_scope > 1;
    1665         5172 :             let prefer_qualified_tables = table_count > 1;
    1666         5172 :             for &co in &top.catalog_objects {
    1667          255 :                 let object = &mut self.candidate_objects[co];
    1668          255 :                 match object.object {
    1669          126 :                     CandidateObjectRef::Column(_) => object.prefer_qualified = prefer_qualified_columns,
    1670           70 :                     CandidateObjectRef::Table(_) => object.prefer_qualified = prefer_qualified_tables,
    1671           59 :                     _ => {}
    1672              :                 }
    1673              :             }
    1674              :         }
    1675              : 
    1676              :         // Qualify column candidates with the aliases of the table refs in scope
    1677          391 :         let Some(analyzed) = self.analyzed() else { return };
    1678          398 :         for &scope_id in &self.cursor.name_scopes {
    1679          288 :             let scope = &analyzed.name_scopes[scope_id as usize];
    1680          288 :             for &table_ref_id in &scope.table_references {
    1681          279 :                 let table_ref = &analyzed.table_references[table_ref_id as usize];
    1682          279 :                 let Some(resolved) = table_ref.inner.as_ref().and_then(|r| r.resolved_table.as_ref()) else { continue };
    1683          131 :                 let mut has_match = false;
    1684          149 :                 for &(table_id, co) in &columns_by_table {
    1685          132 :                     if table_id != resolved.catalog_table_id {
    1686           23 :                         continue;
    1687          109 :                     }
    1688          109 :                     has_match = true;
    1689          109 :                     let CandidateObjectRef::Column(loc) = self.candidate_objects[co].object else { continue };
    1690          109 :                     let column_name = match self.table(loc.table) {
    1691          109 :                         Some(t) => t.table_columns[loc.column as usize].column_name.text.clone(),
    1692            0 :                         None => continue,
    1693              :                     };
    1694          109 :                     let object = &mut self.candidate_objects[co];
    1695          109 :                     match &table_ref.alias {
    1696           56 :                         Some((alias, _)) => {
    1697           56 :                             object.qualified_name = vec![alias.text.clone(), column_name];
    1698           56 :                             object.qualified_name_target_idx = 1;
    1699           56 :                             object.prefer_qualified = true;
    1700           56 :                         }
    1701           53 :                         None => {
    1702           53 :                             let mut names = Self::qualified_table_name(&resolved.table_name);
    1703           53 :                             names.push(column_name);
    1704           53 :                             object.qualified_name_target_idx = names.len() - 1;
    1705           53 :                             object.qualified_name = names;
    1706           53 :                         }
    1707              :                     }
    1708              :                 }
    1709          131 :                 if has_match {
    1710          123 :                     columns_by_table.retain(|&(table_id, _)| table_id != resolved.catalog_table_id);
    1711           54 :                 }
    1712              :             }
    1713              :         }
    1714          391 :     }
    1715              : 
    1716              :     /// Derive keyword continuations of the top candidates
    1717          442 :     fn derive_keyword_continuations(&mut self, prefix: &PrefixSnapshot) {
    1718          442 :         let Some(target) = self.target_symbol else { return };
    1719          442 :         let symbols = self.symbols();
    1720          442 :         let target_id = if self.between_symbols && !self.is_at_eof(target.symbol_id) {
    1721           51 :             self.next_symbol(target.symbol_id)
    1722              :         } else {
    1723          391 :             target.symbol_id
    1724              :         };
    1725          442 :         let (feed_id, replace_target) = self.feed_point();
    1726              : 
    1727              :         // The single keyword, or the uniquely best scored continuation
    1728         5490 :         let find_continuation = |expected: &[SymbolKind]| -> Option<SymbolKind> {
    1729         5490 :             let mut best = None;
    1730         5490 :             let mut best_score = 0;
    1731         5490 :             let mut best_count = 0;
    1732         5490 :             let mut keyword_count = 0;
    1733         5490 :             let mut unique = None;
    1734      1137398 :             for &sym in expected {
    1735      1137398 :                 if Keyword::symbol_text(sym).is_none_or(|t| t.is_empty()) {
    1736        44052 :                     continue;
    1737      1093346 :                 }
    1738      1093346 :                 keyword_count += 1;
    1739      1093346 :                 unique = Some(sym);
    1740      1093346 :                 let score = keyword_continuation_score(sym);
    1741      1093346 :                 if score > best_score {
    1742         6625 :                     best_score = score;
    1743         6625 :                     best = Some(sym);
    1744         6625 :                     best_count = 1;
    1745      1086721 :                 } else if score == best_score && score > 0 {
    1746         6000 :                     best_count += 1;
    1747      1080721 :                 }
    1748              :             }
    1749         5490 :             if keyword_count == 1 {
    1750          610 :                 return unique;
    1751         4880 :             }
    1752         4880 :             if best_count == 1 {
    1753          337 :                 return best;
    1754         4543 :             }
    1755         4543 :             None
    1756         5490 :         };
    1757              :         // Keep a continuation only if the tokens after the cursor still parse after it
    1758          442 :         let has_post_cursor_token = self.has_post_cursor_token;
    1759         1021 :         let fits_suffix = |candidate: SymbolKind, continuation: SymbolKind| -> bool {
    1760          947 :             if !has_post_cursor_token {
    1761          904 :                 return true;
    1762           43 :             }
    1763           43 :             let probe =
    1764           43 :                 probe::probe_suffix_sequence(symbols, feed_id, prefix, &[candidate, continuation], replace_target);
    1765           43 :             probe.k1_compatible || probe.reached_eof
    1766          947 :         };
    1767              : 
    1768          442 :         let mut ident_continuation: Option<Option<&'static str>> = None;
    1769         5666 :         for candidate in self.top_candidates.iter_mut() {
    1770         5666 :             match candidate.keyword_symbol {
    1771         5230 :                 Some(keyword) => {
    1772         5230 :                     let expected = probe::parse_until_after(symbols, target_id, keyword);
    1773         5230 :                     let Some(continuation) = find_continuation(&expected) else { continue };
    1774          930 :                     if !fits_suffix(keyword, continuation) {
    1775           34 :                         continue;
    1776          896 :                     }
    1777          896 :                     candidate.keyword_continuation = Keyword::symbol_text(continuation);
    1778              :                 }
    1779              :                 None => {
    1780          436 :                     let text = *ident_continuation.get_or_insert_with(|| {
    1781          260 :                         let expected = probe::parse_until_after(symbols, target_id, S_IDENT);
    1782          260 :                         find_continuation(&expected).filter(|&c| fits_suffix(S_IDENT, c)).and_then(Keyword::symbol_text)
    1783          260 :                     });
    1784          436 :                     if let Some(text) = text.filter(|t| !t.is_empty()) {
    1785           12 :                         candidate.keyword_continuation = Some(text);
    1786          424 :                     }
    1787              :                 }
    1788              :             }
    1789              :         }
    1790          442 :     }
    1791              : }
        

Generated by: LCOV version 2.0-1