Autocomplete - insertion and replacement with enter and tab

Wondering if I can mimic the behaviour of Idea Intelij completion using codemirror :

How Idea works

I’ve the following line :

TopCount( [Member] ,50 )

The cursor is before the C of Count. The proposition returns 3 values : TopCount, TopSum and TopPercent. I choose TopSum

If I use enter the proposition is inserted → TopSumCount( [Member], 50)
If I use tab the proposition is replaces → TopSum( [Member], 50)

How to implement this in codemirror ?

I tried the pseudocode, but I can’t manage to find what xxx should be

                key: "Tab", run: (view) => {
                    const state = view.state;
                    const completionState = view.state.field(xxx);
                    
                    if (completionState?.active && completionState.active.length > 0) {
                        const active = completionState.active[0];
                        const selected = active.options[active.selected || 0];
                        if (selected) {
                            const { from, to } = active.range;
                            view.dispatch({
                                changes: { from, to, insert: selected.label },
                            });
                            closeCompletion(view); // Close the popup
                            return true;
                        }
                    }
                    return false; // Let Tab behave normally if no completion is active
                }

help welcomed

You can use selectedCompletion to get the currently selected option. But note that applying a completion is slightly more involved than what you’re doing here, and I don’t think you’ll be able to implement this behavior for completions that provide a custom apply function.

Thanks a lot, can you give me a hint how you would implement this or as default always make the substitution (I can easily add to CompletionResult the length of the substitution) ?

I’ve found some way to get around this, although cursed.

I have an extension that provides suggestion for wiki links.

async function customCompletion(context: CompletionContext): Promise<CompletionResult | null> {
    const cursorPos = context.pos;
    const notes = await getNotes();
    // Find the start of the `[[` block
    let start = cursorPos - 2;
    while (start > 0 && context.state.sliceDoc(start, start + 2) !== '[[') {
        start--;
    }
    const beforeText = context.state.sliceDoc(start, cursorPos);

    // Ensure we're inside `[[ ]]` brackets
    if (!beforeText.startsWith('[[')) return null;

    // Extract the typed text between `[[` and the cursor
    const typedText = beforeText.slice(2);

    const suggestedNotes = notes.filter((note) =>
        note.localPath?.includes(typedText)
    );
    // If no matches, return null (hide the suggestion box)
    if (suggestedNotes.length === 0) return null;

    // Generate dynamic suggestions
    const options: Completion[] = suggestedNotes.map((note) => ({
        label: `[[${note.name}]]`,
        displayLabel: note.name,
        tab: (view: EditorView) => {
            console.log('tab action', note, view);
        },
        apply: (view: EditorView, completion: Completion, from: number, to: number) => {
            // Replace `[[...]]` with `[word](word)`
            const replacement = `[${note.name}](${note.localPath})`;
            view.dispatch({
                changes: {
                    from: start,
                    to: cursorPos + 2,
                    insert: replacement
                },
                selection: { anchor: start + replacement.length } // Move cursor to end
            });
        }
    }));

    return {
        from: start, // Start of the `[[` block
        to: cursorPos, // End of the current word being typed
        options
    };
}

The apply function handles what happens when the user accept the suggestion by pressing Enter. The tab function handles what should happen when the user press Tab during auto-completion (indent otherwise).

I’ve then modified the keybinding to the Tab key

{
    key: 'Tab',
    run: (view) => {
        const state = view.state;
        const selected = selectedCompletion(state);
        if (
            selected !== null &&
            'tab' in selected &&
            typeof selected.tab === 'function'
        ) {
            selected.tab(view);
            return true;
        }
        indentMore({ state: state, dispatch: view.dispatch });
        return true;
    }
}

It works in my use-case but I think it’s kinda hacky. I don’t know if there is a clever way to do this