Skip to main content

squawk_ide/
document_symbols.rs

1use rowan::TextRange;
2use squawk_syntax::ast::{self, AstNode};
3
4use crate::binder::{self, extract_string_literal};
5use crate::resolve::{
6    resolve_aggregate_info, resolve_function_info, resolve_procedure_info, resolve_sequence_info,
7    resolve_table_info, resolve_type_info, resolve_view_info,
8};
9
10#[derive(Debug)]
11pub enum DocumentSymbolKind {
12    Schema,
13    Table,
14    View,
15    MaterializedView,
16    Function,
17    Aggregate,
18    Procedure,
19    EventTrigger,
20    Role,
21    Policy,
22    Type,
23    Enum,
24    Index,
25    Domain,
26    Sequence,
27    Trigger,
28    Tablespace,
29    Database,
30    Server,
31    Extension,
32    Column,
33    Variant,
34    Cursor,
35    PreparedStatement,
36    Channel,
37}
38
39#[derive(Debug)]
40pub struct DocumentSymbol {
41    pub name: String,
42    pub detail: Option<String>,
43    pub kind: DocumentSymbolKind,
44    /// Range used for determining when cursor is inside the symbol for showing
45    /// in the UI
46    pub full_range: TextRange,
47    /// Range selected when symbol is selected
48    pub focus_range: TextRange,
49    pub children: Vec<DocumentSymbol>,
50}
51
52pub fn document_symbols(file: &ast::SourceFile) -> Vec<DocumentSymbol> {
53    // TODO: we should salsa this
54    let binder = binder::bind(file);
55    let mut symbols = vec![];
56
57    for stmt in file.stmts() {
58        match stmt {
59            ast::Stmt::CreateSchema(create_schema) => {
60                if let Some(symbol) = create_schema_symbol(create_schema) {
61                    symbols.push(symbol);
62                }
63            }
64            ast::Stmt::CreateTable(create_table) => {
65                if let Some(symbol) = create_table_symbol(&binder, create_table) {
66                    symbols.push(symbol);
67                }
68            }
69            ast::Stmt::CreateTableAs(create_table_as) => {
70                if let Some(symbol) = create_table_as_symbol(&binder, create_table_as) {
71                    symbols.push(symbol);
72                }
73            }
74            ast::Stmt::CreateForeignTable(create_foreign_table) => {
75                if let Some(symbol) = create_table_symbol(&binder, create_foreign_table) {
76                    symbols.push(symbol);
77                }
78            }
79            ast::Stmt::CreateFunction(create_function) => {
80                if let Some(symbol) = create_function_symbol(&binder, create_function) {
81                    symbols.push(symbol);
82                }
83            }
84            ast::Stmt::CreateAggregate(create_aggregate) => {
85                if let Some(symbol) = create_aggregate_symbol(&binder, create_aggregate) {
86                    symbols.push(symbol);
87                }
88            }
89            ast::Stmt::CreateProcedure(create_procedure) => {
90                if let Some(symbol) = create_procedure_symbol(&binder, create_procedure) {
91                    symbols.push(symbol);
92                }
93            }
94            ast::Stmt::CreateIndex(create_index) => {
95                if let Some(symbol) = create_index_symbol(create_index) {
96                    symbols.push(symbol);
97                }
98            }
99            ast::Stmt::CreateDomain(create_domain) => {
100                if let Some(symbol) = create_domain_symbol(&binder, create_domain) {
101                    symbols.push(symbol);
102                }
103            }
104            ast::Stmt::CreateSequence(create_sequence) => {
105                if let Some(symbol) = create_sequence_symbol(&binder, create_sequence) {
106                    symbols.push(symbol);
107                }
108            }
109            ast::Stmt::CreateTrigger(create_trigger) => {
110                if let Some(symbol) = create_trigger_symbol(create_trigger) {
111                    symbols.push(symbol);
112                }
113            }
114            ast::Stmt::CreateEventTrigger(create_event_trigger) => {
115                if let Some(symbol) = create_event_trigger_symbol(create_event_trigger) {
116                    symbols.push(symbol);
117                }
118            }
119            ast::Stmt::CreateTablespace(create_tablespace) => {
120                if let Some(symbol) = create_tablespace_symbol(create_tablespace) {
121                    symbols.push(symbol);
122                }
123            }
124            ast::Stmt::CreateDatabase(create_database) => {
125                if let Some(symbol) = create_database_symbol(create_database) {
126                    symbols.push(symbol);
127                }
128            }
129            ast::Stmt::CreateServer(create_server) => {
130                if let Some(symbol) = create_server_symbol(create_server) {
131                    symbols.push(symbol);
132                }
133            }
134            ast::Stmt::CreateExtension(create_extension) => {
135                if let Some(symbol) = create_extension_symbol(create_extension) {
136                    symbols.push(symbol);
137                }
138            }
139            ast::Stmt::CreateRole(create_role) => {
140                if let Some(symbol) = create_role_symbol(create_role) {
141                    symbols.push(symbol);
142                }
143            }
144            ast::Stmt::CreatePolicy(create_policy) => {
145                if let Some(symbol) = create_policy_symbol(create_policy) {
146                    symbols.push(symbol);
147                }
148            }
149            ast::Stmt::CreateType(create_type) => {
150                if let Some(symbol) = create_type_symbol(&binder, create_type) {
151                    symbols.push(symbol);
152                }
153            }
154            ast::Stmt::CreateView(create_view) => {
155                if let Some(symbol) = create_view_symbol(&binder, create_view) {
156                    symbols.push(symbol);
157                }
158            }
159            ast::Stmt::CreateMaterializedView(create_view) => {
160                if let Some(symbol) = create_materialized_view_symbol(&binder, create_view) {
161                    symbols.push(symbol);
162                }
163            }
164            ast::Stmt::Declare(declare) => {
165                if let Some(symbol) = create_declare_cursor_symbol(declare) {
166                    symbols.push(symbol);
167                }
168            }
169            ast::Stmt::Prepare(prepare) => {
170                if let Some(symbol) = create_prepare_symbol(prepare) {
171                    symbols.push(symbol);
172                }
173            }
174            ast::Stmt::Select(select) => {
175                symbols.extend(cte_table_symbols(select));
176            }
177            ast::Stmt::SelectInto(select_into) => {
178                symbols.extend(cte_table_symbols(select_into));
179            }
180            ast::Stmt::Insert(insert) => {
181                symbols.extend(cte_table_symbols(insert));
182            }
183            ast::Stmt::Update(update) => {
184                symbols.extend(cte_table_symbols(update));
185            }
186            ast::Stmt::Delete(delete) => {
187                symbols.extend(cte_table_symbols(delete));
188            }
189            ast::Stmt::Listen(listen) => {
190                if let Some(symbol) = create_listen_symbol(listen) {
191                    symbols.push(symbol);
192                }
193            }
194            ast::Stmt::Notify(notify) => {
195                if let Some(symbol) = create_notify_symbol(notify) {
196                    symbols.push(symbol);
197                }
198            }
199            ast::Stmt::Unlisten(unlisten) => {
200                if let Some(symbol) = create_unlisten_symbol(unlisten) {
201                    symbols.push(symbol);
202                }
203            }
204
205            _ => (),
206        }
207    }
208
209    symbols
210}
211
212fn cte_table_symbols(stmt: impl ast::HasWithClause) -> Vec<DocumentSymbol> {
213    let Some(with_clause) = stmt.with_clause() else {
214        return vec![];
215    };
216
217    with_clause
218        .with_tables()
219        .filter_map(create_cte_table_symbol)
220        .collect()
221}
222
223fn create_cte_table_symbol(with_table: ast::WithTable) -> Option<DocumentSymbol> {
224    let name_node = with_table.name()?;
225    let name = name_node.syntax().text().to_string();
226
227    let full_range = with_table.syntax().text_range();
228    let focus_range = name_node.syntax().text_range();
229
230    symbols_from_column_list(
231        with_table.column_list(),
232        name,
233        full_range,
234        focus_range,
235        DocumentSymbolKind::Table,
236    )
237}
238
239fn create_schema_symbol(create_schema: ast::CreateSchema) -> Option<DocumentSymbol> {
240    let (name, focus_range) = if let Some(name_node) = create_schema.name() {
241        (
242            name_node.syntax().text().to_string(),
243            name_node.syntax().text_range(),
244        )
245    } else if let Some(name) = create_schema.role().and_then(|r| r.name()) {
246        (name.syntax().text().to_string(), name.syntax().text_range())
247    } else {
248        return None;
249    };
250
251    let full_range = create_schema.syntax().text_range();
252
253    Some(DocumentSymbol {
254        name,
255        detail: None,
256        kind: DocumentSymbolKind::Schema,
257        full_range,
258        focus_range,
259        children: vec![],
260    })
261}
262
263fn create_table_symbol(
264    binder: &binder::Binder,
265    create_table: impl ast::HasCreateTable,
266) -> Option<DocumentSymbol> {
267    let path = create_table.path()?;
268    let segment = path.segment()?;
269    let name_node = segment.name()?;
270
271    let (schema, table_name) = resolve_table_info(binder, &path)?;
272    let name = format!("{}.{}", schema.0, table_name);
273
274    let full_range = create_table.syntax().text_range();
275    let focus_range = name_node.syntax().text_range();
276
277    let mut children = vec![];
278    if let Some(table_arg_list) = create_table.table_arg_list() {
279        for arg in table_arg_list.args() {
280            if let ast::TableArg::Column(column) = arg
281                && let Some(column_symbol) = create_column_symbol(column)
282            {
283                children.push(column_symbol);
284            }
285        }
286    }
287
288    Some(DocumentSymbol {
289        name,
290        detail: None,
291        kind: DocumentSymbolKind::Table,
292        full_range,
293        focus_range,
294        children,
295    })
296}
297
298fn create_table_as_symbol(
299    binder: &binder::Binder,
300    create_table_as: ast::CreateTableAs,
301) -> Option<DocumentSymbol> {
302    let path = create_table_as.path()?;
303    let segment = path.segment()?;
304    let name_node = if let Some(name) = segment.name() {
305        name.syntax().clone()
306    } else {
307        return None;
308    };
309
310    let (schema, table_name) = resolve_table_info(binder, &path)?;
311    let name = format!("{}.{}", schema.0, table_name);
312
313    let full_range = create_table_as.syntax().text_range();
314    let focus_range = name_node.text_range();
315
316    Some(DocumentSymbol {
317        name,
318        detail: None,
319        kind: DocumentSymbolKind::Table,
320        full_range,
321        focus_range,
322        // TODO: infer the column names, we need the same for views without
323        // explicit column lists
324        children: vec![],
325    })
326}
327
328fn create_view_symbol(
329    binder: &binder::Binder,
330    create_view: ast::CreateView,
331) -> Option<DocumentSymbol> {
332    let path = create_view.path()?;
333    let segment = path.segment()?;
334    let name_node = segment.name()?;
335
336    let (schema, view_name) = resolve_view_info(binder, &path)?;
337    let name = format!("{}.{}", schema.0, view_name);
338
339    let full_range = create_view.syntax().text_range();
340    let focus_range = name_node.syntax().text_range();
341
342    symbols_from_column_list(
343        create_view.column_list(),
344        name,
345        full_range,
346        focus_range,
347        DocumentSymbolKind::View,
348    )
349}
350
351fn symbols_from_column_list(
352    column_list: Option<ast::ColumnList>,
353    name: String,
354    full_range: TextRange,
355    focus_range: TextRange,
356    kind: DocumentSymbolKind,
357) -> Option<DocumentSymbol> {
358    let mut children = vec![];
359    if let Some(column_list) = column_list {
360        for column in column_list.columns() {
361            if let Some(column_symbol) = create_column_symbol(column) {
362                children.push(column_symbol);
363            }
364        }
365    }
366
367    Some(DocumentSymbol {
368        name,
369        detail: None,
370        kind,
371        full_range,
372        focus_range,
373        children,
374    })
375}
376
377// TODO: combine with create_view_symbol
378fn create_materialized_view_symbol(
379    binder: &binder::Binder,
380    create_view: ast::CreateMaterializedView,
381) -> Option<DocumentSymbol> {
382    let path = create_view.path()?;
383    let segment = path.segment()?;
384    let name_node = segment.name()?;
385
386    let (schema, view_name) = resolve_view_info(binder, &path)?;
387    let name = format!("{}.{}", schema.0, view_name);
388
389    let full_range = create_view.syntax().text_range();
390    let focus_range = name_node.syntax().text_range();
391
392    symbols_from_column_list(
393        create_view.column_list(),
394        name,
395        full_range,
396        focus_range,
397        DocumentSymbolKind::MaterializedView,
398    )
399}
400
401fn create_function_symbol(
402    binder: &binder::Binder,
403    create_function: ast::CreateFunction,
404) -> Option<DocumentSymbol> {
405    let path = create_function.path()?;
406    let segment = path.segment()?;
407    let name_node = segment.name()?;
408
409    let (schema, function_name) = resolve_function_info(binder, &path)?;
410    let name = format!("{}.{}", schema.0, function_name);
411
412    let full_range = create_function.syntax().text_range();
413    let focus_range = name_node.syntax().text_range();
414
415    Some(DocumentSymbol {
416        name,
417        detail: None,
418        kind: DocumentSymbolKind::Function,
419        full_range,
420        focus_range,
421        children: vec![],
422    })
423}
424
425fn create_aggregate_symbol(
426    binder: &binder::Binder,
427    create_aggregate: ast::CreateAggregate,
428) -> Option<DocumentSymbol> {
429    let path = create_aggregate.path()?;
430    let segment = path.segment()?;
431    let name_node = segment.name()?;
432
433    let (schema, aggregate_name) = resolve_aggregate_info(binder, &path)?;
434    let name = format!("{}.{}", schema.0, aggregate_name);
435
436    let full_range = create_aggregate.syntax().text_range();
437    let focus_range = name_node.syntax().text_range();
438
439    Some(DocumentSymbol {
440        name,
441        detail: None,
442        kind: DocumentSymbolKind::Aggregate,
443        full_range,
444        focus_range,
445        children: vec![],
446    })
447}
448
449fn create_procedure_symbol(
450    binder: &binder::Binder,
451    create_procedure: ast::CreateProcedure,
452) -> Option<DocumentSymbol> {
453    let path = create_procedure.path()?;
454    let segment = path.segment()?;
455    let name_node = segment.name()?;
456
457    let (schema, procedure_name) = resolve_procedure_info(binder, &path)?;
458    let name = format!("{}.{}", schema.0, procedure_name);
459
460    let full_range = create_procedure.syntax().text_range();
461    let focus_range = name_node.syntax().text_range();
462
463    Some(DocumentSymbol {
464        name,
465        detail: None,
466        kind: DocumentSymbolKind::Procedure,
467        full_range,
468        focus_range,
469        children: vec![],
470    })
471}
472
473fn create_index_symbol(create_index: ast::CreateIndex) -> Option<DocumentSymbol> {
474    let name_node = create_index.name()?;
475    let name = name_node.syntax().text().to_string();
476
477    let full_range = create_index.syntax().text_range();
478    let focus_range = name_node.syntax().text_range();
479
480    Some(DocumentSymbol {
481        name,
482        detail: None,
483        kind: DocumentSymbolKind::Index,
484        full_range,
485        focus_range,
486        children: vec![],
487    })
488}
489
490fn create_domain_symbol(
491    binder: &binder::Binder,
492    create_domain: ast::CreateDomain,
493) -> Option<DocumentSymbol> {
494    let path = create_domain.path()?;
495    let segment = path.segment()?;
496    let name_node = segment.name()?;
497
498    let (schema, domain_name) = resolve_type_info(binder, &path)?;
499    let name = format!("{}.{}", schema.0, domain_name);
500
501    let full_range = create_domain.syntax().text_range();
502    let focus_range = name_node.syntax().text_range();
503
504    Some(DocumentSymbol {
505        name,
506        detail: None,
507        kind: DocumentSymbolKind::Domain,
508        full_range,
509        focus_range,
510        children: vec![],
511    })
512}
513
514fn create_sequence_symbol(
515    binder: &binder::Binder,
516    create_sequence: ast::CreateSequence,
517) -> Option<DocumentSymbol> {
518    let path = create_sequence.path()?;
519    let segment = path.segment()?;
520    let name_node = segment.name()?;
521
522    let (schema, sequence_name) = resolve_sequence_info(binder, &path)?;
523    let name = format!("{}.{}", schema.0, sequence_name);
524
525    let full_range = create_sequence.syntax().text_range();
526    let focus_range = name_node.syntax().text_range();
527
528    Some(DocumentSymbol {
529        name,
530        detail: None,
531        kind: DocumentSymbolKind::Sequence,
532        full_range,
533        focus_range,
534        children: vec![],
535    })
536}
537
538fn create_trigger_symbol(create_trigger: ast::CreateTrigger) -> Option<DocumentSymbol> {
539    let name_node = create_trigger.name()?;
540    let name = name_node.syntax().text().to_string();
541
542    let full_range = create_trigger.syntax().text_range();
543    let focus_range = name_node.syntax().text_range();
544
545    Some(DocumentSymbol {
546        name,
547        detail: None,
548        kind: DocumentSymbolKind::Trigger,
549        full_range,
550        focus_range,
551        children: vec![],
552    })
553}
554
555fn create_event_trigger_symbol(
556    create_event_trigger: ast::CreateEventTrigger,
557) -> Option<DocumentSymbol> {
558    let name_node = create_event_trigger.name()?;
559    let name = name_node.syntax().text().to_string();
560
561    let full_range = create_event_trigger.syntax().text_range();
562    let focus_range = name_node.syntax().text_range();
563
564    Some(DocumentSymbol {
565        name,
566        detail: None,
567        kind: DocumentSymbolKind::EventTrigger,
568        full_range,
569        focus_range,
570        children: vec![],
571    })
572}
573
574fn create_tablespace_symbol(create_tablespace: ast::CreateTablespace) -> Option<DocumentSymbol> {
575    let name_node = create_tablespace.name()?;
576    let name = name_node.syntax().text().to_string();
577
578    let full_range = create_tablespace.syntax().text_range();
579    let focus_range = name_node.syntax().text_range();
580
581    Some(DocumentSymbol {
582        name,
583        detail: None,
584        kind: DocumentSymbolKind::Tablespace,
585        full_range,
586        focus_range,
587        children: vec![],
588    })
589}
590
591fn create_database_symbol(create_database: ast::CreateDatabase) -> Option<DocumentSymbol> {
592    let name_node = create_database.name()?;
593    let name = name_node.syntax().text().to_string();
594
595    let full_range = create_database.syntax().text_range();
596    let focus_range = name_node.syntax().text_range();
597
598    Some(DocumentSymbol {
599        name,
600        detail: None,
601        kind: DocumentSymbolKind::Database,
602        full_range,
603        focus_range,
604        children: vec![],
605    })
606}
607
608fn create_server_symbol(create_server: ast::CreateServer) -> Option<DocumentSymbol> {
609    let name_node = create_server.name()?;
610    let name = name_node.syntax().text().to_string();
611
612    let full_range = create_server.syntax().text_range();
613    let focus_range = name_node.syntax().text_range();
614
615    Some(DocumentSymbol {
616        name,
617        detail: None,
618        kind: DocumentSymbolKind::Server,
619        full_range,
620        focus_range,
621        children: vec![],
622    })
623}
624
625fn create_extension_symbol(create_extension: ast::CreateExtension) -> Option<DocumentSymbol> {
626    let name_node = create_extension.name()?;
627    let name = name_node.syntax().text().to_string();
628
629    let full_range = create_extension.syntax().text_range();
630    let focus_range = name_node.syntax().text_range();
631
632    Some(DocumentSymbol {
633        name,
634        detail: None,
635        kind: DocumentSymbolKind::Extension,
636        full_range,
637        focus_range,
638        children: vec![],
639    })
640}
641
642fn create_role_symbol(create_role: ast::CreateRole) -> Option<DocumentSymbol> {
643    let name_node = create_role.name()?;
644    let name = name_node.syntax().text().to_string();
645
646    let full_range = create_role.syntax().text_range();
647    let focus_range = name_node.syntax().text_range();
648
649    Some(DocumentSymbol {
650        name,
651        detail: None,
652        kind: DocumentSymbolKind::Role,
653        full_range,
654        focus_range,
655        children: vec![],
656    })
657}
658
659fn create_policy_symbol(create_policy: ast::CreatePolicy) -> Option<DocumentSymbol> {
660    let name_node = create_policy.name()?;
661    let name = name_node.syntax().text().to_string();
662
663    let full_range = create_policy.syntax().text_range();
664    let focus_range = name_node.syntax().text_range();
665
666    Some(DocumentSymbol {
667        name,
668        detail: None,
669        kind: DocumentSymbolKind::Policy,
670        full_range,
671        focus_range,
672        children: vec![],
673    })
674}
675
676fn create_type_symbol(
677    binder: &binder::Binder,
678    create_type: ast::CreateType,
679) -> Option<DocumentSymbol> {
680    let path = create_type.path()?;
681    let segment = path.segment()?;
682    let name_node = segment.name()?;
683
684    let (schema, type_name) = resolve_type_info(binder, &path)?;
685    let name = format!("{}.{}", schema.0, type_name);
686
687    let full_range = create_type.syntax().text_range();
688    let focus_range = name_node.syntax().text_range();
689
690    let mut children = vec![];
691    if let Some(variant_list) = create_type.variant_list() {
692        for variant in variant_list.variants() {
693            if let Some(variant_symbol) = create_variant_symbol(variant) {
694                children.push(variant_symbol);
695            }
696        }
697    } else if let Some(column_list) = create_type.column_list() {
698        for column in column_list.columns() {
699            if let Some(column_symbol) = create_column_symbol(column) {
700                children.push(column_symbol);
701            }
702        }
703    }
704
705    Some(DocumentSymbol {
706        name,
707        detail: None,
708        kind: if create_type.variant_list().is_some() {
709            DocumentSymbolKind::Enum
710        } else {
711            DocumentSymbolKind::Type
712        },
713        full_range,
714        focus_range,
715        children,
716    })
717}
718
719fn create_column_symbol(column: ast::Column) -> Option<DocumentSymbol> {
720    let name_node = column.name()?;
721    let name = name_node.syntax().text().to_string();
722
723    let detail = column.ty().map(|t| t.syntax().text().to_string());
724
725    let full_range = column.syntax().text_range();
726    let focus_range = name_node.syntax().text_range();
727
728    Some(DocumentSymbol {
729        name,
730        detail,
731        kind: DocumentSymbolKind::Column,
732        full_range,
733        focus_range,
734        children: vec![],
735    })
736}
737
738fn create_variant_symbol(variant: ast::Variant) -> Option<DocumentSymbol> {
739    let literal = variant.literal()?;
740    let name = extract_string_literal(&literal)?;
741
742    let full_range = variant.syntax().text_range();
743    let focus_range = literal.syntax().text_range();
744
745    Some(DocumentSymbol {
746        name,
747        detail: None,
748        kind: DocumentSymbolKind::Variant,
749        full_range,
750        focus_range,
751        children: vec![],
752    })
753}
754
755fn create_declare_cursor_symbol(declare: ast::Declare) -> Option<DocumentSymbol> {
756    let name_node = declare.name()?;
757    let name = name_node.syntax().text().to_string();
758
759    let full_range = declare.syntax().text_range();
760    let focus_range = name_node.syntax().text_range();
761
762    Some(DocumentSymbol {
763        name,
764        detail: None,
765        kind: DocumentSymbolKind::Cursor,
766        full_range,
767        focus_range,
768        children: vec![],
769    })
770}
771
772fn create_prepare_symbol(prepare: ast::Prepare) -> Option<DocumentSymbol> {
773    let name_node = prepare.name()?;
774    let name = name_node.syntax().text().to_string();
775
776    let full_range = prepare.syntax().text_range();
777    let focus_range = name_node.syntax().text_range();
778
779    Some(DocumentSymbol {
780        name,
781        detail: None,
782        kind: DocumentSymbolKind::PreparedStatement,
783        full_range,
784        focus_range,
785        children: vec![],
786    })
787}
788
789fn create_listen_symbol(listen: ast::Listen) -> Option<DocumentSymbol> {
790    let name_node = listen.name()?;
791    let name = name_node.syntax().text().to_string();
792
793    let full_range = listen.syntax().text_range();
794    let focus_range = name_node.syntax().text_range();
795
796    Some(DocumentSymbol {
797        name,
798        detail: Some("listen".to_string()),
799        kind: DocumentSymbolKind::Channel,
800        full_range,
801        focus_range,
802        children: vec![],
803    })
804}
805
806fn create_notify_symbol(notify: ast::Notify) -> Option<DocumentSymbol> {
807    let name_node = notify.name_ref()?;
808    let name = name_node.syntax().text().to_string();
809
810    let full_range = notify.syntax().text_range();
811    let focus_range = name_node.syntax().text_range();
812
813    Some(DocumentSymbol {
814        name,
815        detail: Some("notify".to_string()),
816        kind: DocumentSymbolKind::Channel,
817        full_range,
818        focus_range,
819        children: vec![],
820    })
821}
822
823fn create_unlisten_symbol(unlisten: ast::Unlisten) -> Option<DocumentSymbol> {
824    let name_node = unlisten.name_ref()?;
825    let name = name_node.syntax().text().to_string();
826
827    let full_range = unlisten.syntax().text_range();
828    let focus_range = name_node.syntax().text_range();
829
830    Some(DocumentSymbol {
831        name,
832        detail: Some("unlisten".to_string()),
833        kind: DocumentSymbolKind::Channel,
834        full_range,
835        focus_range,
836        children: vec![],
837    })
838}
839
840#[cfg(test)]
841mod tests {
842    use super::*;
843    use annotate_snippets::{
844        AnnotationKind, Group, Level, Renderer, Snippet, renderer::DecorStyle,
845    };
846    use insta::assert_snapshot;
847
848    fn symbols_not_found(sql: &str) {
849        let parse = ast::SourceFile::parse(sql);
850        let file = parse.tree();
851        let symbols = document_symbols(&file);
852        if !symbols.is_empty() {
853            panic!("Symbols found. If this is expected, use `symbols` instead.")
854        }
855    }
856
857    fn symbols(sql: &str) -> String {
858        let parse = ast::SourceFile::parse(sql);
859        let file = parse.tree();
860        let symbols = document_symbols(&file);
861        if symbols.is_empty() {
862            panic!("No symbols found. If this is expected, use `symbols_not_found` instead.")
863        }
864
865        let mut output = vec![];
866        for symbol in symbols {
867            let group = symbol_to_group(&symbol, sql);
868            output.push(group);
869        }
870        Renderer::plain()
871            .decor_style(DecorStyle::Unicode)
872            .render(&output)
873            .to_string()
874    }
875
876    fn symbol_to_group<'a>(symbol: &DocumentSymbol, sql: &'a str) -> Group<'a> {
877        let kind = match symbol.kind {
878            DocumentSymbolKind::Schema => "schema",
879            DocumentSymbolKind::Table => "table",
880            DocumentSymbolKind::View => "view",
881            DocumentSymbolKind::MaterializedView => "materialized view",
882            DocumentSymbolKind::Function => "function",
883            DocumentSymbolKind::Aggregate => "aggregate",
884            DocumentSymbolKind::Procedure => "procedure",
885            DocumentSymbolKind::EventTrigger => "event trigger",
886            DocumentSymbolKind::Role => "role",
887            DocumentSymbolKind::Policy => "policy",
888            DocumentSymbolKind::Type => "type",
889            DocumentSymbolKind::Enum => "enum",
890            DocumentSymbolKind::Index => "index",
891            DocumentSymbolKind::Domain => "domain",
892            DocumentSymbolKind::Sequence => "sequence",
893            DocumentSymbolKind::Trigger => "trigger",
894            DocumentSymbolKind::Tablespace => "tablespace",
895            DocumentSymbolKind::Database => "database",
896            DocumentSymbolKind::Server => "server",
897            DocumentSymbolKind::Extension => "extension",
898            DocumentSymbolKind::Column => "column",
899            DocumentSymbolKind::Variant => "variant",
900            DocumentSymbolKind::Cursor => "cursor",
901            DocumentSymbolKind::PreparedStatement => "prepared statement",
902            DocumentSymbolKind::Channel => "channel",
903        };
904
905        let title = if let Some(detail) = &symbol.detail {
906            format!("{}: {} {}", kind, symbol.name, detail)
907        } else {
908            format!("{}: {}", kind, symbol.name)
909        };
910
911        let snippet = Snippet::source(sql)
912            .fold(true)
913            .annotation(
914                AnnotationKind::Primary
915                    .span(symbol.focus_range.into())
916                    .label("focus range"),
917            )
918            .annotation(
919                AnnotationKind::Context
920                    .span(symbol.full_range.into())
921                    .label("full range"),
922            );
923
924        let mut group = Level::INFO.primary_title(title.clone()).element(snippet);
925
926        if !symbol.children.is_empty() {
927            let child_labels: Vec<String> = symbol
928                .children
929                .iter()
930                .map(|child| {
931                    let kind = match child.kind {
932                        DocumentSymbolKind::Column => "column",
933                        DocumentSymbolKind::Variant => "variant",
934                        _ => unreachable!("only columns and variants can be children"),
935                    };
936                    if let Some(detail) = &child.detail {
937                        format!("{}: {} {}", kind, child.name, detail)
938                    } else {
939                        format!("{}: {}", kind, child.name)
940                    }
941                })
942                .collect();
943
944            let mut children_snippet = Snippet::source(sql).fold(true);
945
946            for (i, child) in symbol.children.iter().enumerate() {
947                children_snippet = children_snippet
948                    .annotation(
949                        AnnotationKind::Context
950                            .span(child.full_range.into())
951                            .label(format!("full range for `{}`", child_labels[i].clone())),
952                    )
953                    .annotation(
954                        AnnotationKind::Primary
955                            .span(child.focus_range.into())
956                            .label("focus range"),
957                    );
958            }
959
960            group = group.element(children_snippet);
961        }
962
963        group
964    }
965
966    #[test]
967    fn create_table() {
968        assert_snapshot!(symbols("
969create table users (
970  id int,
971  email citext
972);"), @r"
973        info: table: public.users
974          ╭▸ 
975        2 │   create table users (
976          │   │            ━━━━━ focus range
977          │ ┌─┘
978          │ │
979        3 │ │   id int,
980        4 │ │   email citext
981        5 │ │ );
982          │ └─┘ full range
983984985        3 │     id int,
986          │     ┯━────
987          │     │
988          │     full range for `column: id int`
989          │     focus range
990        4 │     email citext
991          │     ┯━━━━───────
992          │     │
993          │     full range for `column: email citext`
994          ╰╴    focus range
995        ");
996    }
997
998    #[test]
999    fn create_table_as() {
1000        assert_snapshot!(symbols("
1001create table t as select 1 a;
1002"), @r"
1003        info: table: public.t
1004          ╭▸ 
1005        2 │ create table t as select 1 a;
1006          │ ┬────────────┯──────────────
1007          │ │            │
1008          │ │            focus range
1009          ╰╴full range
1010        ");
1011    }
1012
1013    #[test]
1014    fn create_schema() {
1015        assert_snapshot!(symbols("
1016create schema foo;
1017"), @r"
1018        info: schema: foo
1019          ╭▸ 
1020        2 │ create schema foo;
1021          │ ┬─────────────┯━━
1022          │ │             │
1023          │ │             focus range
1024          ╰╴full range
1025        ");
1026    }
1027
1028    #[test]
1029    fn create_schema_authorization() {
1030        assert_snapshot!(symbols("
1031create schema authorization foo;
1032"), @r"
1033        info: schema: foo
1034          ╭▸ 
1035        2 │ create schema authorization foo;
1036          │ ┬───────────────────────────┯━━
1037          │ │                           │
1038          │ │                           focus range
1039          ╰╴full range
1040        ");
1041    }
1042
1043    #[test]
1044    fn listen_notify_unlisten() {
1045        assert_snapshot!(symbols("
1046listen updates;
1047notify updates;
1048unlisten updates;
1049unlisten *;
1050"), @r"
1051        info: channel: updates listen
1052          ╭▸ 
1053        2 │ listen updates;
1054          │ ┬──────┯━━━━━━
1055          │ │      │
1056          │ │      focus range
1057          │ full range
1058          ╰╴
1059        info: channel: updates notify
1060          ╭▸ 
1061        3 │ notify updates;
1062          │ ┬──────┯━━━━━━
1063          │ │      │
1064          │ │      focus range
1065          ╰╴full range
1066        info: channel: updates unlisten
1067          ╭▸ 
1068        4 │ unlisten updates;
1069          │ ┬────────┯━━━━━━
1070          │ │        │
1071          │ │        focus range
1072          ╰╴full range
1073        ");
1074    }
1075
1076    #[test]
1077    fn create_function() {
1078        assert_snapshot!(
1079            symbols("create function hello() returns void as $$ select 1; $$ language sql;"),
1080            @r"
1081        info: function: public.hello
1082          ╭▸ 
1083        1 │ create function hello() returns void as $$ select 1; $$ language sql;
1084          │ ┬───────────────┯━━━━───────────────────────────────────────────────
1085          │ │               │
1086          │ │               focus range
1087          ╰╴full range
1088        "
1089        );
1090    }
1091
1092    #[test]
1093    fn create_materialized_view() {
1094        assert_snapshot!(
1095            symbols("create materialized view reports as select 1;"),
1096            @r"
1097        info: materialized view: public.reports
1098          ╭▸ 
1099        1 │ create materialized view reports as select 1;
1100          │ ┬────────────────────────┯━━━━━━────────────
1101          │ │                        │
1102          │ │                        focus range
1103          ╰╴full range
1104        "
1105        );
1106    }
1107
1108    #[test]
1109    fn create_aggregate() {
1110        assert_snapshot!(
1111            symbols("create aggregate myavg(int) (sfunc = int4_avg_accum, stype = _int8);"),
1112            @r"
1113        info: aggregate: public.myavg
1114          ╭▸ 
1115        1 │ create aggregate myavg(int) (sfunc = int4_avg_accum, stype = _int8);
1116          │ ┬────────────────┯━━━━─────────────────────────────────────────────
1117          │ │                │
1118          │ │                focus range
1119          ╰╴full range
1120        "
1121        );
1122    }
1123
1124    #[test]
1125    fn create_procedure() {
1126        assert_snapshot!(
1127            symbols("create procedure hello() language sql as $$ select 1; $$;"),
1128            @r"
1129        info: procedure: public.hello
1130          ╭▸ 
1131        1 │ create procedure hello() language sql as $$ select 1; $$;
1132          │ ┬────────────────┯━━━━──────────────────────────────────
1133          │ │                │
1134          │ │                focus range
1135          ╰╴full range
1136        "
1137        );
1138    }
1139
1140    #[test]
1141    fn create_index() {
1142        assert_snapshot!(symbols("
1143create index idx_users_email on users (email);
1144"), @r"
1145        info: index: idx_users_email
1146          ╭▸ 
1147        2 │ create index idx_users_email on users (email);
1148          │ ┬────────────┯━━━━━━━━━━━━━━─────────────────
1149          │ │            │
1150          │ │            focus range
1151          ╰╴full range
1152        ");
1153    }
1154
1155    #[test]
1156    fn create_domain() {
1157        assert_snapshot!(
1158            symbols("create domain email_addr as text;"),
1159            @r"
1160        info: domain: public.email_addr
1161          ╭▸ 
1162        1 │ create domain email_addr as text;
1163          │ ┬─────────────┯━━━━━━━━━────────
1164          │ │             │
1165          │ │             focus range
1166          ╰╴full range
1167        "
1168        );
1169    }
1170
1171    #[test]
1172    fn create_sequence() {
1173        assert_snapshot!(
1174            symbols("create sequence user_id_seq;"),
1175            @r"
1176        info: sequence: public.user_id_seq
1177          ╭▸ 
1178        1 │ create sequence user_id_seq;
1179          │ ┬───────────────┯━━━━━━━━━━
1180          │ │               │
1181          │ │               focus range
1182          ╰╴full range
1183        "
1184        );
1185    }
1186
1187    #[test]
1188    fn create_trigger() {
1189        assert_snapshot!(symbols("
1190create trigger update_timestamp
1191  before update on users
1192  execute function update_modified_column();
1193"), @r"
1194        info: trigger: update_timestamp
1195          ╭▸ 
1196        2 │   create trigger update_timestamp
1197          │   │              ━━━━━━━━━━━━━━━━ focus range
1198          │ ┌─┘
1199          │ │
1200        3 │ │   before update on users
1201        4 │ │   execute function update_modified_column();
1202          ╰╴└───────────────────────────────────────────┘ full range
1203        ");
1204    }
1205
1206    #[test]
1207    fn create_event_trigger() {
1208        assert_snapshot!(
1209            symbols("create event trigger et on ddl_command_start execute function f();"),
1210            @r"
1211        info: event trigger: et
1212          ╭▸ 
1213        1 │ create event trigger et on ddl_command_start execute function f();
1214          │ ┬────────────────────┯━──────────────────────────────────────────
1215          │ │                    │
1216          │ │                    focus range
1217          ╰╴full range
1218        "
1219        );
1220    }
1221
1222    #[test]
1223    fn create_tablespace() {
1224        assert_snapshot!(symbols("
1225create tablespace dbspace location '/data/dbs';
1226"), @r"
1227        info: tablespace: dbspace
1228          ╭▸ 
1229        2 │ create tablespace dbspace location '/data/dbs';
1230          │ ┬─────────────────┯━━━━━━─────────────────────
1231          │ │                 │
1232          │ │                 focus range
1233          ╰╴full range
1234        ");
1235    }
1236
1237    #[test]
1238    fn create_database() {
1239        assert_snapshot!(
1240            symbols("create database mydb;"),
1241            @r"
1242        info: database: mydb
1243          ╭▸ 
1244        1 │ create database mydb;
1245          │ ┬───────────────┯━━━
1246          │ │               │
1247          │ │               focus range
1248          ╰╴full range
1249        "
1250        );
1251    }
1252
1253    #[test]
1254    fn create_server() {
1255        assert_snapshot!(symbols("
1256create server myserver foreign data wrapper postgres_fdw;
1257"), @r"
1258        info: server: myserver
1259          ╭▸ 
1260        2 │ create server myserver foreign data wrapper postgres_fdw;
1261          │ ┬─────────────┯━━━━━━━──────────────────────────────────
1262          │ │             │
1263          │ │             focus range
1264          ╰╴full range
1265        ");
1266    }
1267
1268    #[test]
1269    fn create_extension() {
1270        assert_snapshot!(
1271            symbols("create extension pgcrypto;"),
1272            @r"
1273        info: extension: pgcrypto
1274          ╭▸ 
1275        1 │ create extension pgcrypto;
1276          │ ┬────────────────┯━━━━━━━
1277          │ │                │
1278          │ │                focus range
1279          ╰╴full range
1280        "
1281        );
1282    }
1283
1284    #[test]
1285    fn create_role() {
1286        assert_snapshot!(symbols("
1287create role reader;
1288"), @r"
1289        info: role: reader
1290          ╭▸ 
1291        2 │ create role reader;
1292          │ ┬───────────┯━━━━━
1293          │ │           │
1294          │ │           focus range
1295          ╰╴full range
1296        ");
1297    }
1298
1299    #[test]
1300    fn create_policy() {
1301        assert_snapshot!(symbols("
1302create policy allow_read on t;
1303"), @r"
1304        info: policy: allow_read
1305          ╭▸ 
1306        2 │ create policy allow_read on t;
1307          │ ┬─────────────┯━━━━━━━━━─────
1308          │ │             │
1309          │ │             focus range
1310          ╰╴full range
1311        ");
1312    }
1313
1314    #[test]
1315    fn multiple_symbols() {
1316        assert_snapshot!(symbols("
1317create table users (id int);
1318create table posts (id int);
1319create function get_user(user_id int) returns void as $$ select 1; $$ language sql;
1320"), @r"
1321        info: table: public.users
1322          ╭▸ 
1323        2 │ create table users (id int);
1324          │ ┬────────────┯━━━━─────────
1325          │ │            │
1326          │ │            focus range
1327          │ full range
132813291330        2 │ create table users (id int);
1331          │                     ┯━────
1332          │                     │
1333          │                     full range for `column: id int`
1334          │                     focus range
1335          ╰╴
1336        info: table: public.posts
1337          ╭▸ 
1338        3 │ create table posts (id int);
1339          │ ┬────────────┯━━━━─────────
1340          │ │            │
1341          │ │            focus range
1342          │ full range
134313441345        3 │ create table posts (id int);
1346          │                     ┯━────
1347          │                     │
1348          │                     full range for `column: id int`
1349          ╰╴                    focus range
1350        info: function: public.get_user
1351          ╭▸ 
1352        4 │ create function get_user(user_id int) returns void as $$ select 1; $$ language sql;
1353          │ ┬───────────────┯━━━━━━━──────────────────────────────────────────────────────────
1354          │ │               │
1355          │ │               focus range
1356          ╰╴full range
1357        ");
1358    }
1359
1360    #[test]
1361    fn qualified_names() {
1362        assert_snapshot!(symbols("
1363create table public.users (id int);
1364create function my_schema.hello() returns void as $$ select 1; $$ language sql;
1365"), @r"
1366        info: table: public.users
1367          ╭▸ 
1368        2 │ create table public.users (id int);
1369          │ ┬───────────────────┯━━━━─────────
1370          │ │                   │
1371          │ │                   focus range
1372          │ full range
137313741375        2 │ create table public.users (id int);
1376          │                            ┯━────
1377          │                            │
1378          │                            full range for `column: id int`
1379          │                            focus range
1380          ╰╴
1381        info: function: my_schema.hello
1382          ╭▸ 
1383        3 │ create function my_schema.hello() returns void as $$ select 1; $$ language sql;
1384          │ ┬─────────────────────────┯━━━━───────────────────────────────────────────────
1385          │ │                         │
1386          │ │                         focus range
1387          ╰╴full range
1388        ");
1389    }
1390
1391    #[test]
1392    fn create_type() {
1393        assert_snapshot!(
1394            symbols("create type status as enum ('active', 'inactive');"),
1395            @r"
1396        info: enum: public.status
1397          ╭▸ 
1398        1 │ create type status as enum ('active', 'inactive');
1399          │ ┬───────────┯━━━━━───────────────────────────────
1400          │ │           │
1401          │ │           focus range
1402          │ full range
140314041405        1 │ create type status as enum ('active', 'inactive');
1406          │                             ┯━━━━━━━  ┯━━━━━━━━━
1407          │                             │         │
1408          │                             │         full range for `variant: inactive`
1409          │                             │         focus range
1410          │                             full range for `variant: active`
1411          ╰╴                            focus range
1412        "
1413        );
1414    }
1415
1416    #[test]
1417    fn create_type_composite() {
1418        assert_snapshot!(
1419            symbols("create type person as (name text, age int);"),
1420            @r"
1421        info: type: public.person
1422          ╭▸ 
1423        1 │ create type person as (name text, age int);
1424          │ ┬───────────┯━━━━━────────────────────────
1425          │ │           │
1426          │ │           focus range
1427          │ full range
142814291430        1 │ create type person as (name text, age int);
1431          │                        ┯━━━─────  ┯━━────
1432          │                        │          │
1433          │                        │          full range for `column: age int`
1434          │                        │          focus range
1435          │                        full range for `column: name text`
1436          ╰╴                       focus range
1437        "
1438        );
1439    }
1440
1441    #[test]
1442    fn create_type_composite_multiple_columns() {
1443        assert_snapshot!(
1444            symbols("create type address as (street text, city text, zip varchar(10));"),
1445            @r"
1446        info: type: public.address
1447          ╭▸ 
1448        1 │ create type address as (street text, city text, zip varchar(10));
1449          │ ┬───────────┯━━━━━━─────────────────────────────────────────────
1450          │ │           │
1451          │ │           focus range
1452          │ full range
145314541455        1 │ create type address as (street text, city text, zip varchar(10));
1456          │                         ┯━━━━━─────  ┯━━━─────  ┯━━────────────
1457          │                         │            │          │
1458          │                         │            │          full range for `column: zip varchar(10)`
1459          │                         │            │          focus range
1460          │                         │            full range for `column: city text`
1461          │                         │            focus range
1462          │                         full range for `column: street text`
1463          ╰╴                        focus range
1464        "
1465        );
1466    }
1467
1468    #[test]
1469    fn create_type_with_schema() {
1470        assert_snapshot!(
1471            symbols("create type myschema.status as enum ('active', 'inactive');"),
1472            @r"
1473        info: enum: myschema.status
1474          ╭▸ 
1475        1 │ create type myschema.status as enum ('active', 'inactive');
1476          │ ┬────────────────────┯━━━━━───────────────────────────────
1477          │ │                    │
1478          │ │                    focus range
1479          │ full range
148014811482        1 │ create type myschema.status as enum ('active', 'inactive');
1483          │                                      ┯━━━━━━━  ┯━━━━━━━━━
1484          │                                      │         │
1485          │                                      │         full range for `variant: inactive`
1486          │                                      │         focus range
1487          │                                      full range for `variant: active`
1488          ╰╴                                     focus range
1489        "
1490        );
1491    }
1492
1493    #[test]
1494    fn create_type_enum_multiple_variants() {
1495        assert_snapshot!(
1496            symbols("create type priority as enum ('low', 'medium', 'high', 'urgent');"),
1497            @r"
1498        info: enum: public.priority
1499          ╭▸ 
1500        1 │ create type priority as enum ('low', 'medium', 'high', 'urgent');
1501          │ ┬───────────┯━━━━━━━────────────────────────────────────────────
1502          │ │           │
1503          │ │           focus range
1504          │ full range
150515061507        1 │ create type priority as enum ('low', 'medium', 'high', 'urgent');
1508          │                               ┯━━━━  ┯━━━━━━━  ┯━━━━━  ┯━━━━━━━
1509          │                               │      │         │       │
1510          │                               │      │         │       full range for `variant: urgent`
1511          │                               │      │         │       focus range
1512          │                               │      │         full range for `variant: high`
1513          │                               │      │         focus range
1514          │                               │      full range for `variant: medium`
1515          │                               │      focus range
1516          │                               full range for `variant: low`
1517          ╰╴                              focus range
1518        "
1519        );
1520    }
1521
1522    #[test]
1523    fn declare_cursor() {
1524        assert_snapshot!(symbols("
1525declare c scroll cursor for select * from t;
1526"), @r"
1527        info: cursor: c
1528          ╭▸ 
1529        2 │ declare c scroll cursor for select * from t;
1530          │ ┬───────┯──────────────────────────────────
1531          │ │       │
1532          │ │       focus range
1533          ╰╴full range
1534        ");
1535    }
1536
1537    #[test]
1538    fn prepare_statement() {
1539        assert_snapshot!(symbols("
1540prepare stmt as select 1;
1541"), @r"
1542        info: prepared statement: stmt
1543          ╭▸ 
1544        2 │ prepare stmt as select 1;
1545          │ ┬───────┯━━━────────────
1546          │ │       │
1547          │ │       focus range
1548          ╰╴full range
1549        ");
1550    }
1551
1552    #[test]
1553    fn empty_file() {
1554        symbols_not_found("")
1555    }
1556
1557    #[test]
1558    fn non_create_statements() {
1559        symbols_not_found("select * from users;")
1560    }
1561
1562    #[test]
1563    fn cte_table() {
1564        assert_snapshot!(
1565            symbols("
1566with recent_users as (
1567  select id, email as user_email
1568  from users
1569)
1570select * from recent_users;
1571"),
1572            @r"
1573        info: table: recent_users
1574          ╭▸ 
1575        2 │   with recent_users as (
1576          │        │━━━━━━━━━━━
1577          │        │
1578          │ ┌──────focus range
1579          │ │
1580        3 │ │   select id, email as user_email
1581        4 │ │   from users
1582        5 │ │ )
1583          ╰╴└─┘ full range
1584        "
1585        );
1586    }
1587
1588    #[test]
1589    fn cte_table_with_column_list() {
1590        assert_snapshot!(
1591            symbols("
1592with t(a, b, c) as (
1593  select 1, 2, 3
1594)
1595select * from t;
1596"),
1597            @r"
1598        info: table: t
1599          ╭▸ 
1600        2 │   with t(a, b, c) as (
1601          │        ━ focus range
1602          │ ┌──────┘
1603          │ │
1604        3 │ │   select 1, 2, 3
1605        4 │ │ )
1606          │ └─┘ full range
160716081609        2 │   with t(a, b, c) as (
1610          │          ┯  ┯  ┯
1611          │          │  │  │
1612          │          │  │  full range for `column: c`
1613          │          │  │  focus range
1614          │          │  full range for `column: b`
1615          │          │  focus range
1616          │          full range for `column: a`
1617          ╰╴         focus range
1618        "
1619        );
1620    }
1621
1622    #[test]
1623    fn create_foreign_table() {
1624        assert_snapshot!(symbols("
1625create foreign table films (
1626  code char(5),
1627  title varchar(40)
1628) server film_server;
1629"), @r"
1630        info: table: public.films
1631          ╭▸ 
1632        2 │   create foreign table films (
1633          │   │                    ━━━━━ focus range
1634          │ ┌─┘
1635          │ │
1636        3 │ │   code char(5),
1637        4 │ │   title varchar(40)
1638        5 │ │ ) server film_server;
1639          │ └────────────────────┘ full range
164016411642        3 │     code char(5),
1643          │     ┯━━━────────
1644          │     │
1645          │     full range for `column: code char(5)`
1646          │     focus range
1647        4 │     title varchar(40)
1648          │     ┯━━━━────────────
1649          │     │
1650          │     full range for `column: title varchar(40)`
1651          ╰╴    focus range
1652        ");
1653    }
1654}