1use sql_dialect_fmt_syntax::SyntaxKind;
13use std::ops::Range;
14
15mod data;
16mod generated;
17mod scan;
18mod spec;
19
20use data::{
21 keyword_template, language_template, property_template, type_info, StaticHover,
22 ROUTINE_OPTION_STOPS,
23};
24use scan::{spanned_tokens, token_at, SpannedToken};
25
26pub use data::{CREATE_PROCEDURE_DOCS, CREATE_TASK_DOCS, DATA_TYPES_DOCS};
27
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct Hover {
30 pub kind: HoverKind,
31 pub title: String,
32 pub body: String,
33 pub range: Range<usize>,
34 pub docs_url: Option<&'static str>,
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum HoverKind {
39 Keyword,
40 Type,
41 Procedure,
42 Task,
43 Language,
44 Property,
45 Feature,
48 Function,
50}
51
52pub fn hover_at(source: &str, offset: usize) -> Option<Hover> {
57 let tokens = spanned_tokens(source);
58 let index = token_at(&tokens, offset)?;
59 let token = tokens[index].clone();
60
61 if let Some(hover) = procedure_symbol_hover(source, &tokens, index) {
62 return Some(hover);
63 }
64 if let Some(hover) = task_symbol_hover(source, &tokens, index) {
65 return Some(hover);
66 }
67 if let Some(hover) = type_hover(&token) {
68 return Some(hover);
69 }
70 if let Some(hover) = language_hover(&token) {
71 return Some(hover);
72 }
73 if let Some(hover) = property_hover(&token) {
74 return Some(hover);
75 }
76 if let Some(hover) = spec::function_call_hover(&tokens, index) {
77 return Some(hover);
78 }
79 if let Some(hover) = keyword_hover(&token) {
80 return Some(hover);
81 }
82 spec::feature_hover(&tokens, index)
83}
84
85fn procedure_symbol_hover(
86 source: &str,
87 tokens: &[SpannedToken<'_>],
88 index: usize,
89) -> Option<Hover> {
90 let object = object_declaration(tokens, index, "PROCEDURE")?;
91 if word(tokens.get(object.keyword + 1)?, "SCOPED") {
92 return None;
93 }
94 let name_range = procedure_name_range(tokens, object.keyword, object.end)?;
95 if !name_range.contains(&index) {
96 return None;
97 }
98
99 let name = compact_token_text(tokens, name_range.clone());
100 let args = procedure_args(source, tokens, name_range.end, object.end)
101 .unwrap_or_else(|| String::from(""));
102 let returns = clause_after_keyword(
103 source,
104 tokens,
105 object.keyword,
106 object.end,
107 "RETURNS",
108 &[
109 "LANGUAGE",
110 "RUNTIME_VERSION",
111 "PACKAGES",
112 "IMPORTS",
113 "HANDLER",
114 "AS",
115 "COMMENT",
116 "EXECUTE",
117 ],
118 );
119 let language = value_after_keyword(source, tokens, object.keyword, object.end, "LANGUAGE");
120 let handler =
121 routine_option_after_keyword(source, tokens, object.keyword, object.end, "HANDLER");
122 let runtime = routine_option_after_keyword(
123 source,
124 tokens,
125 object.keyword,
126 object.end,
127 "RUNTIME_VERSION",
128 );
129 let packages =
130 routine_option_after_keyword(source, tokens, object.keyword, object.end, "PACKAGES");
131 let imports =
132 routine_option_after_keyword(source, tokens, object.keyword, object.end, "IMPORTS");
133 let target_path =
134 routine_option_after_keyword(source, tokens, object.keyword, object.end, "TARGET_PATH");
135
136 let mut lines = vec![format!("Stored procedure `{name}`.")];
137 if !args.is_empty() {
138 lines.push(format!("Arguments: `{args}`."));
139 }
140 if let Some(returns) = returns {
141 lines.push(format!("Returns: `{returns}`."));
142 }
143 if let Some(language) = language {
144 lines.push(format!("Language: `{language}`."));
145 }
146 if let Some(handler) = handler {
147 lines.push(format!("Handler: `{handler}`."));
148 }
149 if let Some(runtime) = runtime {
150 lines.push(format!("Runtime: `{runtime}`."));
151 }
152 if let Some(packages) = packages {
153 lines.push(format!("Packages: `{packages}`."));
154 }
155 if let Some(imports) = imports {
156 lines.push(format!("Imports: `{imports}`."));
157 }
158 if let Some(target_path) = target_path {
159 lines.push(format!("Target path: `{target_path}`."));
160 }
161 lines.push(String::from(
162 "Snowflake resolves stored procedures by name plus argument types.",
163 ));
164 lines.push(String::from(
165 "External-language procedures usually pair LANGUAGE with HANDLER, PACKAGES, IMPORTS, and RUNTIME_VERSION.",
166 ));
167
168 Some(Hover {
169 kind: HoverKind::Procedure,
170 title: format!("Stored procedure `{name}`"),
171 body: lines.join("\n"),
172 range: combined_range(tokens, name_range),
173 docs_url: Some(CREATE_PROCEDURE_DOCS),
174 })
175}
176
177fn task_symbol_hover(source: &str, tokens: &[SpannedToken<'_>], index: usize) -> Option<Hover> {
178 let object = object_declaration(tokens, index, "TASK")?;
179 let name_range = task_name_range(tokens, object.keyword, object.end)?;
180 if !name_range.contains(&index) {
181 return None;
182 }
183
184 let name = compact_token_text(tokens, name_range.clone());
185 let warehouse = value_after_keyword(source, tokens, object.keyword, object.end, "WAREHOUSE")
186 .or_else(|| {
187 value_after_keyword(
188 source,
189 tokens,
190 object.keyword,
191 object.end,
192 "USER_TASK_MANAGED_INITIAL_WAREHOUSE_SIZE",
193 )
194 });
195 let schedule = value_after_keyword(source, tokens, object.keyword, object.end, "SCHEDULE");
196 let after = clause_after_keyword(
197 source,
198 tokens,
199 object.keyword,
200 object.end,
201 "AFTER",
202 &["WHEN", "AS", "EXECUTE", "COMMENT", "FINALIZE"],
203 );
204 let when = clause_after_keyword(source, tokens, object.keyword, object.end, "WHEN", &["AS"]);
205
206 let mut lines = vec![format!("Task `{name}`.")];
207 if let Some(warehouse) = warehouse {
208 lines.push(format!("Compute: `{warehouse}`."));
209 }
210 if let Some(schedule) = schedule {
211 lines.push(format!("Schedule: `{schedule}`."));
212 }
213 if let Some(after) = after {
214 lines.push(format!("Predecessors: `{after}`."));
215 }
216 if let Some(when) = when {
217 lines.push(format!("Condition: `{when}`."));
218 }
219 lines.push(String::from(
220 "Tasks run SQL on a schedule or after predecessor tasks; newly created tasks start suspended.",
221 ));
222
223 Some(Hover {
224 kind: HoverKind::Task,
225 title: format!("Task `{name}`"),
226 body: lines.join("\n"),
227 range: combined_range(tokens, name_range),
228 docs_url: Some(CREATE_TASK_DOCS),
229 })
230}
231
232#[derive(Clone, Copy)]
233struct ObjectDeclaration {
234 keyword: usize,
235 end: usize,
236}
237
238fn object_declaration(
239 tokens: &[SpannedToken<'_>],
240 index: usize,
241 object_keyword: &str,
242) -> Option<ObjectDeclaration> {
243 let (start, end) = statement_bounds(tokens, index);
244 for keyword in (start..=index.min(end.saturating_sub(1))).rev() {
245 if !word(&tokens[keyword], object_keyword) {
246 continue;
247 }
248 if tokens[start..keyword]
249 .iter()
250 .any(|token| word(token, "CREATE"))
251 {
252 return Some(ObjectDeclaration { keyword, end });
253 }
254 }
255 None
256}
257
258fn statement_bounds(tokens: &[SpannedToken<'_>], index: usize) -> (usize, usize) {
259 let start = tokens[..index]
260 .iter()
261 .rposition(|token| token.kind == SyntaxKind::SEMICOLON)
262 .map_or(0, |idx| idx + 1);
263 let end = tokens[index..]
264 .iter()
265 .position(|token| token.kind == SyntaxKind::SEMICOLON)
266 .map_or(tokens.len(), |relative| index + relative);
267 (start, end)
268}
269
270fn procedure_name_range(
271 tokens: &[SpannedToken<'_>],
272 procedure_keyword: usize,
273 end: usize,
274) -> Option<Range<usize>> {
275 let start = procedure_keyword + 1;
276 let mut cursor = start;
277 while cursor < end && tokens[cursor].kind != SyntaxKind::L_PAREN {
278 if !is_name_part(&tokens[cursor]) {
279 return None;
280 }
281 cursor += 1;
282 }
283 (start < cursor).then_some(start..cursor)
284}
285
286fn task_name_range(
287 tokens: &[SpannedToken<'_>],
288 task_keyword: usize,
289 end: usize,
290) -> Option<Range<usize>> {
291 let mut start = task_keyword + 1;
292 if word_seq(tokens, start, &["IF", "NOT", "EXISTS"]) {
293 start += 3;
294 }
295
296 let mut cursor = start;
297 while cursor < end && is_name_part(&tokens[cursor]) && !is_clause_boundary(&tokens[cursor]) {
298 cursor += 1;
299 }
300 (start < cursor).then_some(start..cursor)
301}
302
303fn procedure_args(
304 source: &str,
305 tokens: &[SpannedToken<'_>],
306 start: usize,
307 end: usize,
308) -> Option<String> {
309 let open = (start..end).find(|&idx| tokens[idx].kind == SyntaxKind::L_PAREN)?;
310 let close = matching_paren(tokens, open, end)?;
311 let inside = token_slice(source, tokens, open + 1, close);
312 Some(inside)
313}
314
315fn matching_paren(tokens: &[SpannedToken<'_>], open: usize, end: usize) -> Option<usize> {
316 let mut depth = 0usize;
317 for (idx, token) in tokens.iter().enumerate().take(end).skip(open) {
318 match token.kind {
319 SyntaxKind::L_PAREN => depth += 1,
320 SyntaxKind::R_PAREN => {
321 depth = depth.saturating_sub(1);
322 if depth == 0 {
323 return Some(idx);
324 }
325 }
326 _ => {}
327 }
328 }
329 None
330}
331
332fn value_after_keyword(
333 source: &str,
334 tokens: &[SpannedToken<'_>],
335 start: usize,
336 end: usize,
337 keyword: &str,
338) -> Option<String> {
339 let idx = (start..end).find(|&idx| word(&tokens[idx], keyword))?;
340 let mut value_start = idx + 1;
341 if value_start < end && tokens[value_start].kind == SyntaxKind::EQ {
342 value_start += 1;
343 }
344 let mut value_end = value_start;
345 while value_end < end
346 && !is_clause_boundary(&tokens[value_end])
347 && tokens[value_end].kind != SyntaxKind::COMMA
348 {
349 value_end += 1;
350 }
351 let value = token_slice(source, tokens, value_start, value_end);
352 (!value.is_empty()).then_some(value)
353}
354
355fn routine_option_after_keyword(
356 source: &str,
357 tokens: &[SpannedToken<'_>],
358 start: usize,
359 end: usize,
360 keyword: &str,
361) -> Option<String> {
362 clause_after_keyword(source, tokens, start, end, keyword, ROUTINE_OPTION_STOPS)
363 .map(|value| value.strip_prefix("= ").unwrap_or(&value).to_string())
364}
365
366fn clause_after_keyword(
367 source: &str,
368 tokens: &[SpannedToken<'_>],
369 start: usize,
370 end: usize,
371 keyword: &str,
372 stops: &[&str],
373) -> Option<String> {
374 let idx = (start..end).find(|&idx| word(&tokens[idx], keyword))?;
375 let value_start = idx + 1;
376 let mut depth = 0usize;
377 let mut value_end = value_start;
378 while value_end < end {
379 let token = &tokens[value_end];
380 match token.kind {
381 SyntaxKind::L_PAREN | SyntaxKind::L_BRACKET => depth += 1,
382 SyntaxKind::R_PAREN | SyntaxKind::R_BRACKET => depth = depth.saturating_sub(1),
383 _ => {}
384 }
385 if depth == 0 && stops.iter().any(|stop| word(token, stop)) {
386 break;
387 }
388 value_end += 1;
389 }
390 let value = token_slice(source, tokens, value_start, value_end);
391 (!value.is_empty()).then_some(value)
392}
393
394fn token_slice(source: &str, tokens: &[SpannedToken<'_>], start: usize, end: usize) -> String {
395 if start >= end || start >= tokens.len() {
396 return String::new();
397 }
398 let end = end.min(tokens.len());
399 compact_text(&source[tokens[start].range.start..tokens[end - 1].range.end])
400}
401
402fn compact_text(text: &str) -> String {
403 text.split_whitespace().collect::<Vec<_>>().join(" ")
404}
405
406fn compact_token_text(tokens: &[SpannedToken<'_>], range: Range<usize>) -> String {
407 tokens[range]
408 .iter()
409 .map(|token| token.text)
410 .collect::<Vec<_>>()
411 .join("")
412}
413
414fn combined_range(tokens: &[SpannedToken<'_>], range: Range<usize>) -> Range<usize> {
415 tokens[range.start].range.start..tokens[range.end - 1].range.end
416}
417
418fn is_name_part(token: &SpannedToken<'_>) -> bool {
419 matches!(
420 token.kind,
421 SyntaxKind::IDENT | SyntaxKind::QUOTED_IDENT | SyntaxKind::DOT
422 )
423}
424
425fn is_clause_boundary(token: &SpannedToken<'_>) -> bool {
426 matches!(
427 token.kind,
428 SyntaxKind::SEMICOLON | SyntaxKind::R_PAREN | SyntaxKind::R_BRACKET
429 ) || [
430 "AS",
431 "AFTER",
432 "ARTIFACT_REPOSITORY",
433 "CALLED",
434 "COMMENT",
435 "CONFIG",
436 "COPY",
437 "ERROR_INTEGRATION",
438 "EXECUTE",
439 "EXTERNAL_ACCESS_INTEGRATIONS",
440 "FINALIZE",
441 "HANDLER",
442 "IMMUTABLE",
443 "IMPORTS",
444 "LANGUAGE",
445 "MEMOIZABLE",
446 "NULL",
447 "OVERLAP_POLICY",
448 "PACKAGES",
449 "RETURNS",
450 "RUNTIME_VERSION",
451 "SCHEDULE",
452 "SECRETS",
453 "SECURE",
454 "STRICT",
455 "SUCCESS_INTEGRATION",
456 "TASK_AUTO_RETRY_ATTEMPTS",
457 "TARGET_PATH",
458 "USER_TASK_MANAGED_INITIAL_WAREHOUSE_SIZE",
459 "USER_TASK_TIMEOUT_MS",
460 "VOLATILE",
461 "WAREHOUSE",
462 "WHEN",
463 ]
464 .iter()
465 .any(|boundary| word(token, boundary))
466}
467
468fn word(token: &SpannedToken<'_>, expected: &str) -> bool {
469 (token.kind == SyntaxKind::IDENT || token.kind.is_keyword())
470 && token.text.eq_ignore_ascii_case(expected)
471}
472
473fn word_seq(tokens: &[SpannedToken<'_>], start: usize, words: &[&str]) -> bool {
474 words.iter().enumerate().all(|(offset, expected)| {
475 tokens
476 .get(start + offset)
477 .is_some_and(|t| word(t, expected))
478 })
479}
480
481fn type_hover(token: &SpannedToken<'_>) -> Option<Hover> {
482 let (canonical, body) = type_info(token.text)?;
483 Some(Hover {
484 kind: HoverKind::Type,
485 title: format!("Snowflake type `{canonical}`"),
486 body: body.to_string(),
487 range: token.range.clone(),
488 docs_url: Some(DATA_TYPES_DOCS),
489 })
490}
491
492fn language_hover(token: &SpannedToken<'_>) -> Option<Hover> {
493 language_template(token.text).map(|template| from_static(token.range.clone(), template))
494}
495
496fn property_hover(token: &SpannedToken<'_>) -> Option<Hover> {
497 property_template(token.text).map(|template| from_static(token.range.clone(), template))
498}
499
500fn keyword_hover(token: &SpannedToken<'_>) -> Option<Hover> {
501 keyword_template(token.text).map(|template| from_static(token.range.clone(), template))
502}
503
504fn from_static(range: Range<usize>, template: StaticHover) -> Hover {
505 Hover {
506 kind: template.kind,
507 title: template.title.to_string(),
508 body: template.body.to_string(),
509 range,
510 docs_url: template.docs_url,
511 }
512}