LCOV - code coverage report
Current view: top level - dashql-app/src/session/editor - dashql_completion_list.ts Coverage Total Hit
Test: DashQL d805a4b Lines: 100.0 % 94 94
Test Date: 2026-11-09 20:22:13

            Line data    Source code
       1              : import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
       2              : import { EditorState } from '@codemirror/state';
       3              : 
       4              : import { DashQLCompletionNextCandidateVariantEffect, DashQLCompletionPreviousCandidateVariantEffect, DashQLCompletionState, DashQLCompletionStatus, DashQLProcessorPlugin } from './dashql_processor.js';
       5              : import { CandidateListRenderer, collectCandidates, placeBelowOrAbove, type Position } from '../../views/completion_list/completion_list_view.js';
       6              : 
       7              : 
       8              : // This file contains a CodeMirror plugin for showing the completion list of completion_list_view.ts.
       9              : 
      10              : 
      11            1 : class CompletionList {
      12              :     /// The list renderer
      13            1 :     list: CandidateListRenderer;
      14              :     /// The dom that this container is mounted to (if any)
      15            1 :     dom: HTMLElement | null = null;
      16              :     /// The rendered completion
      17            1 :     renderedCompletion: DashQLCompletionState | null = null;
      18              : 
      19            1 :     constructor(view: EditorView) {
      20            1 :         this.list = new CandidateListRenderer(
      21            1 :             () => view.dispatch({ effects: DashQLCompletionPreviousCandidateVariantEffect.of(null) }),
      22            1 :             () => view.dispatch({ effects: DashQLCompletionNextCandidateVariantEffect.of(null) }),
      23            1 :         );
      24            1 :     }
      25              :     /// Destroy the container
      26            1 :     destroy() {
      27            1 :         this.unmount();
      28            1 :         this.list.destroy();
      29            1 :     }
      30              :     /// Unmount a completion list container
      31            1 :     unmount() {
      32            1 :         if (this.dom) {
      33            1 :             this.list.rootElement.remove();
      34            1 :             this.dom = null;
      35            1 :         }
      36            1 :     }
      37              :     /// Mount a completion list container
      38            1 :     mount(dom: HTMLElement) {
      39            1 :         if (this.dom == dom) {
      40            1 :             return;
      41              :         }
      42            1 :         this.unmount();
      43            1 :         document.body.appendChild(this.list.rootElement);
      44            1 :         this.dom = dom;
      45            1 :     }
      46              :     /// Helper to compute a position
      47            1 :     static computePosition(view: EditorView, offset: number): (Position | null) {
      48            1 :         const candidateCoords = view.coordsAtPos(offset);
      49            1 :         if (candidateCoords == null) return null;
      50            1 :         return placeBelowOrAbove(candidateCoords);
      51            1 :     }
      52              : 
      53              :     /// Update the completion list
      54            1 :     update(view: EditorView, state: EditorState) {
      55            1 :         const processor = state.field(DashQLProcessorPlugin);
      56              : 
      57              :         // Short-circuit noops
      58            1 :         if (this.renderedCompletion === processor.scriptCompletion) {
      59            1 :             return;
      60              :         }
      61            1 :         this.renderedCompletion = processor.scriptCompletion;
      62              : 
      63              :         // Hide completion?
      64            1 :         if (processor.scriptCompletion?.status !== DashQLCompletionStatus.AVAILABLE
      65            1 :             || processor.scriptCompletion?.passiveHint) {
      66            1 :             if (!this.list.isHidden) {
      67            1 :                 view.requestMeasure<(Position | null)>({
      68            1 :                     key: this,
      69            1 :                     read: (_view) => null,
      70            1 :                     write: (_null, _view) => {
      71            1 :                         this.list.hide();
      72            1 :                     }
      73            1 :                 });
      74              :             }
      75            1 :             return;
      76            1 :         }
      77            1 :         const selectedCandidate = processor.scriptCompletion.candidateId;
      78            1 :         const selectedCatalogObject = processor.scriptCompletion.catalogObjectId ?? null;
      79              : 
      80              :         // Invalid candidate?
      81            1 :         const completion = processor.scriptCompletion;
      82            1 :         const completionBuffer = completion.buffer.read();
      83            1 :         if (completion.candidateId >= completionBuffer.candidatesLength()) {
      84            1 :             return;
      85              :         }
      86              :         // Current candidate
      87            1 :         const candidate = completionBuffer.candidates(completion.candidateId);
      88            1 :         if (candidate == null) {
      89            1 :             return;
      90              :         }
      91            1 :         const candidateLoc = candidate.targetLocation()!;
      92            1 :         const candidateLocOffset = candidateLoc.offset();
      93              : 
      94              :         // Collect all candidates
      95            1 :         const pending = collectCandidates(completionBuffer, selectedCandidate, selectedCatalogObject);
      96              : 
      97              :         // Update the container position.
      98              :         // Keyed so that a later update (e.g. hiding) supersedes a pending measure.
      99              :         // The offset can still outlive the text it points into, the list is hidden then.
     100            1 :         view.requestMeasure<(Position | null)>({
     101            1 :             key: this,
     102            1 :             read: (view) => {
     103            1 :                 if (candidateLocOffset > view.state.doc.length) return null;
     104            1 :                 return CompletionList.computePosition(view, candidateLocOffset);
     105            1 :             },
     106            1 :             write: (pos: Position | null, _view) => {
     107            1 :                 if (pos == null) {
     108            1 :                     this.list.hide();
     109            1 :                     return;
     110            1 :                 }
     111            1 :                 this.list.show();
     112            1 :                 this.list.updateCandidates(pending);
     113            1 :                 this.list.updatePosition(this.list.fitIntoWindow(pos));
     114            1 :             }
     115            1 :         });
     116            1 :     }
     117            1 : }
     118              : 
     119            1 : export const DashQLCompletionListPlugin = ViewPlugin.fromClass(
     120            1 :     class {
     121            1 :         container: CompletionList;
     122              : 
     123            1 :         constructor(view: EditorView) {
     124            1 :             this.container = new CompletionList(view);
     125            1 :         }
     126            1 :         update(update: ViewUpdate) {
     127            1 :             this.container.mount(update.view.dom);
     128            1 :             this.container.update(update.view, update.state);
     129            1 :         }
     130            1 :         destroy() {
     131            1 :             this.container.destroy();
     132            1 :         }
     133            1 :     }
     134            1 : );
        

Generated by: LCOV version 2.0-1