Line data Source code
1 : import type * as dashql from '@ankoh/dashql-core';
2 : import type { Terminal } from '@xterm/xterm';
3 :
4 : import type { DashQLShell } from './api.js';
5 : import { CandidateListRenderer, collectCandidates, placeBelowOrAbove } from '../views/completion_list/completion_list_view.js';
6 :
7 : import * as styles from '../views/completion_list/completion_list.module.css';
8 :
9 : /// The completion list of a shell, over the terminal.
10 : /// The shell owns the candidates, the selection and the keys, this only shows what the shell says.
11 1 : export class TerminalCompletionOverlay {
12 : /// The list
13 1 : readonly list: CandidateListRenderer;
14 : /// The candidates of the shell
15 1 : buffer: dashql.FlatBufferPtr<dashql.buffers.completion.Completion> | null = null;
16 : /// The revision of the candidates
17 1 : revision: bigint | null = null;
18 : /// The terminal column of the start of the replaced text
19 1 : anchorColumn: number = 0;
20 : /// The anchor that the list is placed at, null if it has to be placed
21 1 : placedAt: string | null = null;
22 :
23 1 : constructor(
24 1 : readonly container: HTMLElement,
25 1 : readonly terminal: Terminal,
26 1 : selectPreviousVariant: () => void,
27 1 : selectNextVariant: () => void,
28 1 : ) {
29 1 : this.list = new CandidateListRenderer(selectPreviousVariant, selectNextVariant);
30 : // The terminal is dark in both schemes
31 1 : this.list.rootElement.classList.add(styles.on_dark_surface);
32 : // Inside the container and not on the body: the list has to hide with a page that goes to the background
33 1 : container.appendChild(this.list.rootElement);
34 1 : }
35 :
36 : /// Is a completion list open? It can still be hidden if its row is scrolled out of the terminal.
37 1 : public get isOpen() { return this.buffer != null; }
38 :
39 : /// Show what the shell says, after the terminal processed the output of the shell
40 1 : public update(shell: DashQLShell) {
41 1 : const state = shell.terminalCompletionList();
42 1 : if (state == null) {
43 1 : this.close();
44 1 : return;
45 1 : }
46 1 : if (this.buffer == null || this.revision !== state.revision) {
47 1 : this.buffer = shell.terminalCompletionBuffer();
48 1 : this.revision = state.revision;
49 1 : }
50 1 : if (this.buffer == null) {
51 0 : this.close();
52 0 : return;
53 0 : }
54 1 : this.anchorColumn = state.anchorColumn;
55 1 : this.list.show();
56 1 : this.list.updateCandidates(collectCandidates(this.buffer.read(), state.selection, state.variant));
57 : // The width of the list can differ now
58 1 : this.placedAt = null;
59 1 : this.place();
60 1 : }
61 :
62 : /// Place the list at its anchor, if the terminal moved it
63 1 : public place() {
64 1 : if (!this.isOpen) return;
65 1 : const screen = this.terminal.element?.querySelector('.xterm-screen');
66 1 : if (screen == null) return;
67 1 : const rect = screen.getBoundingClientRect();
68 1 : const { cols, rows, buffer: { active } } = this.terminal;
69 1 : const row = active.baseY + active.cursorY - active.viewportY;
70 1 : if (rect.width === 0 || row < 0 || row >= rows) {
71 1 : this.list.hide();
72 1 : this.placedAt = null;
73 1 : return;
74 1 : }
75 1 : const placedAt = `${rect.left} ${rect.top} ${rect.width} ${rect.height} ${row} ${cols} ${rows} ${this.anchorColumn}`;
76 1 : if (placedAt === this.placedAt) return;
77 1 : this.placedAt = placedAt;
78 1 : const cellWidth = rect.width / cols;
79 1 : const cellHeight = rect.height / rows;
80 1 : this.list.show();
81 1 : this.list.updatePosition(this.list.fitIntoWindow(placeBelowOrAbove({
82 1 : left: rect.left + this.anchorColumn * cellWidth,
83 1 : top: rect.top + row * cellHeight,
84 1 : bottom: rect.top + (row + 1) * cellHeight,
85 1 : })));
86 1 : }
87 :
88 : /// Close the list
89 1 : public close() {
90 1 : this.buffer = null;
91 1 : this.revision = null;
92 1 : this.placedAt = null;
93 1 : this.list.hide();
94 1 : }
95 :
96 1 : public destroy() {
97 1 : this.close();
98 1 : this.list.destroy();
99 1 : }
100 1 : }
|