LCOV - code coverage report
Current view: top level - dashql-core-session/src - script_session.rs Coverage Total Hit
Test: DashQL 6e74948 Lines: 95.1 % 878 835
Test Date: 2026-11-06 17:46:50

            Line data    Source code
       1              : //! Editor sessions.
       2              : //!
       3              : //! A session owns the script of one editor document. Editor events are validated and applied
       4              : //! atomically, offsets at the API boundary use the unit chosen when the session is created (UTF-8
       5              : //! bytes or UTF-16 code units), and every operation answers with an `EditorUpdate` that projects
       6              : //! the current analysis (diagnostics, syntax and semantic spans, cursor, annotations, statistics).
       7              : //!
       8              : //! **Analysis versions.** An analysis is identified by the text version and the catalog version it
       9              : //! was analyzed at. An event names the version of the analysis the host already holds
      10              : //! (`known_analysis`). If the analysis after the event has that version, the update leaves the
      11              : //! analysis out and sets `analysis_omitted`, so a cursor move sends only the cursor.
      12              : //!
      13              : //! **Catalog ownership.** A session does not hold its catalog: every call that reads it takes the
      14              : //! catalog, and publishing takes it mutably. A session belongs to the catalog it was created with,
      15              : //! and calls must pass that catalog. Whoever owns the session and the catalog removes the script from
      16              : //! the catalog when the session ends (`drop_from_catalog`); dropping a session leaves the catalog as it
      17              : //! is. The wasm module shares the catalog between its JS objects.
      18              : //!
      19              : //! Projections convert the mostly ascending offsets of a script in one pass (`Utf16Projector`).
      20              : 
      21              : use dashql_core::analyzer::{AnalyzedScript, ExpressionInner, TableReference};
      22              : use dashql_core::catalog::{Catalog, CatalogEntryId, CatalogLayer, CatalogTableId, QualifiedTableName, PROTO_NULL_U32};
      23              : use crate::compiler::{compile, CompilationOptions};
      24              : use dashql_core::completion::is_symbol_kind_completable;
      25              : use dashql_core::cursor::CursorContext;
      26              : use dashql_core::error::{Error, Result};
      27              : use dashql_core::annotation::Annotation;
      28              : use dashql_core::vis_rewrite::{VisBoard, VisFlow, VisRenderer};
      29              : use dashql_core::pack::{pack_completion, pack_tokens};
      30              : use dashql_core::script::{Script, TextVersion};
      31              : use dashql_core::table_parameters::TableParameters;
      32              : use dashql_core::script_diff::ScriptDiff;
      33              : use dashql_core::utils::rope::{resolve_byte_boundary, resolve_utf16_boundary, TextPosition, Utf16Projector};
      34              : use dashql_buffers::analyzer::AnalyzerErrorSeverity;
      35              : use dashql_buffers::completion::CompletionT;
      36              : use dashql_buffers::cursor::RelativeSymbolPosition;
      37              : use dashql_buffers::diff::ScriptDiffT;
      38              : use dashql_buffers::editor::{
      39              :     EditorCommentTagT, EditorCursorSemanticContextT, EditorCursorSemanticKind, EditorCursorStateT, EditorDiagnosticSeverity,
      40              :     EditorDiagnosticSource, EditorDiagnosticT, EditorEvent, EditorEventAction, EditorEventIntent, EditorEventOrigin,
      41              :     EditorEventT, EditorOffsetUnit, EditorProcessingStatisticsT, EditorAnalysisVersionT, EditorScriptAnnotationsT,
      42              :     EditorBoardPositionT, EditorFlowPositionT,
      43              :     EditorSelectionT, EditorSemanticReferenceKind, EditorSemanticResolution, EditorSemanticSpanT,
      44              :     EditorStatementDescriptionT, EditorStatementT, EditorSyntaxSpanT, EditorTableDefinitionT, EditorTextChangeT,
      45              :     EditorTextSpanT, EditorUpdateStatus, EditorUpdateT,
      46              : };
      47              : use dashql_buffers::execution::ScriptCompilationResultT;
      48              : use dashql_buffers::formatting::{FormattingConfigT, KeywordCase};
      49              : use dashql_buffers::parser::{AttributeKey, CommentBlockType, ScannerTokenType, SymbolSpan, SymbolSpanT, TextSpanT};
      50              : use dashql_buffers::status::StatusCode;
      51              : use dashql_parser::attrs;
      52              : use dashql_parser::parser::context::resolve_text_span;
      53              : use dashql_parser::span::TextSpan;
      54              : use ropey::Rope;
      55              : use std::sync::Arc;
      56              : 
      57              : /// A version that never matches
      58              : const NO_VERSION: u64 = u64::MAX;
      59              : 
      60              : /// A vector that is left out of the packed buffer when empty
      61          912 : fn non_empty<T>(v: Vec<T>) -> Option<Vec<T>> {
      62          912 :     (!v.is_empty()).then_some(v)
      63          912 : }
      64              : 
      65              : /// A string that is left out of the packed buffer when empty
      66           40 : fn non_empty_string(s: impl Into<String>) -> Option<String> {
      67           40 :     let s = s.into();
      68           40 :     (!s.is_empty()).then_some(s)
      69           40 : }
      70              : 
      71              : /// A text change, validated and resolved to byte and code point offsets
      72              : struct PreparedChange<'e> {
      73              :     /// The UTF-8 byte offset where the replaced range begins
      74              :     from_byte: usize,
      75              :     /// The UTF-8 byte offset where the replaced range ends
      76              :     to_byte: usize,
      77              :     /// The code point offset where the replaced range begins
      78              :     from_codepoint: usize,
      79              :     /// The code point offset where the replaced range ends
      80              :     to_codepoint: usize,
      81              :     /// The inserted text
      82              :     insert: &'e str,
      83              :     /// The index of the change in the event, orders changes at the same range
      84              :     input_order: usize,
      85              : }
      86              : 
      87              : /// The catalog ids of a resolved table
      88              : #[derive(Clone, Copy)]
      89              : struct ResolvedTableIds {
      90              :     /// The database id
      91              :     database_id: u32,
      92              :     /// The schema id
      93              :     schema_id: u32,
      94              :     /// The packed table id
      95              :     table_id: u64,
      96              :     /// The catalog version the reference was resolved against
      97              :     catalog_version: u32,
      98              : }
      99              : 
     100              : impl ResolvedTableIds {
     101           40 :     fn same_table(&self, other: &ResolvedTableIds) -> bool {
     102           40 :         self.database_id == other.database_id && self.schema_id == other.schema_id && self.table_id == other.table_id
     103           40 :     }
     104              : }
     105              : 
     106              : /// The ids of the table a reference resolved to
     107           72 : fn resolved_table_ids(reference: &TableReference) -> Option<ResolvedTableIds> {
     108           72 :     let resolved = reference.inner.as_ref()?.resolved_table.as_ref()?;
     109           59 :     let (database_id, schema_id) = resolved.catalog_schema_id.unpack_schema_id();
     110           59 :     Some(ResolvedTableIds {
     111           59 :         database_id,
     112           59 :         schema_id,
     113           59 :         table_id: resolved.catalog_table_id.unpack_table_id().pack(),
     114           59 :         catalog_version: resolved.referenced_catalog_version,
     115           59 :     })
     116           72 : }
     117              : 
     118              : /// The ids of the table and column of a resolved column reference
     119           91 : fn resolved_column_ids(expression: &dashql_core::analyzer::Expression) -> Option<(ResolvedTableIds, u32)> {
     120           91 :     let ExpressionInner::ColumnRef(column) = &expression.inner else {
     121            9 :         return None;
     122              :     };
     123           82 :     let resolved = column.resolved_ids?;
     124           75 :     let (database_id, schema_id) = resolved.catalog_schema_id.unpack_schema_id();
     125           75 :     let (table_id, column_id) = resolved.catalog_table_column_id.unpack_table_column_id();
     126           75 :     Some((
     127           75 :         ResolvedTableIds {
     128           75 :             database_id,
     129           75 :             schema_id,
     130           75 :             table_id: table_id.pack(),
     131           75 :             catalog_version: resolved.referenced_catalog_version,
     132           75 :         },
     133           75 :         column_id,
     134           75 :     ))
     135           91 : }
     136              : 
     137              : /// The entry that declares a resolved table, and the text of the declaration in the script of that
     138              : /// entry: the column definition of a resolved column, else the table name
     139           13 : fn declaration(
     140           13 :     analyzed: &AnalyzedScript,
     141           13 :     catalog: &Catalog,
     142           13 :     table_id: CatalogTableId,
     143           13 :     column: Option<u32>,
     144           13 : ) -> Option<(CatalogEntryId, Option<TextSpan>)> {
     145           13 :     let table = if table_id.origin == analyzed.catalog_entry_id() {
     146            0 :         analyzed.entry.table_by_id(table_id)
     147           13 :     } else if analyzed.snapshot.version() == catalog.version() {
     148              :         // A script that republishes the same declarations keeps the catalog version but moves their
     149              :         // locations, the catalog holds the current ones
     150           13 :         catalog.table_by_id(table_id)
     151              :     } else {
     152            0 :         analyzed.snapshot.table_by_id(table_id)
     153            2 :     }?;
     154           11 :     let column_location = column.and_then(|c| table.column_by_id(c)?.location);
     155           11 :     Some((table_id.origin, column_location.or(table.location)))
     156           13 : }
     157              : 
     158              : /// The table name with its schema, if it has one
     159           25 : fn format_table_name(name: &QualifiedTableName) -> String {
     160           25 :     let mut out = String::new();
     161           25 :     if !name.schema_name.text.is_empty() {
     162            0 :         out.push_str(&name.schema_name.text);
     163            0 :         out.push('.');
     164           25 :     }
     165           25 :     out.push_str(&name.table_name.text);
     166           25 :     out
     167           25 : }
     168              : 
     169              : /// Converts internal UTF-8 byte offsets to the offset unit of a session
     170              : enum Projector<'a> {
     171              :     Bytes,
     172              :     Utf16(Utf16Projector<'a>),
     173              : }
     174              : 
     175              : impl Projector<'_> {
     176              :     /// Project a byte offset, None if it does not lie on a code point boundary of the text. UTF-8
     177              :     /// offsets are passed through unchecked (see docs/design/core_quirks.md).
     178         1961 :     fn offset(&mut self, offset: u64) -> Option<u64> {
     179         1961 :         match self {
     180         1624 :             Projector::Bytes => Some(offset),
     181          337 :             Projector::Utf16(projector) => {
     182          337 :                 let offset = usize::try_from(offset).ok()?;
     183          337 :                 projector.project(offset).map(|o| o as u64)
     184              :             }
     185              :         }
     186         1961 :     }
     187              : 
     188              :     /// Project a byte span
     189          958 :     fn span(&mut self, span: TextSpan) -> Option<EditorTextSpanT> {
     190          958 :         let begin = self.offset(span.offset as u64)?;
     191          958 :         let end = self.offset(span.offset as u64 + span.length as u64)?;
     192          958 :         if end < begin {
     193            0 :             return None;
     194          958 :         }
     195          958 :         let mut out = EditorTextSpanT::default();
     196          958 :         out.offset = begin;
     197          958 :         out.length = end - begin;
     198          958 :         Some(out)
     199          958 :     }
     200              : }
     201              : 
     202              : /// A selection in the offset unit of a session
     203              : #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     204              : pub struct Selection {
     205              :     /// Where the selection starts, it stays when the selection is extended
     206              :     pub anchor: u64,
     207              :     /// Where the selection ends, the position of the cursor
     208              :     pub head: u64,
     209              : }
     210              : 
     211              : impl Selection {
     212           26 :     fn from_event(selection: &EditorSelectionT) -> Self {
     213           26 :         Self {
     214           26 :             anchor: selection.anchor,
     215           26 :             head: selection.head,
     216           26 :         }
     217           26 :     }
     218              : 
     219           45 :     fn pack(self) -> Box<EditorSelectionT> {
     220           45 :         let mut selection = EditorSelectionT::default();
     221           45 :         selection.anchor = self.anchor;
     222           45 :         selection.head = self.head;
     223           45 :         Box::new(selection)
     224           45 :     }
     225              : }
     226              : 
     227              : /// The state of one editor document
     228              : pub struct ScriptSession {
     229              :     /// The id of the catalog the session belongs to
     230              :     catalog_id: u64,
     231              :     /// The script of the document
     232              :     script: Script,
     233              :     /// The unit of the text offsets exchanged with the editor
     234              :     offset_unit: EditorOffsetUnit,
     235              :     /// Bumped by every committed change of the text, selection or analysis
     236              :     state_revision: u64,
     237              :     /// The text version the cursor is current with, `NO_VERSION` if not placed
     238              :     cursor_text_version: TextVersion,
     239              :     /// The catalog version the cursor is current with, `NO_VERSION` if not placed
     240              :     cursor_catalog_revision: u64,
     241              :     /// The primary editor selection, in the offset unit of the session
     242              :     primary_selection: Option<Selection>,
     243              : }
     244              : 
     245              : impl ScriptSession {
     246              :     /// Create an empty session whose script gets a new entry id of the catalog
     247           67 :     pub fn new(catalog: &Catalog, offset_unit: EditorOffsetUnit) -> Result<Self> {
     248           67 :         Self::with_script(catalog, Script::new(catalog), offset_unit)
     249           67 :     }
     250              : 
     251              :     /// Create an empty session whose script takes an entry id that the id source of the catalog
     252              :     /// allocated before, e.g. the id of a script that moves to another catalog of the same source.
     253              :     /// The catalog must not hold an entry with the id.
     254            3 :     pub fn with_entry_id(catalog: &Catalog, offset_unit: EditorOffsetUnit, id: CatalogEntryId) -> Result<Self> {
     255            3 :         if !catalog.entry_id_allocated(id) || catalog.contains(id) {
     256            2 :             return Err(Error::invalid_argument("invalid or colliding script catalog entry ID"));
     257            1 :         }
     258            1 :         Self::with_script(catalog, Script::with_entry_id(id), offset_unit)
     259            3 :     }
     260              : 
     261           68 :     fn with_script(catalog: &Catalog, script: Script, offset_unit: EditorOffsetUnit) -> Result<Self> {
     262           68 :         if offset_unit != EditorOffsetUnit::UTF8_BYTES && offset_unit != EditorOffsetUnit::UTF16_CODE_UNITS {
     263            1 :             return Err(Error::invalid_argument("invalid editor offset unit"));
     264           67 :         }
     265           67 :         Ok(Self {
     266           67 :             catalog_id: catalog.id(),
     267           67 :             script,
     268           67 :             offset_unit,
     269           67 :             state_revision: 0,
     270           67 :             cursor_text_version: NO_VERSION,
     271           67 :             cursor_catalog_revision: NO_VERSION,
     272           67 :             primary_selection: None,
     273           67 :         })
     274           68 :     }
     275              : 
     276          218 :     pub fn catalog_entry_id(&self) -> CatalogEntryId {
     277          218 :         self.script.catalog_entry_id()
     278          218 :     }
     279              : 
     280              :     /// The text
     281           59 :     pub fn text(&self) -> String {
     282           59 :         self.script.to_text()
     283           59 :     }
     284              : 
     285              :     /// The text version, bumped by every committed text change
     286          493 :     pub fn text_version(&self) -> TextVersion {
     287          493 :         self.script.text_version()
     288          493 :     }
     289              : 
     290              :     /// The revision bumped by every committed change of the text, selection or analysis
     291            1 :     pub fn state_revision(&self) -> u64 {
     292            1 :         self.state_revision
     293            1 :     }
     294              : 
     295            2 :     pub fn offset_unit(&self) -> EditorOffsetUnit {
     296            2 :         self.offset_unit
     297            2 :     }
     298              : 
     299              :     /// The primary selection, if the host supplied one
     300            5 :     pub fn primary_selection(&self) -> Option<Selection> {
     301            5 :         self.primary_selection
     302            5 :     }
     303              : 
     304           22 :     pub fn script(&self) -> &Script {
     305           22 :         &self.script
     306           22 :     }
     307              : 
     308              :     /// The script, for workflows such as compilation and execution. Direct edits share the text
     309              :     /// version of the session.
     310            7 :     pub fn script_mut(&mut self) -> &mut Script {
     311            7 :         &mut self.script
     312            7 :     }
     313              : 
     314              :     /// The end of the document in the offset unit
     315          139 :     fn document_end(&self) -> u64 {
     316          139 :         match self.offset_unit {
     317           34 :             EditorOffsetUnit::UTF16_CODE_UNITS => self.script.text().len_utf16_cu() as u64,
     318          105 :             _ => self.script.text().len_bytes() as u64,
     319              :         }
     320          139 :     }
     321              : 
     322              :     /// Resolve an offset of the session unit to a position in a text
     323          272 :     fn resolve_offset(&self, text: &Rope, offset: u64) -> Option<TextPosition> {
     324          272 :         let offset = usize::try_from(offset).ok()?;
     325          272 :         match self.offset_unit {
     326           67 :             EditorOffsetUnit::UTF16_CODE_UNITS => resolve_utf16_boundary(text, offset),
     327          205 :             _ => resolve_byte_boundary(text, offset),
     328              :         }
     329          272 :     }
     330              : 
     331              :     /// A projector of byte offsets into the current text. `text` speeds up UTF-16 projections and
     332              :     /// must equal the current text.
     333          102 :     fn projector<'a>(&'a self, text: Option<&'a str>) -> Projector<'a> {
     334          102 :         match self.offset_unit {
     335              :             EditorOffsetUnit::UTF16_CODE_UNITS => {
     336           27 :                 let rope = self.script.text();
     337           27 :                 Projector::Utf16(Utf16Projector::new(text.filter(|t| t.len() == rope.len_bytes()), rope))
     338              :             }
     339           75 :             _ => Projector::Bytes,
     340              :         }
     341          102 :     }
     342              : 
     343              :     /// Set the results of the scripts of the session that the table parameters read. An analysis with other results
     344              :     /// is not current.
     345            2 :     pub fn set_table_parameters(&mut self, results: Option<Arc<TableParameters>>) {
     346            2 :         self.script.table_parameters = results;
     347            2 :     }
     348              : 
     349              :     /// The named table parameters of the analysis that reference scripts, `:foo`, with the script that each finds among
     350              :     /// `names` (`find_table_parameter`), in text order and in the offset unit of the session. Positional parameters and the
     351              :     /// targets of writes reference no script. Empty if the analysis lags behind the text.
     352            2 :     pub fn script_references(&self, names: &[String]) -> Vec<(EditorTextSpanT, Option<usize>)> {
     353              :         use dashql_buffers::parser::AttributeKey;
     354              :         use dashql_core::analyzer::ParameterKind;
     355              :         use dashql_core::table_parameters::{find_table_parameter, TableParameterLookup};
     356            2 :         let Some((scanned, parsed, analyzed)) = self.script.current_analysis() else {
     357            1 :             return Vec::new();
     358              :         };
     359            1 :         let mut found = Vec::new();
     360            4 :         for parameter in analyzed.parameters.iter().filter(|p| p.kind == ParameterKind::Table) {
     361            3 :             let Some(name) = parameter.name.as_deref() else { continue };
     362            3 :             let span = resolve_text_span(&scanned.text, &scanned.symbols, parameter.location);
     363            3 :             if scanned.text.as_bytes().get(span.offset as usize) != Some(&b':') {
     364            0 :                 continue;
     365            3 :             }
     366            3 :             let tableref = &parsed.nodes[parsed.nodes[parameter.ast_node_id as usize].parent() as usize];
     367            3 :             if matches!(tableref.attribute_key(), AttributeKey::SQL_INSERT_TARGET | AttributeKey::SQL_DELETE_TARGET) {
     368            1 :                 continue;
     369            2 :             }
     370            2 :             let script = match find_table_parameter(names.iter().map(String::as_str), name) {
     371            2 :                 TableParameterLookup::Found(i) => Some(i),
     372            0 :                 _ => None,
     373              :             };
     374            2 :             found.push((span, script));
     375              :         }
     376            1 :         let spans = self.project_text_spans(found.iter().map(|(span, _)| *span));
     377            2 :         found.into_iter().zip(spans).filter_map(|((_, script), span)| Some((span?, script))).collect()
     378            2 :     }
     379              : 
     380              :     /// Project byte spans of the current text into the offset unit of the session, None for a span that does not lie
     381              :     /// on code point boundaries
     382            1 :     pub fn project_text_spans(&self, spans: impl IntoIterator<Item = TextSpan>) -> Vec<Option<EditorTextSpanT>> {
     383            1 :         let mut projector = self.projector(None);
     384            2 :         spans.into_iter().map(|span| projector.span(span)).collect()
     385            1 :     }
     386              : 
     387              :     /// Project a byte offset into the current text or fail
     388            5 :     fn project_byte_offset(projector: &mut Projector, offset: u64) -> Result<u64> {
     389            5 :         projector
     390            5 :             .offset(offset)
     391            5 :             .ok_or_else(|| Error::invalid_argument("internal text span is not on a UTF-8 codepoint boundary"))
     392            5 :     }
     393              : 
     394              :     /// Does the analysis match the text and the catalog?
     395              :     ///
     396              :     /// The analysis is current if it was analyzed from the current text, and against a snapshot of
     397              :     /// the catalog that is still current.
     398          433 :     fn is_analysis_current(&self, catalog: &Catalog) -> bool {
     399          433 :         self.script.current_analysis().is_some_and(|(_, _, analyzed)| catalog.is_current(&analyzed.snapshot)) && self.script.table_parameters_current()
     400          433 :     }
     401              : 
     402              :     /// The revision and selection part of an update
     403          141 :     fn make_update(&self, catalog: &Catalog) -> EditorUpdateT {
     404          141 :         debug_assert_eq!(catalog.id(), self.catalog_id, "a session takes the catalog it was created with");
     405          141 :         let mut update = EditorUpdateT::default();
     406          141 :         update.status = EditorUpdateStatus::OK;
     407          141 :         update.offset_unit = self.offset_unit;
     408          141 :         update.catalog_entry_id = self.catalog_entry_id();
     409          141 :         update.text_version = self.text_version();
     410          141 :         update.state_revision = self.state_revision;
     411          141 :         update.catalog_revision = catalog.version() as u64;
     412          141 :         update.analysis_available = self.is_analysis_current(catalog);
     413          141 :         update.primary_selection = self.primary_selection.map(Selection::pack);
     414          141 :         update
     415          141 :     }
     416              : 
     417              :     /// The update of an event buffer that is not a valid EditorEvent
     418            3 :     pub fn invalid_event_update(&self, catalog: &Catalog) -> EditorUpdateT {
     419            3 :         let mut update = EditorUpdateT::default();
     420            3 :         update.status = EditorUpdateStatus::INVALID_EVENT;
     421            3 :         update.status_message = Some("invalid EditorEvent FlatBuffer".to_string());
     422            3 :         update.offset_unit = self.offset_unit;
     423            3 :         update.catalog_entry_id = self.catalog_entry_id();
     424            3 :         update.text_version = self.text_version();
     425            3 :         update.state_revision = self.state_revision;
     426            3 :         update.catalog_revision = catalog.version() as u64;
     427            3 :         update
     428            3 :     }
     429              : 
     430              :     /// Bring the analysis up to date with the text and the catalog
     431           84 :     fn analyze_into(&mut self, catalog: &Catalog, update: &mut EditorUpdateT) -> bool {
     432           84 :         if self.is_analysis_current(catalog) {
     433            8 :             update.analysis_available = true;
     434            8 :             return true;
     435           76 :         }
     436           76 :         match self.script.analyze(catalog, true) {
     437              :             Ok(()) => {
     438           76 :                 update.analysis_updated = true;
     439           76 :                 update.analysis_available = true;
     440           76 :                 true
     441              :             }
     442            0 :             Err(error) => {
     443            0 :                 update.status = EditorUpdateStatus::ANALYSIS_FAILED;
     444            0 :                 update.status_message = non_empty_string(error.what());
     445            0 :                 if let Some(status) = error.status {
     446            0 :                     update.core_status = status;
     447            0 :                 }
     448            0 :                 false
     449              :             }
     450              :         }
     451           84 :     }
     452              : 
     453              :     /// Place the cursor at the head of the primary selection, if it resolves
     454           28 :     fn place_cursor_at_selection(&mut self) -> bool {
     455           28 :         let Some(head) = self.primary_selection.as_ref().map(|s| s.head) else {
     456            0 :             return false;
     457              :         };
     458           28 :         match self.resolve_offset(self.script.text(), head) {
     459           28 :             Some(position) => {
     460           28 :                 self.script.set_cursor_offset(Some(position.text_bytes));
     461           28 :                 true
     462              :             }
     463            0 :             None => false,
     464              :         }
     465           28 :     }
     466              : 
     467              :     /// Replace the whole text if the expected version matches
     468           30 :     pub fn replace_text(&mut self, catalog: &Catalog, expected_text_version: TextVersion, text: &str) -> EditorUpdateT {
     469           30 :         let mut change = EditorTextChangeT::default();
     470           30 :         change.from = 0;
     471           30 :         change.to = self.document_end();
     472           30 :         change.insert = Some(text.to_string());
     473           30 :         let mut event = EditorEventT::default();
     474           30 :         event.expected_text_version = expected_text_version;
     475           30 :         event.origin = EditorEventOrigin::SYSTEM;
     476           30 :         event.intent = EditorEventIntent::REPLACE;
     477           30 :         event.action = EditorEventAction::APPLY;
     478           30 :         event.changes = Some(vec![change]);
     479           30 :         self.apply(catalog, &event)
     480           30 :     }
     481              : 
     482              :     /// Move the primary cursor without changing the text
     483           16 :     pub fn set_primary_cursor(&mut self, catalog: &Catalog, expected_text_version: TextVersion, offset: u64) -> EditorUpdateT {
     484           16 :         let mut selection = EditorSelectionT::default();
     485           16 :         selection.anchor = offset;
     486           16 :         selection.head = offset;
     487           16 :         let mut event = EditorEventT::default();
     488           16 :         event.expected_text_version = expected_text_version;
     489           16 :         event.origin = EditorEventOrigin::USER;
     490           16 :         event.intent = EditorEventIntent::SELECTION;
     491           16 :         event.primary_selection = Some(Box::new(selection));
     492           16 :         self.apply(catalog, &event)
     493           16 :     }
     494              : 
     495              :     /// Apply an EditorEvent FlatBuffer. Buffers that fail verification, including inserts that are
     496              :     /// not valid UTF-8, are rejected with INVALID_EVENT. The verifier checks strings, so an invalid insert
     497              :     /// never reaches `apply`.
     498            4 :     pub fn apply_buffer(&mut self, catalog: &Catalog, event: &[u8]) -> EditorUpdateT {
     499            4 :         match flatbuffers::root::<EditorEvent>(event) {
     500            1 :             Ok(event) => self.apply(catalog, &event.unpack()),
     501            3 :             Err(_) => self.invalid_event_update(catalog),
     502              :         }
     503            4 :     }
     504              : 
     505              :     /// Validate and apply an editor event atomically
     506          113 :     pub fn apply(&mut self, catalog: &Catalog, event: &EditorEventT) -> EditorUpdateT {
     507          113 :         let known = event.known_analysis.as_deref();
     508          113 :         let mut update = self.make_update(catalog);
     509          113 :         update.origin = event.origin;
     510          113 :         update.intent = event.intent;
     511          113 :         update.action = event.action;
     512          113 :         if event.expected_text_version != self.text_version() {
     513            4 :             update.status = EditorUpdateStatus::STALE_TEXT_VERSION;
     514            4 :             update.status_message = Some("editor event targets a stale text version".to_string());
     515            4 :             return self.finalize_update(catalog, update, known);
     516          109 :         }
     517              : 
     518              :         // Validate the changes in pre-change offsets
     519          109 :         let text = self.script.text();
     520          109 :         let document_end = self.document_end();
     521          109 :         let event_changes = event.changes.as_deref().unwrap_or(&[]);
     522          109 :         let mut changes = Vec::with_capacity(event_changes.len());
     523          109 :         for (i, change) in event_changes.iter().enumerate() {
     524           94 :             let insert = change.insert.as_deref().unwrap_or("");
     525           94 :             let insert_past_end = change.from == change.to && change.from > document_end;
     526           94 :             let (from_offset, to_offset) =
     527           94 :                 if insert_past_end { (document_end, document_end) } else { (change.from, change.to) };
     528           94 :             let from = self.resolve_offset(text, from_offset);
     529           94 :             let to = self.resolve_offset(text, to_offset);
     530           94 :             let (Some(from), Some(to)) = (from, to) else {
     531            1 :                 return self.reject(
     532            1 :                     catalog,
     533            1 :                     update,
     534            1 :                     known,
     535              :                     EditorUpdateStatus::INVALID_RANGE,
     536            1 :                     "text change range is not on codepoint boundaries in the session offset unit",
     537              :                 );
     538              :             };
     539           93 :             if change.from > change.to {
     540            1 :                 return self.reject(
     541            1 :                     catalog,
     542            1 :                     update,
     543            1 :                     known,
     544              :                     EditorUpdateStatus::INVALID_RANGE,
     545            1 :                     "text change range is not on codepoint boundaries in the session offset unit",
     546              :                 );
     547           92 :             }
     548           92 :             changes.push(PreparedChange {
     549           92 :                 from_byte: from.text_bytes,
     550           92 :                 to_byte: to.text_bytes,
     551           92 :                 from_codepoint: from.utf8_codepoints,
     552           92 :                 to_codepoint: to.utf8_codepoints,
     553           92 :                 insert,
     554           92 :                 input_order: i,
     555           92 :             });
     556              :         }
     557          107 :         changes.sort_by_key(|c| (c.from_byte, c.to_byte, c.input_order));
     558          107 :         if changes.windows(2).any(|w| w[1].from_byte < w[0].to_byte) {
     559            1 :             return self.reject(
     560            1 :                 catalog,
     561            1 :                 update,
     562            1 :                 known,
     563              :                 EditorUpdateStatus::OVERLAPPING_CHANGES,
     564            1 :                 "text changes overlap in the pre-change document",
     565              :             );
     566          106 :         }
     567              : 
     568              :         // Apply the changes back to front to a copy of the rope, which shares the unchanged nodes
     569          106 :         let mut text_changed = false;
     570          106 :         let mut edited = None;
     571          106 :         if let (Some(first), Some(last)) = (changes.first(), changes.last()) {
     572           87 :             let mut next = text.clone();
     573           90 :             for change in changes.iter().rev() {
     574           90 :                 next.remove(change.from_codepoint..change.to_codepoint);
     575           90 :                 next.insert(change.from_codepoint, change.insert);
     576           90 :             }
     577              :             // Text before the first and after the last change is unchanged, compare only the edited
     578              :             // range if the length stayed the same
     579           87 :             text_changed = next.len_bytes() != text.len_bytes()
     580            4 :                 || next.get_byte_slice(first.from_byte..last.to_byte)
     581            4 :                     != Some(text.byte_slice(first.from_byte..last.to_byte));
     582           87 :             edited = Some(next);
     583           19 :         }
     584          106 :         let next_text = edited.as_ref().unwrap_or(text);
     585              : 
     586          106 :         let mut next_selection = self.primary_selection;
     587          106 :         if let Some(selection) = event.primary_selection.as_deref().map(Selection::from_event) {
     588           26 :             if self.resolve_offset(next_text, selection.anchor).is_none()
     589           24 :                 || self.resolve_offset(next_text, selection.head).is_none()
     590              :             {
     591            2 :                 return self.reject(
     592            2 :                     catalog,
     593            2 :                     update,
     594            2 :                     known,
     595              :                     EditorUpdateStatus::INVALID_RANGE,
     596            2 :                     "primary selection is not on codepoint boundaries in the session offset unit",
     597              :                 );
     598           24 :             }
     599           24 :             next_selection = Some(selection);
     600           80 :         } else if text_changed {
     601           77 :             if let Some(selection) = &next_selection {
     602            3 :                 if self.resolve_offset(next_text, selection.anchor).is_none()
     603            3 :                     || self.resolve_offset(next_text, selection.head).is_none()
     604            1 :                 {
     605            1 :                     next_selection = None;
     606            2 :                 }
     607           74 :             }
     608            3 :         }
     609              : 
     610          104 :         update.text_changed = text_changed;
     611          104 :         update.selection_changed = self.primary_selection != next_selection;
     612          104 :         if text_changed {
     613           83 :             // The catalog keeps the published version until the owner publishes the new analysis.
     614           83 :             // The analysis of the script ignores its own catalog entry.
     615           83 :             self.script.set_text(edited.unwrap());
     616           83 :             self.script.set_cursor_offset(None);
     617           83 :             self.cursor_text_version = NO_VERSION;
     618           83 :             self.cursor_catalog_revision = NO_VERSION;
     619           83 :         }
     620          104 :         self.primary_selection = next_selection;
     621              : 
     622              :         // Selection updates also publish a complete projection. Refresh a stale analysis first,
     623              :         // otherwise a catalog change would turn a cursor move into an empty projection.
     624          104 :         let analysis_stale = !self.is_analysis_current(catalog);
     625          104 :         let analysis_succeeded =
     626          104 :             !(event.analyze || (event.primary_selection.is_some() && analysis_stale)) || self.analyze_into(catalog, &mut update);
     627          104 :         let catalog_version = catalog.version() as u64;
     628          104 :         let analysis_current = self.is_analysis_current(catalog);
     629          104 :         let cursor_outdated = event.primary_selection.is_some()
     630           80 :             || update.text_changed
     631            3 :             || update.analysis_updated
     632            3 :             || self.script.cursor_offset().is_none()
     633            0 :             || self.cursor_text_version != self.text_version()
     634            0 :             || self.cursor_catalog_revision != catalog_version;
     635          104 :         if analysis_succeeded && analysis_current && self.primary_selection.is_some() && cursor_outdated {
     636           25 :             if self.place_cursor_at_selection() {
     637           25 :                 self.cursor_text_version = self.text_version();
     638           25 :                 self.cursor_catalog_revision = catalog_version;
     639           25 :             }
     640           79 :         } else if analysis_succeeded && analysis_current && self.primary_selection.is_none() {
     641           43 :             self.script.set_cursor_offset(None);
     642           43 :             self.cursor_text_version = NO_VERSION;
     643           43 :             self.cursor_catalog_revision = NO_VERSION;
     644           44 :         }
     645          104 :         if update.text_changed || update.selection_changed || update.analysis_updated {
     646          101 :             self.state_revision += 1;
     647          101 :         }
     648              : 
     649          104 :         update.text_version = self.text_version();
     650          104 :         update.state_revision = self.state_revision;
     651          104 :         update.catalog_revision = catalog_version;
     652          104 :         update.analysis_available = analysis_current;
     653          104 :         update.primary_selection = self.primary_selection.map(Selection::pack);
     654          104 :         self.finalize_update(catalog, update, known)
     655          113 :     }
     656              : 
     657              :     /// Reject an event with a status
     658            5 :     fn reject(
     659            5 :         &self,
     660            5 :         catalog: &Catalog,
     661            5 :         mut update: EditorUpdateT,
     662            5 :         known: Option<&EditorAnalysisVersionT>,
     663            5 :         status: EditorUpdateStatus,
     664            5 :         message: &str,
     665            5 :     ) -> EditorUpdateT {
     666            5 :         update.status = status;
     667            5 :         update.status_message = Some(message.to_string());
     668            5 :         self.finalize_update(catalog, update, known)
     669            5 :     }
     670              : 
     671              :     /// Bring the analysis up to date with the text and the catalog and project it (`Analyze()`)
     672           28 :     pub fn analyze(&mut self, catalog: &Catalog) -> EditorUpdateT {
     673           28 :         let mut update = self.make_update(catalog);
     674           28 :         if self.analyze_into(catalog, &mut update) {
     675           28 :             let catalog_version = catalog.version() as u64;
     676           28 :             let cursor_outdated = update.analysis_updated
     677            8 :                 || self.script.cursor_offset().is_none()
     678            3 :                 || self.cursor_text_version != self.text_version()
     679            3 :                 || self.cursor_catalog_revision != catalog_version;
     680           28 :             if self.primary_selection.is_some() && cursor_outdated {
     681            3 :                 self.place_cursor_at_selection();
     682            3 :                 self.cursor_text_version = self.text_version();
     683            3 :                 self.cursor_catalog_revision = catalog_version;
     684           25 :             }
     685           28 :             if update.analysis_updated {
     686           20 :                 self.state_revision += 1;
     687           20 :             }
     688            0 :         }
     689           28 :         update.text_version = self.text_version();
     690           28 :         update.state_revision = self.state_revision;
     691           28 :         update.catalog_revision = catalog.version() as u64;
     692           28 :         self.finalize_update(catalog, update, None)
     693           28 :     }
     694              : 
     695              :     /// Add the projections of the analysis to an update. The analysis is left out
     696              :     /// if the host already holds it (`known`).
     697          141 :     fn finalize_update(
     698          141 :         &self,
     699          141 :         catalog: &Catalog,
     700          141 :         mut update: EditorUpdateT,
     701          141 :         known: Option<&EditorAnalysisVersionT>,
     702          141 :     ) -> EditorUpdateT {
     703          141 :         self.project_editor_state(catalog, &mut update, known);
     704          141 :         update
     705          141 :     }
     706              : 
     707              :     /// Project the current analysis and the cursor
     708          141 :     fn project_editor_state(
     709          141 :         &self,
     710          141 :         catalog: &Catalog,
     711          141 :         update: &mut EditorUpdateT,
     712          141 :         known: Option<&EditorAnalysisVersionT>,
     713          141 :     ) {
     714          141 :         if !update.analysis_available {
     715           43 :             return;
     716           98 :         }
     717           98 :         let (Some(parsed), Some(analyzed)) = (self.script.parsed.as_ref(), self.script.analyzed.as_ref()) else {
     718            0 :             return;
     719              :         };
     720           98 :         if !Arc::ptr_eq(&analyzed.parsed, parsed) {
     721            0 :             return;
     722           98 :         }
     723           98 :         let mut projector = self.projector(Some(&parsed.scanned.text));
     724              :         // A current analysis is identified by the text version and the catalog version
     725              :         // A new analysis of the results of other scripts has the versions of the previous one
     726           98 :         let omitted = !update.analysis_updated && known.is_some_and(|k| {
     727            2 :             k.text_version == self.text_version() && k.catalog_revision == catalog.version() as u64
     728            2 :         });
     729           98 :         if omitted {
     730            2 :             update.analysis_omitted = true;
     731           96 :         } else {
     732           96 :             self.project_analysis(update, analyzed, &mut projector);
     733           96 :         }
     734              : 
     735              :         // The collapsed primary cursor
     736           98 :         let collapsed = self.primary_selection.as_ref().is_some_and(|s| s.anchor == s.head);
     737           98 :         if collapsed && self.script.cursor_offset().is_some() {
     738           32 :             self.project_cursor(catalog, update, analyzed, &mut projector);
     739           66 :         }
     740          141 :     }
     741              : 
     742              :     /// Project the diagnostics, spans, annotations and statistics of the analysis
     743           96 :     fn project_analysis(&self, update: &mut EditorUpdateT, analyzed: &AnalyzedScript, projector: &mut Projector) {
     744           96 :         let parsed = &analyzed.parsed;
     745           96 :         let scanned = &parsed.scanned;
     746          112 :         let resolve = |span: SymbolSpan| resolve_text_span(&scanned.text, &scanned.symbols, span);
     747              : 
     748              :         // Diagnostics
     749           96 :         let mut diagnostics = Vec::new();
     750           96 :         let mut add_diagnostic = |projector: &mut Projector,
     751              :                                   source: EditorDiagnosticSource,
     752              :                                   severity: EditorDiagnosticSeverity,
     753              :                                   message: &str,
     754           15 :                                   span: TextSpan| {
     755           15 :             let Some(text_span) = projector.span(span) else {
     756            0 :                 return;
     757              :             };
     758           15 :             let mut diagnostic = EditorDiagnosticT::default();
     759           15 :             diagnostic.source = source;
     760           15 :             diagnostic.severity = severity;
     761           15 :             diagnostic.message = non_empty_string(message);
     762           15 :             diagnostic.text_span = Some(text_span);
     763           15 :             diagnostics.push(diagnostic);
     764           15 :         };
     765           96 :         for error in &scanned.errors {
     766            1 :             add_diagnostic(
     767            1 :                 projector,
     768            1 :                 EditorDiagnosticSource::SCANNER,
     769            1 :                 EditorDiagnosticSeverity::ERROR,
     770            1 :                 &error.message,
     771            1 :                 error.location,
     772            1 :             );
     773            1 :         }
     774           96 :         for error in &parsed.errors {
     775           13 :             add_diagnostic(
     776           13 :                 projector,
     777           13 :                 EditorDiagnosticSource::PARSER,
     778           13 :                 EditorDiagnosticSeverity::ERROR,
     779           13 :                 &error.message,
     780           13 :                 resolve(error.location),
     781           13 :             );
     782           13 :         }
     783           96 :         for error in &analyzed.errors {
     784            1 :             let Some(span) = error.text_span else {
     785            0 :                 continue;
     786              :             };
     787            1 :             let severity = if error.severity == AnalyzerErrorSeverity::WARNING {
     788            0 :                 EditorDiagnosticSeverity::WARNING
     789              :             } else {
     790            1 :                 EditorDiagnosticSeverity::ERROR
     791              :             };
     792            1 :             add_diagnostic(projector, EditorDiagnosticSource::ANALYZER, severity, &error.message, span);
     793              :         }
     794           96 :         diagnostics.sort_by_key(|d| d.text_span.as_ref().map_or(0, |s| s.offset));
     795           96 :         update.diagnostics = non_empty(diagnostics);
     796              : 
     797              :         // Syntax spans: the tokens and the comments
     798           96 :         let tokens = pack_tokens(parsed);
     799           96 :         let offsets = tokens.token_offsets.as_deref().unwrap_or(&[]);
     800           96 :         let lengths = tokens.token_lengths.as_deref().unwrap_or(&[]);
     801           96 :         let types = tokens.token_types.as_deref().unwrap_or(&[]);
     802           96 :         let mut syntax_spans = Vec::with_capacity(offsets.len() + scanned.comments.len());
     803          702 :         let mut add_syntax = |projector: &mut Projector, token_type: ScannerTokenType, span: TextSpan, in_comment: bool| {
     804          702 :             if let Some(text_span) = projector.span(span) {
     805          702 :                 let mut s = EditorSyntaxSpanT::default();
     806          702 :                 s.token_type = token_type;
     807          702 :                 s.text_span = text_span;
     808          702 :                 s.in_comment = in_comment;
     809          702 :                 syntax_spans.push(s);
     810          702 :             }
     811          702 :         };
     812          615 :         for ((&offset, &length), &token_type) in offsets.iter().zip(lengths).zip(types) {
     813          615 :             add_syntax(projector, token_type, TextSpan::new(offset, length), false);
     814          615 :         }
     815              :         // Comments with `@vis` tags are split into the tokens of the tags
     816           96 :         let (comment_spans, vegalite_specs) = dashql_core::vis_highlight::comment_syntax_spans(parsed);
     817           96 :         for (span, token_type) in comment_spans {
     818           87 :             add_syntax(projector, token_type, span, true);
     819           87 :         }
     820           96 :         syntax_spans.sort_by_key(|s| s.text_span.offset);
     821           96 :         update.syntax_spans = non_empty(syntax_spans);
     822           96 :         let vegalite_specs: Vec<_> = vegalite_specs.into_iter().filter_map(|span| projector.span(span)).collect();
     823           96 :         update.vegalite_specs = non_empty(vegalite_specs);
     824              :         // The tagged regions of the comments, editors style e.g. descriptions apart from other comments
     825           96 :         let mut comment_tags = Vec::new();
     826           96 :         for block in &parsed.comment_blocks.blocks {
     827           19 :             if block.block_type != CommentBlockType::DASHQL_TAG {
     828           10 :                 continue;
     829            9 :             }
     830            9 :             let lines = &parsed.comment_blocks.lines[block.lines_begin as usize..(block.lines_begin + block.lines_count) as usize];
     831            9 :             let mut tag = EditorCommentTagT::default();
     832            9 :             tag.name = Some(scanned.text[block.tag_span.offset as usize..block.tag_span.end() as usize].to_string());
     833            9 :             tag.text_span = projector.span(block.text_span);
     834           10 :             tag.lines = non_empty(lines.iter().filter_map(|&line| projector.span(line)).collect());
     835            9 :             comment_tags.push(tag);
     836              :         }
     837           96 :         update.comment_tags = non_empty(comment_tags);
     838              : 
     839              :         // Semantic spans: table, column and function references
     840           96 :         let mut semantic_spans = Vec::new();
     841           96 :         for (reference_id, reference) in analyzed.table_references.iter().enumerate() {
     842           50 :             let Some(location) = reference.location else {
     843            0 :                 continue;
     844              :             };
     845           50 :             let Some(text_span) = projector.span(resolve(location)) else {
     846            0 :                 continue;
     847              :             };
     848           50 :             let mut span = EditorSemanticSpanT::default();
     849           50 :             span.kind = EditorSemanticReferenceKind::TABLE;
     850           50 :             span.reference_id = reference_id as u32;
     851           50 :             span.text_span = text_span;
     852           50 :             if let Some(resolved) = resolved_table_ids(reference) {
     853           40 :                 span.resolution = EditorSemanticResolution::RESOLVED;
     854           40 :                 span.catalog_database_id = resolved.database_id;
     855           40 :                 span.catalog_schema_id = resolved.schema_id;
     856           40 :                 span.catalog_table_id = resolved.table_id;
     857           40 :                 span.referenced_catalog_version = resolved.catalog_version;
     858           40 :             }
     859           50 :             semantic_spans.push(span);
     860              :         }
     861           96 :         for (expression_id, expression) in analyzed.expressions.iter().enumerate() {
     862           89 :             let is_function = match &expression.inner {
     863           48 :                 ExpressionInner::ColumnRef(_) => false,
     864            1 :                 ExpressionInner::FunctionCall(_) => true,
     865           40 :                 _ => continue,
     866              :             };
     867           49 :             let Some(mut location) = expression.location else {
     868            0 :                 continue;
     869              :             };
     870           49 :             if is_function && (expression.ast_node_id as usize) < parsed.nodes.len() {
     871            1 :                 let node = &parsed.nodes[expression.ast_node_id as usize];
     872            1 :                 if node.children_count() > 0 {
     873            1 :                     let begin = node.children_begin_or_value() as usize;
     874            1 :                     let children = &parsed.nodes[begin..begin + node.children_count() as usize];
     875            1 :                     let [name] = attrs!(children, [AttributeKey::SQL_FUNCTION_NAME]);
     876            1 :                     if let Some(name) = name {
     877            1 :                         location = *name.symbol_span();
     878            1 :                     }
     879            0 :                 }
     880           48 :             }
     881           49 :             let Some(text_span) = projector.span(resolve(location)) else {
     882            0 :                 continue;
     883              :             };
     884           49 :             let mut span = EditorSemanticSpanT::default();
     885              :             span.kind =
     886           49 :                 if is_function { EditorSemanticReferenceKind::FUNCTION } else { EditorSemanticReferenceKind::COLUMN };
     887           49 :             span.reference_id = expression_id as u32;
     888           49 :             span.text_span = text_span;
     889           49 :             if let Some((table, column_id)) = resolved_column_ids(expression) {
     890           41 :                 span.resolution = EditorSemanticResolution::RESOLVED;
     891           41 :                 span.catalog_database_id = table.database_id;
     892           41 :                 span.catalog_schema_id = table.schema_id;
     893           41 :                 span.catalog_table_id = table.table_id;
     894           41 :                 span.catalog_column_id = column_id;
     895           41 :                 span.referenced_catalog_version = table.catalog_version;
     896           41 :             }
     897           49 :             semantic_spans.push(span);
     898              :         }
     899           96 :         semantic_spans.sort_by_key(|s| s.text_span.offset);
     900           96 :         update.semantic_spans = non_empty(semantic_spans);
     901              : 
     902              :         // Annotations
     903           96 :         let mut annotations = EditorScriptAnnotationsT::default();
     904           96 :         let mut table_definitions: Vec<EditorTableDefinitionT> = analyzed
     905           96 :             .entry
     906           96 :             .table_declarations
     907           96 :             .iter()
     908           96 :             .map(|table| {
     909           25 :                 let mut definition = EditorTableDefinitionT::default();
     910           25 :                 definition.name = non_empty_string(format_table_name(&table.table_name));
     911           25 :                 definition.statement_id = table.ast_statement_id.unwrap_or(u32::MAX);
     912           25 :                 definition
     913           25 :             })
     914           96 :             .collect();
     915           96 :         table_definitions.sort_by(|a, b| (a.name.as_deref(), a.statement_id).cmp(&(b.name.as_deref(), b.statement_id)));
     916           96 :         annotations.table_definitions = non_empty(table_definitions);
     917           96 :         let mut referenced: Vec<String> = analyzed
     918           96 :             .table_references
     919           96 :             .iter()
     920           96 :             .filter_map(|r| r.inner.as_ref().map(|rel| rel.table_name.table_name.text.to_string()))
     921           96 :             .collect();
     922           96 :         referenced.sort_unstable();
     923           96 :         referenced.dedup();
     924           96 :         annotations.referenced_table_names = non_empty(referenced);
     925           96 :         annotations.has_visualization_compilation = !analyzed.visualization_specs.is_empty();
     926           96 :         annotations.flow_position = analyzed.flow.map(|flow| {
     927            2 :             let mut position = EditorFlowPositionT::default();
     928            2 :             position.statement_id = flow.statement_id;
     929            2 :             position.x = flow.position.map(|(x, _)| x);
     930            2 :             position.y = flow.position.map(|(_, y)| y);
     931            2 :             position.width = flow.width;
     932            2 :             position.height = flow.height;
     933            2 :             Box::new(position)
     934            2 :         });
     935           96 :         annotations.manual = analyzed.manual;
     936           96 :         annotations.board_position = analyzed.board.as_ref().map(|board| {
     937            1 :             let mut position = EditorBoardPositionT::default();
     938            1 :             position.statement_id = board.statement_id;
     939            1 :             position.row = board.dashboard.row;
     940            1 :             position.column = board.dashboard.column;
     941            1 :             position.width = board.dashboard.width;
     942            1 :             position.height = board.dashboard.height;
     943            1 :             Box::new(position)
     944            1 :         });
     945           96 :         let mut statements = Vec::with_capacity(parsed.statements.len());
     946           96 :         let mut statement_descriptions = Vec::new();
     947          101 :         for (statement_id, (statement, metadata)) in
     948           96 :             parsed.statements.iter().zip(parsed.associate_descriptions()).enumerate()
     949              :         {
     950          101 :             let Some(text_span) = projector.span(metadata.statement_span) else {
     951            0 :                 continue;
     952              :             };
     953          101 :             let mut s = EditorStatementT::default();
     954          101 :             s.statement_id = statement_id as u32;
     955          101 :             s.statement_type = statement.statement_type;
     956          101 :             s.text_span = Some(text_span.clone());
     957          101 :             statements.push(s);
     958          101 :             if metadata.description_count == 0 {
     959           86 :                 continue;
     960           15 :             }
     961           15 :             let mut description = EditorStatementDescriptionT::default();
     962           15 :             description.statement_id = statement_id as u32;
     963           15 :             description.statement_type = statement.statement_type;
     964           15 :             description.text_span = Some(text_span);
     965           15 :             statement_descriptions.push(description);
     966              :         }
     967           96 :         annotations.statements = non_empty(statements);
     968           96 :         annotations.statement_descriptions = non_empty(statement_descriptions);
     969           96 :         update.script_annotations = Some(Box::new(annotations));
     970              : 
     971              :         // Statistics
     972           96 :         let timings = self.script.timings();
     973           96 :         let memory = self.script.memory_statistics();
     974           96 :         let latest = &memory.latest_script;
     975           96 :         let mut statistics = EditorProcessingStatisticsT::default();
     976           96 :         statistics.scanner_last_elapsed_ns = timings.scanner_ns;
     977           96 :         statistics.parser_last_elapsed_ns = timings.parser_ns;
     978           96 :         statistics.analyzer_last_elapsed_ns = timings.analyzer_ns;
     979           96 :         statistics.rope_bytes = memory.rope_bytes as u64;
     980           96 :         statistics.scanner_input_bytes = latest.scanner_input_bytes as u64;
     981           96 :         statistics.scanner_symbol_bytes = latest.scanner_symbol_bytes as u64;
     982           96 :         statistics.scanner_name_dictionary_bytes = latest.scanner_name_dictionary_bytes as u64;
     983           96 :         statistics.parser_ast_bytes = latest.parser_ast_bytes as u64;
     984           96 :         statistics.analyzer_description_bytes = latest.analyzer_description_bytes as u64;
     985           96 :         statistics.analyzer_name_index_size = latest.analyzer_name_index_size as u64;
     986           96 :         statistics.analyzer_name_index_bytes = latest.analyzer_name_index_bytes as u64;
     987           96 :         update.processing_statistics = Some(Box::new(statistics));
     988           96 :     }
     989              : 
     990              :     /// Project the state and the semantic context of the collapsed primary cursor
     991           32 :     fn project_cursor(&self, catalog: &Catalog, update: &mut EditorUpdateT, analyzed: &AnalyzedScript, projector: &mut Projector) {
     992           32 :         let Some(cursor) = self.script.cursor(catalog) else {
     993            0 :             return;
     994              :         };
     995           32 :         if let Some(text_offset) = projector.offset(cursor.text_offset as u64) {
     996           32 :             let mut state = EditorCursorStateT::default();
     997           32 :             state.text_offset = text_offset;
     998           32 :             match &cursor.scanner_location {
     999           32 :                 Some(location) => {
    1000           32 :                     state.scanner_relative_position = location.current.relative_pos;
    1001           32 :                     state.scanner_symbol_completable = is_symbol_kind_completable(location.current.symbol.kind);
    1002           32 :                 }
    1003            0 :                 None => {
    1004            0 :                     state.scanner_relative_position = RelativeSymbolPosition::AFTER_SYMBOL;
    1005            0 :                     state.scanner_symbol_completable = false;
    1006            0 :                 }
    1007              :             }
    1008           32 :             update.primary_cursor_state = Some(Box::new(state));
    1009            0 :         }
    1010              : 
    1011           32 :         let mut context = EditorCursorSemanticContextT::default();
    1012           32 :         let mut target: Option<(ResolvedTableIds, Option<u32>)> = None;
    1013           32 :         match cursor.context {
    1014            6 :             CursorContext::TableRef { table_reference_id, .. } => {
    1015            6 :                 context.kind = EditorCursorSemanticKind::TABLE_REFERENCE;
    1016            6 :                 context.reference_id = table_reference_id;
    1017            6 :                 if let Some(reference) = analyzed.table_references.get(table_reference_id as usize) {
    1018            6 :                     target = resolved_table_ids(reference).map(|t| (t, None));
    1019            0 :                 }
    1020              :             }
    1021           10 :             CursorContext::ColumnRef { expression_id } => {
    1022           10 :                 context.kind = EditorCursorSemanticKind::COLUMN_REFERENCE;
    1023           10 :                 context.reference_id = expression_id;
    1024           10 :                 if let Some(expression) = analyzed.expressions.get(expression_id as usize) {
    1025           10 :                     target = resolved_column_ids(expression).map(|(t, c)| (t, Some(c)));
    1026            0 :                 }
    1027              :             }
    1028              :             // The editor sees a column reference without an expression
    1029            1 :             CursorContext::AliasDot => {
    1030            1 :                 context.kind = EditorCursorSemanticKind::COLUMN_REFERENCE;
    1031            1 :                 context.reference_id = PROTO_NULL_U32;
    1032            1 :             }
    1033           15 :             CursorContext::None => {}
    1034              :         }
    1035           32 :         if let Some((table, column)) = target {
    1036           13 :             context.resolved = true;
    1037           13 :             context.catalog_database_id = table.database_id;
    1038           13 :             context.catalog_schema_id = table.schema_id;
    1039           13 :             context.catalog_table_id = table.table_id;
    1040           13 :             context.catalog_column_id = column.unwrap_or(0);
    1041           13 :             context.referenced_catalog_version = table.catalog_version;
    1042           13 :             if let Some((entry_id, location)) = declaration(analyzed, catalog, CatalogTableId::unpack(table.table_id), column) {
    1043           11 :                 context.declaration_catalog_entry_id = entry_id;
    1044           11 :                 context.declaration_location = location.map(|l| TextSpanT { offset: l.offset, length: l.length });
    1045            2 :             }
    1046           13 :             let related_tables: Vec<u32> = analyzed
    1047           13 :                 .table_references
    1048           13 :                 .iter()
    1049           13 :                 .enumerate()
    1050           16 :                 .filter(|(_, r)| resolved_table_ids(r).is_some_and(|t| t.same_table(&table)))
    1051           13 :                 .map(|(i, _)| i as u32)
    1052           13 :                 .collect();
    1053           13 :             let related_columns: Vec<u32> = analyzed
    1054           13 :                 .expressions
    1055           13 :                 .iter()
    1056           13 :                 .enumerate()
    1057           32 :                 .filter(|(_, e)| resolved_column_ids(e).is_some_and(|(t, _)| t.same_table(&table)))
    1058           21 :                 .map(|(i, _)| i as u32)
    1059           13 :                 .collect();
    1060           13 :             context.related_table_reference_ids = non_empty(related_tables);
    1061           13 :             context.related_column_reference_ids = non_empty(related_columns);
    1062           19 :         }
    1063           32 :         if context.kind != EditorCursorSemanticKind::NONE {
    1064           17 :             update.primary_cursor_context = Some(Box::new(context));
    1065           17 :         }
    1066           32 :     }
    1067              : 
    1068              :     /// Complete at the cursor. With UTF-16 offsets, the completion goes through the object API, so
    1069              :     /// empty strings and vectors are left out (see docs/design/core_quirks.md).
    1070            3 :     pub fn complete(&self, catalog: &Catalog, limit: usize, keyword_case: KeywordCase) -> Result<CompletionT> {
    1071            3 :         let cursor = self.script.cursor(catalog).ok_or_else(|| Error::status(StatusCode::COMPLETION_MISSES_CURSOR))?;
    1072            3 :         let completion = Script::complete_at_cursor(&cursor, limit)?;
    1073            3 :         let mut packed = pack_completion(&completion, keyword_case == KeywordCase::UPPER);
    1074            3 :         if self.offset_unit == EditorOffsetUnit::UTF8_BYTES {
    1075            2 :             return Ok(packed);
    1076            1 :         }
    1077            1 :         let mut projector = self.projector(None);
    1078            1 :         packed.cursor_offset = Self::project_byte_offset(&mut projector, packed.cursor_offset as u64)? as u32;
    1079           20 :         let project_symbol_span = |projector: &mut Projector, span: &mut Option<SymbolSpanT>| {
    1080           20 :             if let Some(s) = span.as_ref() {
    1081           20 :                 *span = projector.span(TextSpan::new(s.offset, s.length)).map(|p| {
    1082           20 :                     let mut out = SymbolSpanT::default();
    1083           20 :                     out.offset = p.offset as u32;
    1084           20 :                     out.length = p.length as u32;
    1085           20 :                     out
    1086           20 :                 });
    1087            0 :             }
    1088           20 :         };
    1089            1 :         let mut candidates = packed.candidates.take().unwrap_or_default();
    1090           10 :         for candidate in &mut candidates {
    1091           10 :             project_symbol_span(&mut projector, &mut candidate.target_location);
    1092           10 :             project_symbol_span(&mut projector, &mut candidate.target_location_qualified);
    1093           10 :             let completion_text = candidate.completion_text.as_deref().unwrap_or("");
    1094           10 :             let cursor = candidate.completion_cursor_offset as usize;
    1095           10 :             if cursor > completion_text.len() || !completion_text.is_char_boundary(cursor) {
    1096            0 :                 return Err(Error::invalid_argument("completion cursor is not on a UTF-8 codepoint boundary"));
    1097           10 :             }
    1098           10 :             candidate.completion_cursor_offset = completion_text[..cursor].encode_utf16().count() as u32;
    1099              :             // The object API round trip
    1100           10 :             candidate.display_text = candidate.display_text.take().filter(|s| !s.is_empty());
    1101           10 :             candidate.completion_text = candidate.completion_text.take().filter(|s| !s.is_empty());
    1102           10 :             candidate.keyword_continuation = candidate.keyword_continuation.take().filter(|s| !s.is_empty());
    1103           10 :             let mut objects = candidate.catalog_objects.take().unwrap_or_default();
    1104           10 :             for object in &mut objects {
    1105            0 :                 object.qualified_name = object.qualified_name.take().filter(|n| !n.is_empty());
    1106              :             }
    1107           10 :             candidate.catalog_objects = non_empty(objects);
    1108              :         }
    1109            1 :         packed.candidates = non_empty(candidates);
    1110            1 :         Ok(packed)
    1111            3 :     }
    1112              : 
    1113              :     /// Compile the current text into executable statements
    1114            1 :     pub fn compile_query(
    1115            1 :         &mut self,
    1116            1 :         catalog: &Catalog,
    1117            1 :         config: &FormattingConfigT,
    1118            1 :         allow_extensions: bool,
    1119            1 :         parse_if_outdated: bool,
    1120            1 :     ) -> ScriptCompilationResultT {
    1121            1 :         compile(&mut self.script, catalog, config, CompilationOptions { allow_extensions, parse_if_outdated })
    1122            1 :     }
    1123              : 
    1124              :     /// Format the script into a new script with an entry id of `catalog`. The formatter can cut a
    1125              :     /// UTF-8 character when it wraps comments; such output is rejected after the entry id was allocated
    1126              :     /// (see docs/design/core_quirks.md).
    1127            1 :     pub fn format(&mut self, catalog: &Catalog, config: &FormattingConfigT, parse_if_outdated: bool) -> Result<Script> {
    1128            1 :         let text = self.script.format_bytes(config, parse_if_outdated)?;
    1129            1 :         let mut result = Script::new(catalog);
    1130            1 :         let text = String::from_utf8(text)
    1131            1 :             .map_err(|_| Error::invalid_argument("invalid UTF-8 while computing rope text statistics"))?;
    1132            1 :         result.replace_text(&text);
    1133            1 :         Ok(result)
    1134            1 :     }
    1135              : 
    1136              :     /// The annotation of a statement: the comment tags in front of it
    1137            2 :     pub fn read_annotation(&mut self, statement_id: usize, parse_if_outdated: bool) -> Result<Annotation> {
    1138            2 :         self.script.read_annotation(statement_id, parse_if_outdated)
    1139            2 :     }
    1140              : 
    1141              :     /// Replace the annotation of a statement, returns the new text
    1142            1 :     pub fn rewrite_annotation(&mut self, statement_id: usize, annotation: &Annotation, parse_if_outdated: bool) -> Result<String> {
    1143            1 :         self.script.rewrite_annotation(statement_id, annotation, parse_if_outdated)
    1144            1 :     }
    1145              : 
    1146              :     /// Set or remove the renderer of a query, its `@vis` renderer tag, returns the new text
    1147            1 :     pub fn rewrite_vis_renderer(
    1148            1 :         &mut self,
    1149            1 :         statement_id: usize,
    1150            1 :         renderer: Option<&VisRenderer>,
    1151            1 :         parse_if_outdated: bool,
    1152            1 :     ) -> Result<String> {
    1153            1 :         self.script.rewrite_vis_renderer(statement_id, renderer, parse_if_outdated)
    1154            1 :     }
    1155              : 
    1156              :     /// Set or remove the dashboard placement of a query, its `@board` tag, returns the new text
    1157            3 :     pub fn rewrite_vis_board(&mut self, statement_id: usize, board: Option<&VisBoard>, parse_if_outdated: bool) -> Result<String> {
    1158            3 :         self.script.rewrite_vis_board(statement_id, board, parse_if_outdated)
    1159            3 :     }
    1160              : 
    1161              :     /// Set or remove the position of the script on the dataflow canvas, its `@flow` tag, returns the new
    1162              :     /// text
    1163            1 :     pub fn rewrite_vis_flow(&mut self, flow: Option<&VisFlow>, parse_if_outdated: bool) -> Result<String> {
    1164            1 :         self.script.rewrite_vis_flow(flow, parse_if_outdated)
    1165            1 :     }
    1166              : 
    1167              :     /// Set or remove the value of an input of the script, its `@param` tag, returns the new text
    1168            1 :     pub fn rewrite_param_tag(&mut self, name: &str, value: Option<&str>, parse_if_outdated: bool) -> Result<String> {
    1169            1 :         self.script.rewrite_param_tag(name, value, parse_if_outdated)
    1170            1 :     }
    1171              : 
    1172              :     /// Can formatting represent every node?
    1173            1 :     pub fn is_fully_formattable(&mut self, config: &FormattingConfigT, parse_if_outdated: bool) -> Result<bool> {
    1174            1 :         self.script.is_fully_formattable(config, parse_if_outdated)
    1175            1 :     }
    1176              : 
    1177              :     /// The statement diff from the session script to a target script. The source spans are projected
    1178              :     /// into the current text even if the parsed script is outdated (see docs/design/core_quirks.md).
    1179            6 :     pub fn diff(&self, target: &Script) -> Result<ScriptDiffT> {
    1180            6 :         let (Some(source_parsed), Some(target_parsed)) = (self.script.parsed.as_ref(), target.parsed.as_ref()) else {
    1181            2 :             return Err(Error::status(StatusCode::SCRIPT_NOT_PARSED));
    1182              :         };
    1183            4 :         let mut diff = ScriptDiff::new(source_parsed, target_parsed).pack();
    1184            4 :         if self.offset_unit == EditorOffsetUnit::UTF8_BYTES {
    1185            2 :             return Ok(diff);
    1186            2 :         }
    1187            2 :         let mut source = self.projector(None);
    1188            2 :         let mut target_projector = Projector::Utf16(Utf16Projector::new(None, target.text()));
    1189            2 :         let mut ops = diff.ops.take().unwrap_or_default();
    1190            3 :         for op in &mut ops {
    1191            3 :             if let Some(span) = op.source_span.as_mut() {
    1192            2 :                 let begin = Self::project_byte_offset(&mut source, span.offset as u64)?;
    1193            2 :                 let end = Self::project_byte_offset(&mut source, span.offset as u64 + span.length as u64)?;
    1194            2 :                 span.offset = begin as u32;
    1195            2 :                 span.length = end.wrapping_sub(begin) as u32;
    1196            1 :             }
    1197            4 :             let mut project_target = |span: &mut TextSpanT, message: &str| -> Result<()> {
    1198            4 :                 let begin = target_projector.offset(span.offset as u64);
    1199            4 :                 let end = target_projector.offset(span.offset as u64 + span.length as u64);
    1200            4 :                 let (Some(begin), Some(end)) = (begin, end) else {
    1201            0 :                     return Err(Error::invalid_argument(message));
    1202              :                 };
    1203            4 :                 span.offset = begin as u32;
    1204            4 :                 span.length = end.wrapping_sub(begin) as u32;
    1205            4 :                 Ok(())
    1206            4 :             };
    1207            3 :             if let Some(span) = op.target_span.as_mut() {
    1208            3 :                 project_target(span, "diff span is not on a UTF-8 codepoint boundary")?;
    1209            0 :             }
    1210            3 :             for change in op.target_changes.iter_mut().flatten() {
    1211            1 :                 project_target(change, "diff change is not on a UTF-8 codepoint boundary")?;
    1212              :             }
    1213              :         }
    1214            2 :         diff.ops = non_empty(ops);
    1215            2 :         Ok(diff)
    1216            6 :     }
    1217              : 
    1218              :     /// Publish the analyzed script into the catalog
    1219           15 :     pub fn load_into_catalog(&mut self, catalog: &mut Catalog, layer: CatalogLayer) -> Result<()> {
    1220           15 :         debug_assert_eq!(catalog.id(), self.catalog_id, "a session takes the catalog it was created with");
    1221           15 :         let analyzed =
    1222           15 :             self.script.analyzed.clone().ok_or_else(|| Error::status(StatusCode::CATALOG_SCRIPT_NOT_ANALYZED))?;
    1223              :         // A publish that changes declarations bumps the catalog version, the analysis then needs a
    1224              :         // refresh against that version before the planner can use it
    1225           15 :         catalog.load_script(analyzed, layer)?;
    1226           15 :         self.cursor_catalog_revision = catalog.version() as u64;
    1227           15 :         Ok(())
    1228           15 :     }
    1229              : 
    1230              :     /// Remove the script from the catalog
    1231           64 :     pub fn drop_from_catalog(&self, catalog: &mut Catalog) {
    1232           64 :         debug_assert_eq!(catalog.id(), self.catalog_id, "a session takes the catalog it was created with");
    1233           64 :         catalog.drop_script(self.catalog_entry_id());
    1234           64 :     }
    1235              : }
        

Generated by: LCOV version 2.0-1