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 : }
|