LCOV - code coverage report
Current view: top level - dashql-app/src/session/editor - dashql_completion_list.ts Coverage Total Hit
Test: DashQL 915828b Lines: 93.8 % 389 365
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as dashql from '@ankoh/dashql-core';
       2              : 
       3              : import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
       4              : import { EditorState } from '@codemirror/state';
       5              : 
       6              : import { DashQLCompletionNextCandidateVariantEffect, DashQLCompletionPreviousCandidateVariantEffect, DashQLCompletionState, DashQLCompletionStatus, DashQLProcessorPlugin } from './dashql_processor.js';
       7              : import { CompletionCandidateType, getCandidateTypeSymbolColor, getCandidateTypeSymbolText } from './dashql_completion_candidate_type.js';
       8              : 
       9              : import * as styles from './dashql_completion_list.module.css';
      10              : import icons from '@ankoh/dashql-svg-symbols';
      11              : 
      12              : 
      13              : // This file contains a CodeMirror plugin for rendering a completion list.
      14              : // The rendering itself is virtualized but deliberately does NOT use React.
      15              : // We're implementing the view consolidation manually here.
      16              : //
      17              : // This makes the extension independent and allows separating it as library later.
      18              : 
      19              : 
      20              : interface Position {
      21              :     /// The top offset (used when rendering below the cursor)
      22              :     top: number | null;
      23              :     /// The bottom offset (used when rendering above the cursor)
      24              :     bottom: number | null;
      25              :     /// The left offset
      26              :     left: number;
      27              : }
      28              : 
      29              : interface VirtualCandidate {
      30              :     /// The candidate type.
      31              :     /// Either the object type of the selected object or the first one.
      32              :     candidateType: CompletionCandidateType | null;
      33              :     /// The candidate text
      34              :     candidateLabel: string;
      35              :     /// Is selected?
      36              :     isSelected: boolean;
      37              :     /// The total catalog objects
      38              :     totalObjectCount: number;
      39              :     /// The selected catalog object
      40              :     selectedCatalogObject: number | null;
      41              : }
      42              : 
      43            1 : class CandidateRenderer {
      44              :     /// The currently rendered candidate
      45            1 :     rendered: VirtualCandidate | null;
      46              : 
      47              :     /// Info element visible?
      48            1 :     infoVisible: boolean;
      49              :     /// Selected object count visible?
      50            1 :     objectContainerVisible: boolean;
      51              :     /// Selected object index visible?
      52            1 :     objectSelectionVisible: boolean;
      53            1 :     navVisible: boolean;
      54              : 
      55              :     /// The entry element
      56            1 :     public readonly rootElement: HTMLDivElement;
      57              :     /// The icon element
      58            1 :     readonly iconElement: HTMLSpanElement;
      59              :     /// The name element
      60            1 :     readonly nameElement: HTMLSpanElement;
      61              :     /// The info element
      62            1 :     readonly infoElement: HTMLDivElement;
      63              : 
      64              :     /// The nav container element
      65            1 :     readonly navContainerElement: HTMLDivElement;
      66              :     /// The left arrow
      67            1 :     readonly navArrowLeftElement: SVGElement;
      68              :     /// The right arrow
      69            1 :     readonly navArrowRightElement: SVGElement;
      70              :     /// The container for the object count
      71            1 :     readonly objectContainerElement: HTMLDivElement;
      72              :     /// The span for the selected catalog object
      73            1 :     readonly objectSelectedSpan: HTMLSpanElement;
      74              :     /// The span for the catalog object count
      75            1 :     readonly objectTotalSpan: HTMLSpanElement;
      76              : 
      77            1 :     constructor(candidate: VirtualCandidate, selectPreviousVariant: () => void, selectNextVariant: () => void) {
      78            1 :         this.rendered = null;
      79            1 :         this.rootElement = document.createElement('div');
      80            1 :         this.iconElement = document.createElement('span');
      81            1 :         this.nameElement = document.createElement('span');
      82            1 :         this.infoElement = document.createElement('div');
      83            1 :         this.infoVisible = true;
      84            1 :         this.objectContainerVisible = true;
      85            1 :         this.objectSelectionVisible = true;
      86            1 :         this.navVisible = true;
      87              : 
      88            1 :         this.navContainerElement = document.createElement('div');
      89            1 :         this.navArrowLeftElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
      90            1 :         this.navArrowRightElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
      91              : 
      92            1 :         const navArrowLeftUse = document.createElementNS('http://www.w3.org/2000/svg', 'use');
      93            1 :         this.navArrowLeftElement.appendChild(navArrowLeftUse);
      94            1 :         this.navArrowLeftElement.setAttribute('width', '13px');
      95            1 :         this.navArrowLeftElement.setAttribute('height', '13px');
      96            1 :         navArrowLeftUse.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', `${icons}#arrow_left_16`);
      97              : 
      98            1 :         const navArrowRightUse = document.createElementNS('http://www.w3.org/2000/svg', 'use');
      99            1 :         this.navArrowRightElement.appendChild(navArrowRightUse);
     100            1 :         this.navArrowRightElement.setAttribute('width', '13px');
     101            1 :         this.navArrowRightElement.setAttribute('height', '13px');
     102            1 :         navArrowRightUse.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', `${icons}#arrow_right_16`);
     103            1 :         this.navArrowLeftElement.setAttribute('role', 'button');
     104            1 :         this.navArrowLeftElement.setAttribute('aria-label', 'Previous catalog object');
     105            1 :         this.navArrowRightElement.setAttribute('role', 'button');
     106            1 :         this.navArrowRightElement.setAttribute('aria-label', 'Next catalog object');
     107            1 :         this.navArrowLeftElement.addEventListener('mousedown', event => {
     108            1 :             event.preventDefault();
     109            1 :             selectPreviousVariant();
     110            1 :         });
     111            1 :         this.navArrowRightElement.addEventListener('mousedown', event => {
     112            1 :             event.preventDefault();
     113            1 :             selectNextVariant();
     114            1 :         });
     115              : 
     116            1 :         this.objectContainerElement = document.createElement('div');
     117            1 :         this.objectSelectedSpan = document.createElement('span');
     118            1 :         this.objectSelectedSpan.classList.add(styles.info_selected_count);
     119            1 :         this.objectTotalSpan = document.createElement('span');
     120              : 
     121            1 :         const objectLogoSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
     122            1 :         const objectLogoUse = document.createElementNS('http://www.w3.org/2000/svg', 'use');
     123            1 :         objectLogoSVG.setAttribute('width', '12px');
     124            1 :         objectLogoSVG.setAttribute('height', '12px');
     125            1 :         objectLogoUse.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', `${icons}#versions_16`);
     126            1 :         objectLogoSVG.appendChild(objectLogoUse);
     127              : 
     128              :         // Set up containers
     129            1 :         this.rootElement.classList.add(styles.candidate_container);
     130            1 :         this.iconElement.classList.add(styles.candidate_icon);
     131            1 :         this.nameElement.classList.add(styles.candidate_name);
     132            1 :         this.infoElement.classList.add(styles.info_container);
     133            1 :         this.navContainerElement.classList.add(styles.info_nav_container);
     134            1 :         this.navArrowLeftElement.classList.add(styles.info_nav_left);
     135            1 :         this.navArrowRightElement.classList.add(styles.info_nav_right);
     136            1 :         this.objectContainerElement.classList.add(styles.info_object_container);
     137              : 
     138            1 :         this.iconElement.textContent = getCandidateTypeSymbolText(candidate.candidateType ?? 0);
     139            1 :         this.iconElement.style.backgroundColor = getCandidateTypeSymbolColor(candidate.candidateType ?? 0);
     140              : 
     141              :         // Wire containers
     142            1 :         this.navContainerElement.appendChild(this.navArrowLeftElement);
     143            1 :         this.navContainerElement.appendChild(this.navArrowRightElement);
     144            1 :         this.objectContainerElement.appendChild(objectLogoSVG);
     145            1 :         this.objectContainerElement.appendChild(this.objectSelectedSpan);
     146            1 :         this.objectContainerElement.appendChild(this.objectTotalSpan);
     147            1 :         this.infoElement.appendChild(this.navContainerElement);
     148            1 :         this.infoElement.appendChild(this.objectContainerElement);
     149            1 :         this.rootElement.appendChild(this.iconElement);
     150            1 :         this.rootElement.appendChild(this.nameElement);
     151            1 :         this.rootElement.appendChild(this.infoElement);
     152              : 
     153            1 :         this.render(candidate);
     154            1 :     }
     155              : 
     156              :     // Destroy the node
     157            0 :     public destroy() {
     158            0 :         this.rootElement.remove();
     159            0 :     }
     160              : 
     161              :     /// Helper to hide the candidate info (if not already hidden)
     162            1 :     protected hideInfoContainer() {
     163            1 :         if (this.infoVisible) {
     164            1 :             this.infoElement.classList.add(styles.hidden);
     165            1 :             this.infoVisible = false;
     166            1 :         }
     167            1 :     }
     168              :     /// Helper to hide the object selection (if not already hidden)
     169            1 :     protected hideSelectedObject() {
     170            1 :         if (this.objectSelectionVisible) {
     171            1 :             this.objectSelectedSpan.classList.add(styles.hidden);
     172            1 :             this.objectSelectionVisible = false;
     173            1 :         }
     174            1 :     }
     175              :     /// Helper to hide the object container (if not already hidden)
     176            1 :     protected hideObjectContainer() {
     177            1 :         if (this.objectContainerVisible) {
     178            1 :             this.objectContainerElement.classList.add(styles.hidden);
     179            1 :             this.objectContainerVisible = false;
     180            1 :         }
     181            1 :     }
     182              :     /// Helper to show the candidate info (if not already hidden)
     183            1 :     protected showInfoContainer() {
     184            1 :         if (!this.infoVisible) {
     185            0 :             this.infoElement.classList.remove(styles.hidden);
     186            0 :             this.infoVisible = true;
     187            0 :         }
     188            1 :     }
     189              :     /// Helper to show the object selection (if not already hidden)
     190            1 :     protected showSelectedObject() {
     191            1 :         if (!this.objectSelectionVisible) {
     192            0 :             this.objectSelectedSpan.classList.remove(styles.hidden);
     193            0 :             this.objectSelectionVisible = true;
     194            0 :         }
     195            1 :     }
     196              :     /// Helper to show the object container (if not already hidden)
     197            1 :     protected showObjectContainer() {
     198            1 :         if (!this.objectContainerVisible) {
     199            1 :             this.objectContainerElement.classList.remove(styles.hidden);
     200            1 :             this.objectContainerVisible = true;
     201            1 :         }
     202            1 :     }
     203            1 :     protected hideNavigation() {
     204            1 :         if (!this.navVisible) return;
     205            1 :         this.navContainerElement.classList.add(styles.hidden);
     206            1 :         this.navVisible = false;
     207            1 :     }
     208            1 :     protected showNavigation() {
     209            1 :         if (this.navVisible) return;
     210            1 :         this.navContainerElement.classList.remove(styles.hidden);
     211            1 :         this.navVisible = true;
     212            1 :     }
     213            1 :     public render(candidate: VirtualCandidate) {
     214              :         // Is the element selected?
     215            1 :         if (candidate.isSelected != this.rendered?.isSelected) {
     216            1 :             if (candidate.isSelected) {
     217            1 :                 this.rootElement.classList.add(styles.selected);
     218              :             } else {
     219            1 :                 this.rootElement.classList.remove(styles.selected);
     220              :             }
     221            1 :         }
     222              :         // Does the label differ?
     223            1 :         if (candidate.candidateLabel != this.rendered?.candidateLabel) {
     224            1 :             this.nameElement.textContent = candidate.candidateLabel;
     225              :         }
     226              :         // Does the object type differ?
     227            1 :         if (candidate.candidateType != this.rendered?.candidateType) {
     228            1 :             this.iconElement.textContent = getCandidateTypeSymbolText(candidate.candidateType ?? 0);
     229            1 :             this.iconElement.style.backgroundColor = getCandidateTypeSymbolColor(candidate.candidateType ?? 0);
     230            1 :         }
     231              :         // Is selected and has selectable?
     232            1 :         const anySelectable = candidate.totalObjectCount > 0;
     233            1 :         if (candidate.isSelected && anySelectable) {
     234            1 :             this.showInfoContainer();
     235              :         } else {
     236            1 :             this.hideInfoContainer();
     237              :         }
     238              :         // Update selected object?
     239            1 :         if (candidate.selectedCatalogObject == null) {
     240            1 :             this.hideSelectedObject();
     241            1 :         } else if (candidate.selectedCatalogObject != this.rendered?.selectedCatalogObject) {
     242            1 :             this.showSelectedObject();
     243            1 :             this.objectSelectedSpan.textContent = (candidate.selectedCatalogObject + 1).toString();
     244            1 :         }
     245              :         // Update the total object count
     246            1 :         if (candidate.totalObjectCount != this.rendered?.totalObjectCount) {
     247            1 :             this.objectTotalSpan.textContent = candidate.totalObjectCount.toString();
     248              :         }
     249              :         // Check object totals
     250            1 :         if (candidate.totalObjectCount > 0) {
     251            1 :             this.showObjectContainer();
     252              :         } else {
     253            1 :             this.hideObjectContainer();
     254              :         }
     255            1 :         if (candidate.totalObjectCount > 1) {
     256            1 :             this.showNavigation();
     257              :         } else {
     258            1 :             this.hideNavigation();
     259              :         }
     260            1 :         this.rendered = candidate;
     261            1 :     }
     262            1 : }
     263              : 
     264            1 : class CandidateListRenderer {
     265              :     /// Is the list shown?
     266            1 :     rootVisible: boolean;
     267              :     /// The list position
     268            1 :     rootPosition: Position;
     269              : 
     270              :     /// The overlay container
     271            1 :     public readonly rootElement: HTMLDivElement;
     272              :     /// The list container
     273            1 :     readonly listContainer: HTMLDivElement;
     274              :     /// The list entries
     275            1 :     readonly renderedCandidates: CandidateRenderer[];
     276            1 :     readonly selectPreviousVariant: () => void;
     277            1 :     readonly selectNextVariant: () => void;
     278              : 
     279            1 :     constructor(selectPreviousVariant: () => void, selectNextVariant: () => void) {
     280            1 :         this.rootVisible = true;
     281            1 :         this.rootPosition = { top: null, bottom: null, left: -1 };
     282            1 :         this.selectPreviousVariant = selectPreviousVariant;
     283            1 :         this.selectNextVariant = selectNextVariant;
     284              : 
     285            1 :         this.rootElement = document.createElement('div');
     286            1 :         this.rootElement.className = styles.overlay_container;
     287            1 :         this.rootElement.dataset.dashqlCompletionList = 'true';
     288            1 :         this.listContainer = document.createElement('div');
     289            1 :         this.listContainer.className = styles.list_container;
     290            1 :         this.rootElement.appendChild(this.listContainer);
     291              : 
     292            1 :         this.renderedCandidates = [];
     293              : 
     294            1 :         this.hide();
     295            1 :     }
     296              : 
     297            1 :     public destroy() {
     298            1 :         this.rootElement.remove();
     299            1 :     }
     300              : 
     301              :     /// Is hidden?
     302            1 :     public get isHidden() { return !this.rootVisible; }
     303              :     /// Hide the list (if shown)
     304            1 :     public hide() {
     305            1 :         if (this.rootVisible) {
     306            1 :             this.rootElement.classList.add(styles.hidden);
     307            1 :             this.rootVisible = false;
     308            1 :         }
     309            1 :     }
     310              :     /// Show the list (if hidden)
     311            1 :     public show() {
     312            1 :         if (!this.rootVisible) {
     313            1 :             this.rootElement.classList.remove(styles.hidden);
     314            1 :             this.rootVisible = true;
     315            1 :         }
     316            1 :     }
     317              :     /// Update the position
     318            1 :     public updatePosition(position: Position) {
     319            1 :         if (this.rootPosition.top !== position.top || this.rootPosition.bottom !== position.bottom || this.rootPosition.left !== position.left) {
     320            1 :             if (position.bottom !== null) {
     321            0 :                 this.rootElement.style.top = '';
     322            0 :                 this.rootElement.style.bottom = `${position.bottom}px`;
     323            1 :             } else {
     324            1 :                 this.rootElement.style.bottom = '';
     325            1 :                 this.rootElement.style.top = `${position.top ?? 0}px`;
     326            1 :             }
     327            1 :             this.rootElement.style.left = `${position.left}px`;
     328            1 :             this.rootPosition = position;
     329            1 :         }
     330            1 :     }
     331              :     /// Update the candidates
     332            1 :     public updateCandidates(candidates: VirtualCandidate[]) {
     333              :         // Reuse rendered candidates
     334            1 :         const n = Math.min(candidates.length, this.renderedCandidates.length);
     335            1 :         for (let i = 0; i < n; ++i) {
     336            1 :             const rendered = this.renderedCandidates[i];
     337            1 :             rendered.render(candidates[i]);
     338              :         }
     339              :         // Delete excess rendered
     340            1 :         const dead = this.renderedCandidates.splice(n, this.renderedCandidates.length - n);
     341            1 :         for (let i = 0; i < dead.length; ++i) {
     342            0 :             this.listContainer.removeChild(dead[i].rootElement);
     343            0 :             dead[i].destroy();
     344            0 :         }
     345              :         // Create new rendered
     346            1 :         for (let i = n; i < candidates.length; ++i) {
     347            1 :             const entry = new CandidateRenderer(candidates[i], this.selectPreviousVariant, this.selectNextVariant);
     348            1 :             this.listContainer.appendChild(entry.rootElement);
     349            1 :             this.renderedCandidates.push(entry);
     350            1 :         }
     351            1 :     }
     352            1 : }
     353              : 
     354            1 : class CompletionList {
     355              :     /// The list renderer
     356            1 :     list: CandidateListRenderer;
     357              :     /// The dom that this container is mounted to (if any)
     358            1 :     dom: HTMLElement | null = null;
     359              :     /// The rendered completion
     360            1 :     renderedCompletion: DashQLCompletionState | null = null;
     361              : 
     362            1 :     constructor(view: EditorView) {
     363            1 :         this.list = new CandidateListRenderer(
     364            1 :             () => view.dispatch({ effects: DashQLCompletionPreviousCandidateVariantEffect.of(null) }),
     365            1 :             () => view.dispatch({ effects: DashQLCompletionNextCandidateVariantEffect.of(null) }),
     366            1 :         );
     367            1 :     }
     368              :     /// Destroy the container
     369            1 :     destroy() {
     370            1 :         this.unmount();
     371            1 :         this.list.destroy();
     372            1 :     }
     373              :     /// Unmount a completion list container
     374            1 :     unmount() {
     375            1 :         if (this.dom) {
     376            1 :             this.list.rootElement.remove();
     377            1 :             this.dom = null;
     378            1 :         }
     379            1 :     }
     380              :     /// Mount a completion list container
     381            1 :     mount(dom: HTMLElement) {
     382            1 :         if (this.dom == dom) {
     383            1 :             return;
     384              :         }
     385            1 :         this.unmount();
     386            1 :         document.body.appendChild(this.list.rootElement);
     387            1 :         this.dom = dom;
     388            1 :     }
     389              :     /// Helper to compute a position
     390            1 :     static computePosition(view: EditorView, offset: number): (Position | null) {
     391            1 :         const candidateCoords = view.coordsAtPos(offset);
     392            1 :         if (candidateCoords == null) return null;
     393              : 
     394              :         // Estimate box dimensions for overflow checks
     395            1 :         const boxWidth = 50;
     396            1 :         const boxHeight = 240; // max-height from CSS
     397              : 
     398              :         // coordsAtPos returns viewport coordinates, which map 1:1 to position:fixed.
     399            1 :         const windowHeight = window.innerHeight;
     400            1 :         const windowWidth = window.innerWidth;
     401              : 
     402              :         // Flip to above if there's insufficient space below.
     403              :         // Use CSS `bottom` positioning so the list anchors above the cursor line.
     404            1 :         const spaceBelow = windowHeight - (candidateCoords.bottom + 5);
     405            1 :         const spaceAbove = candidateCoords.top - 5;
     406            1 :         const renderAbove = spaceBelow < boxHeight && spaceAbove > spaceBelow;
     407              : 
     408              :         // Adjust horizontal position if it would overflow the window
     409            1 :         let left = candidateCoords.left;
     410            1 :         if (left + boxWidth > windowWidth) {
     411            0 :             left = windowWidth - boxWidth;
     412              :         }
     413            1 :         left = Math.max(0, left);
     414              : 
     415            1 :         if (renderAbove) {
     416              :             // position:fixed bottom = distance from viewport bottom to list's bottom edge.
     417              :             // We want the list's bottom edge 5px above the cursor's top.
     418            0 :             const bottom = windowHeight - candidateCoords.top + 5;
     419            0 :             return { top: null, bottom: Math.max(0, bottom), left };
     420            0 :         } else {
     421            1 :             const top = candidateCoords.bottom + 5;
     422            1 :             return { top, bottom: null, left };
     423              :         }
     424            1 :     }
     425              : 
     426              :     /// Collect the candidates
     427            1 :     collectCandidates(completion: dashql.buffers.completion.Completion, selectedCandidate: number, selectedCatalogObject: number | null): VirtualCandidate[] {
     428            1 :         const out: VirtualCandidate[] = [];
     429            1 :         const tmpCandidate = new dashql.buffers.completion.CompletionCandidate();
     430            1 :         const tmpCatalogObject = new dashql.buffers.completion.CompletionCandidateObject();
     431              : 
     432              :         // Collect the candidates
     433            1 :         for (let i = 0; i < completion.candidatesLength(); ++i) {
     434            1 :             const ca = completion.candidates(i, tmpCandidate)!;
     435            1 :             let totalObjects = ca.catalogObjectsLength();
     436            1 :             let candidateType: CompletionCandidateType;
     437            1 :             if (ca.candidateTags()! & dashql.buffers.completion.CandidateTag.IDENTITY) {
     438            1 :                 candidateType = CompletionCandidateType.IDENTITY;
     439            1 :             } else if (ca.catalogObjectsLength() > 0) {
     440            1 :                 const co = ca.catalogObjects(0, tmpCatalogObject)!;
     441            1 :                 candidateType = (co.objectType() as number) as CompletionCandidateType;
     442              :                 // A table parameter reads the result of a script or an attachment
     443            1 :                 switch (co.tableParameter()) {
     444            0 :                     case dashql.buffers.completion.CompletionTableParameter.SCRIPT:
     445            0 :                         candidateType = CompletionCandidateType.SCRIPT;
     446            0 :                         break;
     447            0 :                     case dashql.buffers.completion.CompletionTableParameter.ATTACHMENT:
     448            0 :                         candidateType = CompletionCandidateType.ATTACHMENT;
     449              :                         break;
     450            1 :                 }
     451            1 :             } else {
     452            1 :                 candidateType = CompletionCandidateType.KEYWORD;
     453              :             }
     454            1 :             out.push({
     455            1 :                 candidateLabel: ca.displayText()!,
     456            1 :                 candidateType,
     457            1 :                 isSelected: false,
     458            1 :                 totalObjectCount: totalObjects,
     459            1 :                 selectedCatalogObject: null,
     460            1 :             });
     461            1 :         }
     462              : 
     463              :         // Mark selected
     464            1 :         if (selectedCandidate < out.length) {
     465            1 :             out[selectedCandidate].isSelected = true;
     466              :         }
     467              : 
     468              :         // Update the selected candidate
     469            1 :         const ca = completion.candidates(selectedCandidate, tmpCandidate)!;
     470            1 :         if (selectedCatalogObject != null && ca.catalogObjectsLength() > 0) {
     471            1 :             const co = ca.catalogObjects(selectedCatalogObject, tmpCatalogObject)!;
     472            1 :             const o = out[selectedCandidate];
     473            1 :             o.selectedCatalogObject = selectedCatalogObject;
     474            1 :             const ot = co.objectType();
     475            1 :             o.candidateType = (ot == dashql.buffers.completion.CompletionCandidateObjectType.NONE)
     476            0 :                 ? null
     477            1 :                 : (ot as number) as CompletionCandidateType;
     478            1 :             o.isSelected = true;
     479            1 :             o.totalObjectCount = ca.catalogObjectsLength();
     480            1 :         }
     481            1 :         return out;
     482            1 :     }
     483              : 
     484              :     /// Update the completion list
     485            1 :     update(view: EditorView, state: EditorState) {
     486            1 :         const processor = state.field(DashQLProcessorPlugin);
     487              : 
     488              :         // Short-circuit noops
     489            1 :         if (this.renderedCompletion === processor.scriptCompletion) {
     490            1 :             return;
     491              :         }
     492            1 :         this.renderedCompletion = processor.scriptCompletion;
     493              : 
     494              :         // Hide completion?
     495            1 :         if (processor.scriptCompletion?.status !== DashQLCompletionStatus.AVAILABLE
     496            1 :             || processor.scriptCompletion?.passiveHint) {
     497            1 :             if (!this.list.isHidden) {
     498            1 :                 view.requestMeasure<(Position | null)>({
     499            1 :                     key: this,
     500            1 :                     read: (_view) => null,
     501            1 :                     write: (_null, _view) => {
     502            1 :                         this.list.hide();
     503            1 :                     }
     504            1 :                 });
     505              :             }
     506            1 :             return;
     507            1 :         }
     508            1 :         const selectedCandidate = processor.scriptCompletion.candidateId;
     509            1 :         const selectedCatalogObject = processor.scriptCompletion.catalogObjectId ?? null;
     510              : 
     511              :         // Invalid candidate?
     512            1 :         const completion = processor.scriptCompletion;
     513            1 :         const completionBuffer = completion.buffer.read();
     514            1 :         if (completion.candidateId >= completionBuffer.candidatesLength()) {
     515            1 :             return;
     516              :         }
     517              :         // Current candidate
     518            1 :         const candidate = completionBuffer.candidates(completion.candidateId);
     519            1 :         if (candidate == null) {
     520            1 :             return;
     521              :         }
     522            1 :         const candidateLoc = candidate.targetLocation()!;
     523            1 :         const candidateLocOffset = candidateLoc.offset();
     524              : 
     525              :         // Collect all candidates
     526            1 :         const pending = this.collectCandidates(completionBuffer, selectedCandidate, selectedCatalogObject);
     527              : 
     528              :         // Update the container position.
     529              :         // Keyed so that a later update (e.g. hiding) supersedes a pending measure.
     530              :         // The offset can still outlive the text it points into, the list is hidden then.
     531            1 :         view.requestMeasure<(Position | null)>({
     532            1 :             key: this,
     533            1 :             read: (view) => {
     534            1 :                 if (candidateLocOffset > view.state.doc.length) return null;
     535            1 :                 return CompletionList.computePosition(view, candidateLocOffset);
     536            1 :             },
     537            1 :             write: (pos: Position | null, _view) => {
     538            1 :                 if (pos == null) {
     539            1 :                     this.list.hide();
     540            1 :                     return;
     541            1 :                 }
     542            1 :                 this.list.show();
     543            1 :                 this.list.updatePosition(pos);
     544            1 :                 this.list.updateCandidates(pending);
     545            1 :             }
     546            1 :         });
     547            1 :     }
     548            1 : }
     549              : 
     550            1 : export const DashQLCompletionListPlugin = ViewPlugin.fromClass(
     551            1 :     class {
     552            1 :         container: CompletionList;
     553              : 
     554            1 :         constructor(view: EditorView) {
     555            1 :             this.container = new CompletionList(view);
     556            1 :         }
     557            1 :         update(update: ViewUpdate) {
     558            1 :             this.container.mount(update.view.dom);
     559            1 :             this.container.update(update.view, update.state);
     560            1 :         }
     561            1 :         destroy() {
     562            1 :             this.container.destroy();
     563            1 :         }
     564            1 :     }
     565            1 : );
        

Generated by: LCOV version 2.0-1