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