LCOV - code coverage report
Current view: top level - dashql-core/src - completion_clfn.rs Coverage Total Hit
Test: DashQL d805a4b Lines: 94.5 % 330 312
Test Date: 2026-11-09 20:22:13

            Line data    Source code
       1              : //! Completion inside a `CALL` of a namespace with client functions: its functions, options and nested attributes.
       2              : //!
       3              : //! The text being typed usually doesn't parse, so the position comes from the tokens, not the tree. A walk from the
       4              : //! start of the statement keeps a stack with the schema of every open `(` and `[`. A wrong decision here only affects
       5              : //! suggestions, since nothing of this is part of parsing.
       6              : 
       7              : use crate::catalog::clfn::{ClientFunctionAttribute, ClientFunctionEffect, ClientFunctionField, ClientFunction, ClientFunctionSchema, ClientFunctionSpec, ClientFunctionUnknownPolicy, OPTION_NEXT, OPTION_PAGES, OPTION_POLL, OPTION_WAIT};
       8              : use crate::catalog::CatalogSnapshot;
       9              : use crate::completion::{Candidate, CandidateField, CandidateInfo, CandidateTags, Completion, Score};
      10              : use crate::utils::quote_identifier;
      11              : use dashql_buffers::analyzer::NameTag;
      12              : use dashql_buffers::completion::CandidateTag;
      13              : use dashql_buffers::parser::SymbolSpan;
      14              : use dashql_parser::scanner::Symbol;
      15              : use dashql_parser::symbol_kind::*;
      16              : use dashql_parser::keywords::Keyword;
      17              : use dashql_parser::SymbolKind;
      18              : use std::sync::Arc;
      19              : 
      20              : /// The score of a prefix match, it ranks before every substring match
      21              : const PREFIX_BONUS: Score = 1 << 20;
      22              : /// The score of a required option over an optional one that matches equally well
      23              : const REQUIRED_BONUS: Score = 1 << 18;
      24              : /// The score of an option of the spec over a standard option
      25              : const SPEC_OPTION_BONUS: Score = 1;
      26              : /// The score of a function that only reads over one that changes something
      27              : const READ_FUNCTION_BONUS: Score = 1;
      28              : 
      29              : /// Can a token be the name of a function, an option or an attribute?
      30          128 : fn is_name(kind: SymbolKind) -> bool {
      31          128 :     kind == S_IDENT || Keyword::name_of(kind).is_some_and(|name| !name.is_empty())
      32          128 : }
      33              : 
      34              : /// What an open bracket holds
      35              : enum Frame<'s> {
      36              :     /// The options of the function
      37              :     Options(&'s ClientFunction),
      38              :     /// The fields of an object: its schema, None for a free-form object
      39              :     Fields(Option<&'s ClientFunctionSchema>),
      40              :     /// The map of an attribute: its keys are free, the attribute holds for its values
      41              :     Map(&'s ClientFunctionAttribute),
      42              :     /// The items of an array of the attribute, and is the array a value of its map?
      43              :     Items(Option<&'s ClientFunctionAttribute>, bool),
      44              :     /// Anything else, such as the arguments of a function
      45              :     Opaque,
      46              : }
      47              : 
      48              : /// A frame and its names so far
      49              : struct Scope<'s> {
      50              :     frame: Frame<'s>,
      51              :     /// The names that are used already in lower case, with their values if they are strings
      52              :     used: Vec<ClientFunctionField>,
      53              :     /// The attribute of the value being read
      54              :     attribute: Option<&'s ClientFunctionAttribute>,
      55              :     /// Is the next token a key?
      56              :     at_key: bool,
      57              : }
      58              : 
      59              : impl<'s> Scope<'s> {
      60           49 :     fn new(frame: Frame<'s>) -> Self {
      61           49 :         let at_key = matches!(frame, Frame::Options(_) | Frame::Fields(_) | Frame::Map(_));
      62           49 :         Self { frame, used: Vec::new(), attribute: None, at_key }
      63           49 :     }
      64              : 
      65              :     /// The attributes that a key selects from and if they are required. A union offers what its variants that still
      66              :     /// fit take, and an attribute is only required if all of them need it.
      67           52 :     fn attributes(&self, spec: &'s ClientFunctionSpec) -> Vec<(&'s ClientFunctionAttribute, bool)> {
      68           15 :         match self.frame {
      69          202 :             Frame::Options(function) => function.options.iter().map(|o| (o, o.required)).collect(),
      70           15 :             Frame::Fields(Some(schema)) => variant_attributes(&spec.fit(schema, &self.used).variants),
      71            3 :             _ => Vec::new(),
      72              :         }
      73           52 :     }
      74              : }
      75              : 
      76              : /// What variants take, and if each attribute is required: an attribute is only required if all of them need it
      77           22 : fn variant_attributes<'s>(variants: &[&'s ClientFunctionSchema]) -> Vec<(&'s ClientFunctionAttribute, bool)> {
      78           22 :     let mut out: Vec<(&'s ClientFunctionAttribute, bool)> = Vec::new();
      79           50 :     for attribute in variants.iter().flat_map(|v| v.attributes.iter()) {
      80           50 :         if !out.iter().any(|(a, _)| a.name == attribute.name) {
      81           85 :             let required = variants.iter().all(|v| v.attributes.iter().any(|a| a.name == attribute.name && a.required));
      82           48 :             out.push((attribute, required));
      83            2 :         }
      84              :     }
      85           22 :     out
      86           22 : }
      87              : 
      88              : /// Where the cursor is
      89              : enum Position<'s> {
      90              :     /// After `CALL`: the namespaces
      91              :     Namespace,
      92              :     /// After the namespace: its functions
      93              :     Function(&'s ClientFunctionSpec),
      94              :     /// A key of a scope
      95              :     Key(&'s ClientFunctionSpec, Scope<'s>),
      96              :     /// Somewhere that takes no candidates
      97              :     Nothing,
      98              :     /// Not in a call of a namespace with a spec
      99              :     Outside,
     100              : }
     101              : 
     102              : /// Does an opening parenthesis start embedded SQL? `(select = 1)` is an object, `(select 1)` a query.
     103           18 : fn starts_embedded_sql(symbols: &[Symbol], paren: usize) -> bool {
     104           18 :     let mut next = paren + 1;
     105           18 :     while symbols.get(next).is_some_and(|s| s.kind == S_LRB) {
     106            0 :         next += 1;
     107            0 :     }
     108           18 :     let Some(first) = symbols.get(next) else { return false };
     109           10 :     let follower = symbols.get(next + 1).map(|s| s.kind);
     110           10 :     match first.kind {
     111            3 :         S_SELECT | S_WITH | S_WITH_LA | S_TABLE => follower != Some(S_EQUALS),
     112            0 :         S_VALUES => follower == Some(S_LRB),
     113            7 :         _ => false,
     114              :     }
     115           18 : }
     116              : 
     117              : /// The text of a token in lower case
     118          101 : fn token_text(completion: &Completion, symbol: &Symbol) -> String {
     119          101 :     let text = completion.text_at(symbol.location.offset, symbol.location.length);
     120          101 :     String::from_utf8_lossy(text).trim_matches('"').to_ascii_lowercase()
     121          101 : }
     122              : 
     123              : /// Find the position in the tokens of a statement before the target token
     124           42 : fn find_position<'s>(completion: &Completion, specs: &'s CatalogSnapshot, symbols: &[Symbol]) -> Position<'s> {
     125           42 :     let mut i = 0;
     126           42 :     if symbols.get(i).is_some_and(|s| s.kind == S_EXPLAIN) {
     127            1 :         i += 1;
     128           41 :     }
     129           42 :     if symbols.get(i).is_none_or(|s| s.kind != S_CALL_P) {
     130            2 :         return Position::Outside;
     131           40 :     }
     132           40 :     i += 1;
     133              :     // The namespace
     134           40 :     if i == symbols.len() {
     135            2 :         return Position::Namespace;
     136           38 :     }
     137           38 :     let Some(namespace) = symbols.get(i).filter(|s| is_name(s.kind)) else { return Position::Outside };
     138           38 :     let Some(spec) = specs.client_function_spec(&token_text(completion, namespace)) else { return Position::Outside };
     139           37 :     i += 1;
     140           37 :     let Some(dot) = symbols.get(i) else { return Position::Outside };
     141           37 :     if dot.kind != S_DOT && dot.kind != S_DOT_TRAILING {
     142            0 :         return Position::Outside;
     143           37 :     }
     144           37 :     i += 1;
     145              :     // The function
     146           37 :     let Some(function) = symbols.get(i) else { return Position::Function(spec) };
     147           30 :     let Some(function) = Some(function).filter(|s| is_name(s.kind)).and_then(|s| spec.function(&token_text(completion, s))) else {
     148            0 :         return Position::Nothing;
     149              :     };
     150           30 :     i += 1;
     151           30 :     let Some(open) = symbols.get(i) else { return Position::Nothing };
     152           30 :     if open.kind != S_LRB {
     153            0 :         return Position::Nothing;
     154           30 :     }
     155           30 :     i += 1;
     156              : 
     157           30 :     let mut stack = vec![Scope::new(Frame::Options(function))];
     158          122 :     while let Some(symbol) = symbols.get(i) {
     159           93 :         let Some(top) = stack.last_mut() else { return Position::Outside };
     160           18 :         match symbol.kind {
     161              :             S_RRB | S_RSB => {
     162            1 :                 stack.pop();
     163            1 :                 if stack.is_empty() {
     164            0 :                     return Position::Outside;
     165            1 :                 }
     166              :             }
     167              :             S_COMMA => {
     168            9 :                 top.attribute = top.attribute.filter(|_| matches!(top.frame, Frame::Items(..)));
     169            9 :                 top.at_key = matches!(top.frame, Frame::Options(_) | Frame::Fields(_) | Frame::Map(_));
     170              :             }
     171           18 :             S_LRB if matches!(top.frame, Frame::Opaque) => stack.push(Scope::new(Frame::Opaque)),
     172            0 :             S_LRB if top.at_key => stack.push(Scope::new(Frame::Opaque)),
     173              :             S_LRB => {
     174              :                 // A name before the parenthesis is a function, the analyzer rejects it
     175           18 :                 if i > 0 && is_name(symbols[i - 1].kind) {
     176            0 :                     stack.push(Scope::new(Frame::Opaque));
     177           18 :                 } else if starts_embedded_sql(symbols, i) {
     178              :                     // Skip the query. If the cursor is inside it, core SQL completes it.
     179            2 :                     let mut depth = 0;
     180           11 :                     while let Some(s) = symbols.get(i) {
     181           10 :                         match s.kind {
     182            3 :                             S_LRB => depth += 1,
     183            2 :                             S_RRB => depth -= 1,
     184            5 :                             _ => {}
     185              :                         }
     186           10 :                         if depth == 0 {
     187            1 :                             break;
     188            9 :                         }
     189            9 :                         i += 1;
     190              :                     }
     191            2 :                     if depth != 0 {
     192            1 :                         return Position::Outside;
     193            1 :                     }
     194              :                 } else {
     195           16 :                     let (attribute, in_map) = match top.frame {
     196            3 :                         Frame::Items(attribute, in_map) => (attribute, in_map),
     197            3 :                         Frame::Map(attribute) => (Some(attribute), true),
     198           10 :                         _ => (top.attribute, false),
     199              :                     };
     200           16 :                     match attribute.filter(|a| a.map && !in_map) {
     201            4 :                         Some(map) => stack.push(Scope::new(Frame::Map(map))),
     202              :                         None => {
     203           12 :                             let schema = attribute.and_then(|a| a.schema.as_deref()).and_then(|s| spec.schema(s));
     204           12 :                             stack.push(Scope::new(Frame::Fields(schema)));
     205              :                         }
     206              :                     }
     207              :                 }
     208              :             }
     209              :             S_LSB => {
     210            3 :                 let (attribute, in_map) = match top.frame {
     211            0 :                     Frame::Items(attribute, in_map) => (attribute, in_map),
     212            0 :                     Frame::Map(attribute) => (Some(attribute), true),
     213            0 :                     Frame::Opaque => (None, false),
     214            3 :                     _ => (top.attribute, false),
     215              :                 };
     216            3 :                 stack.push(Scope::new(Frame::Items(attribute, in_map)));
     217              :             }
     218           27 :             S_EQUALS | S_EQUALS_GREATER => {}
     219           27 :             _ if top.at_key => {
     220              :                 // A key: remember its attribute for the value that follows
     221           27 :                 let text = token_text(completion, symbol);
     222           27 :                 top.used.push(ClientFunctionField { name: text.clone(), ..Default::default() });
     223           81 :                 top.attribute = top.attributes(spec).into_iter().map(|(a, _)| a).find(|a| a.name.eq_ignore_ascii_case(&text));
     224           27 :                 top.at_key = false;
     225              :             }
     226              :             // The value of the key, it can select the variant of a union
     227              :             S_SCONST => {
     228            6 :                 if let Some(field) = top.used.last_mut() {
     229            6 :                     field.text = Some(token_text(completion, symbol).trim_matches('\'').to_string());
     230            6 :                 }
     231              :             }
     232            2 :             _ => {}
     233              :         }
     234           92 :         i += 1;
     235              :     }
     236           29 :     let Some(top) = stack.pop() else { return Position::Outside };
     237           29 :     if top.at_key { Position::Key(spec, top) } else { Position::Nothing }
     238           42 : }
     239              : 
     240              : /// A name as it is inserted in a script
     241          101 : fn insert_text(name: &str) -> Arc<str> {
     242              :     // Salesforce-style names keep their case, they scan fine without quotes
     243          693 :     let plain = name.bytes().enumerate().all(|(i, c)| c.is_ascii_alphabetic() || c == b'_' || (i > 0 && c.is_ascii_digit()));
     244          101 :     if plain { Arc::from(name) } else { Arc::from(quote_identifier(name)) }
     245          101 : }
     246              : 
     247              : /// What a candidate of a call is
     248              : #[derive(Clone, Copy)]
     249              : enum Source<'s> {
     250              :     Namespace(&'s ClientFunctionSpec),
     251              :     Function(&'s ClientFunctionSpec, &'s ClientFunction),
     252              :     /// An option or attribute, and is it required here? In a union that depends on the variants that still fit.
     253              :     Attribute(&'s ClientFunctionSpec, &'s ClientFunctionAttribute, bool),
     254              :     StandardOption(&'static str),
     255              : }
     256              : 
     257              : impl Source<'_> {
     258          101 :     fn name_tags(self) -> u8 {
     259          101 :         match self {
     260            2 :             Source::Namespace(_) => NameTag::SCHEMA_NAME.0,
     261           31 :             Source::Function(..) => NameTag::FUNCTION_NAME.0,
     262           68 :             Source::Attribute(..) | Source::StandardOption(_) => NameTag::OPTION_NAME.0,
     263              :         }
     264          101 :     }
     265              : 
     266              :     /// What a list shows about the candidate
     267           88 :     fn info(self) -> CandidateInfo {
     268           88 :         match self {
     269            2 :             Source::Namespace(spec) => CandidateInfo { detail: spec.title.clone(), ..Default::default() },
     270           25 :             Source::Function(spec, function) => function_info(spec, function),
     271           50 :             Source::Attribute(spec, attribute, required) => attribute_info(spec, attribute, required),
     272           11 :             Source::StandardOption(option) => standard_option_info(option),
     273              :         }
     274           88 :     }
     275              : }
     276              : 
     277              : /// A fact of the details: a label and a value
     278           27 : fn fact(label: &str, value: impl Into<String>) -> CandidateField {
     279           27 :     CandidateField { name: label.to_string(), detail: Some(value.into()), description: None }
     280           27 : }
     281              : 
     282              : /// The table of options or attributes, the required ones first
     283           32 : fn attribute_fields(attributes: &[(&ClientFunctionAttribute, bool)]) -> Vec<CandidateField> {
     284           32 :     let required = attributes.iter().filter(|(_, required)| *required);
     285           53 :     let optional = attributes.iter().filter(|(_, required)| !*required);
     286           32 :     required
     287           32 :         .chain(optional)
     288           53 :         .map(|(a, required)| CandidateField { name: a.name.clone(), detail: attribute_detail(a, *required), description: a.description.clone() })
     289           32 :         .collect()
     290           32 : }
     291              : 
     292              : /// The signature of a function: the options of the spec with the required ones first, `?` marks an optional one. A
     293              : /// function without options has none, and neither does the escape hatch, which takes any option.
     294           25 : fn function_signature(function: &ClientFunction) -> Option<String> {
     295           25 :     if function.raw || function.options.is_empty() {
     296            8 :         return None;
     297           17 :     }
     298           17 :     let required = function.options.iter().filter(|o| o.required);
     299           39 :     let optional = function.options.iter().filter(|o| !o.required);
     300           17 :     let options: Vec<String> = required
     301           17 :         .chain(optional)
     302           39 :         .map(|option| {
     303           39 :             let name = if option.required { option.name.clone() } else { format!("{}?", option.name) };
     304           39 :             match attribute_detail(option, false) {
     305           34 :                 Some(value) => format!("{name} {value}"),
     306            5 :                 None => name,
     307              :             }
     308           39 :         })
     309           17 :         .collect();
     310           17 :     Some(format!("({})", options.join(", ")))
     311           25 : }
     312              : 
     313              : /// What a list shows about a function
     314           25 : fn function_info(spec: &ClientFunctionSpec, function: &ClientFunction) -> CandidateInfo {
     315           39 :     let options: Vec<(&ClientFunctionAttribute, bool)> = function.options.iter().map(|o| (o, o.required)).collect();
     316           25 :     let mut parameters = attribute_fields(&options);
     317           25 :     parameters.extend(function.standard_options(spec).into_iter().map(|option| {
     318           12 :         let info = standard_option_info(option);
     319           12 :         CandidateField { name: option.to_string(), detail: info.detail, description: info.description }
     320           12 :     }));
     321           25 :     let columns = function
     322           25 :         .columns
     323           25 :         .iter()
     324           25 :         .map(|column| CandidateField { name: column.name.clone(), detail: column.column_type.clone(), description: None })
     325           25 :         .collect();
     326           25 :     let mut facts = Vec::new();
     327           25 :     if let Some(family) = &function.family {
     328            6 :         facts.push(fact("Family", family));
     329           19 :     }
     330           25 :     if let (Some(method), Some(path)) = (&function.method, &function.path) {
     331            9 :         facts.push(fact("Endpoint", format!("{method} {path}")));
     332           20 :     }
     333           25 :     if let Some(paging) = &function.paging {
     334            6 :         facts.push(fact("Paging", &paging.kind));
     335           19 :     }
     336           25 :     CandidateInfo {
     337           25 :         detail: function_signature(function),
     338           25 :         description: function.description.clone(),
     339           25 :         parameters,
     340           25 :         result_description: function.result_description.clone(),
     341           25 :         columns,
     342           25 :         example: function.example.clone(),
     343           25 :         facts,
     344           25 :     }
     345           25 : }
     346              : 
     347              : /// What a list shows about an option or attribute. A schema lists its attributes one level deep, the list of the object
     348              : /// shows the next level when someone writes it.
     349           50 : fn attribute_info(spec: &ClientFunctionSpec, attribute: &ClientFunctionAttribute, required: bool) -> CandidateInfo {
     350           50 :     let schema = attribute.schema.as_deref().and_then(|name| spec.schema(name));
     351           50 :     let mut facts = Vec::new();
     352           50 :     if !attribute.values.is_empty() {
     353            6 :         let values: Vec<String> = attribute.values.iter().map(|v| v.as_str().map_or_else(|| v.to_string(), str::to_string)).collect();
     354            3 :         facts.push(fact("Values", values.join(", ")));
     355           47 :     }
     356           50 :     if let Some(format) = &attribute.format {
     357            3 :         facts.push(fact("Format", format));
     358           47 :     }
     359              :     CandidateInfo {
     360           50 :         detail: attribute_detail(attribute, required),
     361           50 :         description: attribute.description.clone(),
     362           50 :         parameters: schema.map_or_else(Vec::new, |s| attribute_fields(&variant_attributes(&spec.fit(s, &[]).variants))),
     363           50 :         facts,
     364           50 :         ..Default::default()
     365              :     }
     366           50 : }
     367              : 
     368              : /// The type of an option or attribute. A schema has none to show: its name says nothing in a script, and a list shows
     369              : /// its attributes instead.
     370          142 : fn attribute_detail(attribute: &ClientFunctionAttribute, required: bool) -> Option<String> {
     371          142 :     let arrays = "array of ".repeat(attribute.array as usize);
     372          142 :     let value = match attribute.value_type.as_deref().filter(|_| attribute.schema.is_none()) {
     373          116 :         Some(value) => Some(format!("{arrays}{value}")),
     374           26 :         None if attribute.array > 0 => Some(arrays.trim_end_matches(" of ").to_string()),
     375           15 :         None => None,
     376              :     };
     377          142 :     let value = match (attribute.map, value) {
     378           12 :         (true, Some(value)) => Some(format!("map of {value}")),
     379            0 :         (true, None) => Some("map".to_string()),
     380          130 :         (false, value) => value,
     381              :     };
     382          142 :     match (value, required) {
     383           28 :         (Some(value), true) => Some(format!("{value} ยท required")),
     384            0 :         (None, true) => Some("required".to_string()),
     385          114 :         (value, false) => value,
     386              :     }
     387          142 : }
     388              : 
     389              : /// What a list shows about a standard option, no spec describes them
     390           23 : fn standard_option_info(option: &str) -> CandidateInfo {
     391           23 :     let (detail, description) = match option {
     392           23 :         OPTION_NEXT => ("string", "The page to read, the `_next` column of an earlier result. `null` means the first page."),
     393           13 :         OPTION_PAGES => ("integer", "The number of pages to read, 1 without this option."),
     394            2 :         OPTION_POLL => ("string", "The pending result to poll, the `_poll` column of an earlier result."),
     395            1 :         OPTION_WAIT => ("integer", "The seconds to poll a pending result for, no polling without this option."),
     396            0 :         _ => return CandidateInfo::default(),
     397              :     };
     398           23 :     CandidateInfo { detail: Some(detail.to_string()), description: Some(description.to_string()), ..Default::default() }
     399           23 : }
     400              : 
     401              : /// The candidates at a cursor
     402              : struct Candidates<'s> {
     403              :     /// The typed prefix in lower case
     404              :     prefix: String,
     405              :     /// The text range that a candidate replaces
     406              :     target: SymbolSpan,
     407              :     /// The candidates and what they are
     408              :     out: Vec<(Candidate, Source<'s>)>,
     409              : }
     410              : 
     411              : impl<'s> Candidates<'s> {
     412              :     /// Add a candidate if it matches the prefix, the bonus ranks the candidates that match equally well
     413          144 :     fn add(&mut self, name: &str, bonus: Score, source: Source<'s>) {
     414          144 :         let lower = name.to_ascii_lowercase();
     415          144 :         let (tags, bonus): (CandidateTags, Score) = if self.prefix.is_empty() || lower.starts_with(&self.prefix) {
     416           99 :             (CandidateTag::PREFIX_MATCH.0, bonus + PREFIX_BONUS)
     417           45 :         } else if lower.contains(&self.prefix) {
     418            2 :             (CandidateTag::SUBSTRING_MATCH.0, bonus)
     419              :         } else {
     420           43 :             return;
     421              :         };
     422          101 :         let mut candidate = Candidate::new(insert_text(name), source.name_tags(), tags, self.target, self.target);
     423          101 :         candidate.completion_text_is_verbatim = true;
     424          101 :         candidate.score = bonus;
     425          101 :         self.out.push((candidate, source));
     426          144 :     }
     427              : 
     428              :     /// The top k candidates: by score, then by name. Only these get their info, a namespace can have many functions.
     429           38 :     fn finish(mut self, k: usize) -> Vec<Candidate> {
     430           98 :         self.out.sort_by(|(l, _), (r, _)| r.score.cmp(&l.score).then_with(|| l.completion_text.cmp(&r.completion_text)));
     431           38 :         self.out.truncate(k);
     432           38 :         self.out
     433           38 :             .into_iter()
     434           88 :             .map(|(mut candidate, source)| {
     435           88 :                 candidate.info = source.info();
     436           88 :                 candidate
     437           88 :             })
     438           38 :             .collect()
     439           38 :     }
     440              : }
     441              : 
     442              : /// Complete at a cursor in a `CALL` of a namespace with client functions. Returns false if the cursor is elsewhere, or in a
     443              : /// query inside the call, which core SQL completes.
     444          507 : pub(crate) fn complete_client_function_call(completion: &mut Completion, k: usize) -> bool {
     445          507 :     let specs = completion.catalog();
     446          507 :     if specs.client_function_specs().is_empty() {
     447          465 :         return false;
     448           42 :     }
     449           42 :     let symbols = completion.symbols();
     450           42 :     let offset = completion.cursor.text_offset as u32;
     451              :     // The tokens that end before the cursor, and the name that the cursor is in or right behind
     452           42 :     let before = symbols
     453           42 :         .iter()
     454          342 :         .take_while(|s| s.kind != S_YYEOF && s.location.offset + s.location.length <= offset)
     455           42 :         .count();
     456           42 :     let containing = symbols.get(before).filter(|s| s.location.offset < offset && s.kind != S_YYEOF);
     457           42 :     if containing.is_some_and(|s| !is_name(s.kind)) {
     458            0 :         return false;
     459           42 :     }
     460           42 :     let (end, target) = match (containing, before.checked_sub(1).map(|i| &symbols[i])) {
     461            0 :         (Some(symbol), _) => (before, Some(symbol)),
     462              :         // Right behind a name
     463           42 :         (None, Some(previous)) if is_name(previous.kind) && previous.location.offset + previous.location.length == offset => {
     464           12 :             (before - 1, Some(previous))
     465              :         }
     466           30 :         _ => (before, None),
     467              :     };
     468          279 :     let statement_begin = symbols[..end].iter().rposition(|s| s.kind == S_SEMICOLON).map_or(0, |i| i + 1);
     469           42 :     let position = find_position(completion, specs, &symbols[statement_begin..end]);
     470           42 :     let (prefix, span) = match target {
     471           12 :         Some(symbol) => {
     472           12 :             let length = offset.saturating_sub(symbol.location.offset);
     473           12 :             let typed = completion.text_at(symbol.location.offset, length);
     474           12 :             let prefix = String::from_utf8_lossy(typed).trim_matches('"').to_ascii_lowercase();
     475           12 :             (prefix, SymbolSpan::new(symbol.location.offset, symbol.location.length))
     476              :         }
     477           30 :         None => (String::new(), SymbolSpan::new(offset, 0)),
     478              :     };
     479           42 :     let mut candidates = Candidates { prefix, target: span, out: Vec::new() };
     480           42 :     completion.client_function_name = matches!(position, Position::Function(_) | Position::Key(..));
     481           42 :     match position {
     482            4 :         Position::Outside => return false,
     483            1 :         Position::Nothing => {}
     484              :         // A procedure of the backend is possible as well, core SQL completes it if no namespace matches
     485              :         Position::Namespace => {
     486            2 :             for (namespace, spec) in specs.client_function_specs() {
     487            2 :                 candidates.add(namespace, 0, Source::Namespace(spec));
     488            2 :             }
     489            2 :             if candidates.out.is_empty() {
     490            0 :                 return false;
     491            2 :             }
     492              :         }
     493            7 :         Position::Function(spec) => {
     494           43 :             for function in &spec.functions {
     495           43 :                 candidates.add(
     496           43 :                     &function.name,
     497           43 :                     if function.effect() == ClientFunctionEffect::Read { READ_FUNCTION_BONUS } else { 0 },
     498           43 :                     Source::Function(spec, function),
     499              :                 );
     500              :             }
     501              :         }
     502           28 :         Position::Key(spec, scope) => {
     503           28 :             let free_form = match scope.frame {
     504            2 :                 Frame::Fields(None) | Frame::Map(_) => true,
     505           10 :                 Frame::Fields(Some(schema)) => schema.attributes.is_empty() && schema.unknown == ClientFunctionUnknownPolicy::Allow,
     506           16 :                 _ => false,
     507              :             };
     508           28 :             if !free_form {
     509          105 :                 let used = |name: &str| scope.used.iter().any(|u| u.name.eq_ignore_ascii_case(name));
     510           93 :                 for (attribute, required) in scope.attributes(spec).into_iter().filter(|(a, _)| !used(&a.name)) {
     511           87 :                     candidates.add(
     512           87 :                         &attribute.name,
     513           87 :                         if required { REQUIRED_BONUS } else { SPEC_OPTION_BONUS },
     514           87 :                         Source::Attribute(spec, attribute, required),
     515              :                     );
     516              :                 }
     517           25 :                 if let Frame::Options(function) = scope.frame {
     518           16 :                     for option in function.standard_options(spec).iter().filter(|o| !used(o)) {
     519           12 :                         candidates.add(option, 0, Source::StandardOption(option));
     520           12 :                     }
     521            9 :                 }
     522            3 :             }
     523              :         }
     524              :     }
     525           38 :     completion.top_candidates = candidates.finish(k);
     526           38 :     true
     527          507 : }
        

Generated by: LCOV version 2.0-1