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 : );
|