Line data Source code
1 : import * as React from 'react';
2 :
3 : import { EditorView } from '@codemirror/view';
4 :
5 : import { DashQLCompletionAbortEffect, DashQLProcessorPlugin } from './dashql_processor.js';
6 :
7 : /// The writable DashQL editor of a DOM element, null for read-only editors and other elements
8 1 : function dashqlEditorOf(target: EventTarget | null): EditorView | null {
9 1 : if (!(target instanceof Element)) return null;
10 1 : const dom = target.closest('.cm-editor');
11 1 : if (!(dom instanceof HTMLElement)) return null;
12 1 : const view = EditorView.findFromDOM(dom);
13 1 : if (view == null || view.state.readOnly || view.state.field(DashQLProcessorPlugin, false) == null) return null;
14 1 : return view;
15 1 : }
16 :
17 : /// The last writable DashQL editor of the document that had the focus, a script editor or the prompt of the composer.
18 : /// The editor keeps its selection when it loses the focus, a mention can be inserted at its cursor afterwards.
19 1 : export function useLastFocusedEditor(): () => EditorView | null {
20 1 : const last = React.useRef<EditorView | null>(null);
21 1 : React.useEffect(() => {
22 1 : const onFocusIn = (event: FocusEvent) => {
23 1 : const view = dashqlEditorOf(event.target);
24 1 : if (view != null) last.current = view;
25 1 : };
26 1 : document.addEventListener('focusin', onFocusIn);
27 1 : return () => document.removeEventListener('focusin', onFocusIn);
28 1 : }, []);
29 1 : return React.useCallback(() => {
30 0 : const view = last.current;
31 : // A destroyed view leaves the document
32 0 : if (view == null || !view.dom.isConnected) {
33 0 : last.current = null;
34 0 : return null;
35 0 : }
36 0 : return view;
37 1 : }, []);
38 1 : }
39 :
40 : /// Insert a mention at the cursor of an editor, separated by spaces from the text around it, and focus the editor
41 1 : export function insertMentionAtCursor(view: EditorView, mention: string): void {
42 1 : const text = view.state.doc;
43 1 : const cursor = view.state.selection.main;
44 1 : const charBefore = cursor.from > 0 ? text.sliceString(cursor.from - 1, cursor.from) : '';
45 1 : const charAfter = cursor.to < text.length ? text.sliceString(cursor.to, cursor.to + 1) : '';
46 1 : const before = charBefore !== '' && !/\s|\(/.test(charBefore) ? ' ' : '';
47 1 : const after = charAfter !== '' && /\s|\)|;|,/.test(charAfter) ? '' : ' ';
48 1 : const insert = `${before}${mention}${after}`;
49 1 : view.dispatch({
50 1 : changes: { from: cursor.from, to: cursor.to, insert },
51 1 : selection: { anchor: cursor.from + insert.length },
52 1 : effects: DashQLCompletionAbortEffect.of(null),
53 1 : userEvent: 'input',
54 1 : scrollIntoView: true,
55 1 : });
56 1 : view.focus();
57 1 : }
|