Skip to content

Conversation

@senithkay
Copy link
Contributor

@senithkay senithkay commented Nov 19, 2025

Purpose

The completion suggestions were not appearing when typing certain identifiers such as user. to access fields of a record type.
This occurred because CodeMirror was using stale completion data, as the newly fetched completions were not being synced with the internal completionSource.

Resolves: wso2/product-ballerina-integrator#1841

Goals

  • Ensure CodeMirror always uses the latest fetched completion items.
  • Avoid showing stale or outdated completion lists.
  • Improve reliability and accuracy of code completion, especially when accessing fields on record types (e.g., user.).

Approach

  • Implemented a synchronization mechanism between the fetched completions and CodeMirror’s completionSource.
  • Added a waitForStateChange() utility that waits until the updated completion values are applied before CodeMirror handles completion queries.
  • Synced the latest completions via useRef and requestAnimationFrame, ensuring the editor receives up-to-date data.
  • This resolves the issue where completions would not appear because CodeMirror was referencing outdated values.

Summary by CodeRabbit

  • Refactor
    • Improved code completion responsiveness by implementing enhanced asynchronous handling for completion data retrieval and processing in the expression editor.
    • Strengthened synchronization of completion state to ensure better data consistency and reliability across the editing interface.
    • These optimizations provide a smoother and more responsive experience when working with code completion suggestions.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 19, 2025

Walkthrough

The changes introduce asynchronous completion handling in the expression editor. buildCompletionSource now accepts and awaits async completion fetches instead of synchronous ones. A new synchronization mechanism using refs in ChipExpressionEditor coordinates when completions are ready, ensuring the completion source waits for updates before processing user input.

Changes

Cohort / File(s) Summary
Asynchronous completion source
workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/CodeUtils.ts
Modified buildCompletionSource to accept () => Promise<CompletionItem[]> instead of synchronous completions. Returned function is now async and awaits completions. Completion trigger logic simplified to only suppress '+' character (previously '+' and ':'). Preceding text analysis moved earlier to enable conditional gating based on dot or valid word.
Completion synchronization
workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionEditor.tsx
Added completionsFetchScheduledRef and completionsRef to track completion fetch state. Introduced waitForStateChange helper that polls via requestAnimationFrame until completions are ready. Wrapped completionSource in useMemo to synchronize with prop updates. Updated effect to reset fetch flag when completions arrive. Added useMemo import.

Sequence Diagram

sequenceDiagram
    participant User
    participant Editor as ChipExpressionEditor
    participant Waiter as waitForStateChange
    participant Source as buildCompletionSource
    participant Fetcher as getCompletions

    User->>Editor: Types character
    Editor->>Editor: Set completionsFetchScheduledRef=true
    Editor->>Waiter: Poll for completions
    
    rect rgb(200, 220, 255)
    Note over Waiter: Polling phase
    loop requestAnimationFrame
        Waiter->>Editor: Check completionsFetchScheduledRef
    end
    end
    
    par Async fetch
        Source->>Fetcher: await getCompletions()
        Fetcher-->>Source: Promise<CompletionItem[]>
    end
    
    Editor->>Editor: Update completionsRef
    Editor->>Editor: Set completionsFetchScheduledRef=false
    Waiter-->>Source: Return ready completions
    
    rect rgb(220, 240, 220)
    Note over Source: Process phase
    Source->>Source: Filter & map completions
    Source-->>Editor: CompletionResult | null
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Pay particular attention to the waitForStateChange polling mechanism and its interaction with requestAnimationFrame—ensure the synchronization logic correctly prevents race conditions between completion fetches and editor updates
  • Verify that the simplified completion trigger logic (only suppressing '+') doesn't inadvertently suppress necessary completions for other contexts (previously also suppressed ':')
  • Review the dependency array in useMemo to confirm props.completions is the correct and only required dependency

Possibly related PRs

  • PR #906: Directly modifies the same functions in CodeUtils.ts and completion handling in ChipExpressionEditor.tsx with overlapping scope.
  • PR #832: Addresses stale completion issues by implementing a defer/wait mechanism for completion updates, similar synchronization approach to this PR.

Suggested reviewers

  • hevayo
  • gigara
  • kanushka

Poem

🐰 Hop, hop—expressions now complete,
With async awaits and synchrony sweet,
No more periods lost in the fray,
Member suggestions light up the way!
The editor grins, RefS in hand,
Waiting for completions across the land! 🌟

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix completions not displayed for field access' accurately describes the main issue being resolved—completions not showing when accessing fields via dot notation (e.g., user.)
Description check ✅ Passed The PR description covers Purpose (with linked issue), Goals, and Approach sections comprehensively, explaining the stale completion data issue and the synchronization solution implemented.
Linked Issues check ✅ Passed The PR successfully addresses issue #1841 by enabling completion suggestions when typing a period for field access through the implemented synchronization mechanism between fetched completions and CodeMirror's completionSource.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the completion display issue: making getCompletions async, simplifying trigger logic, and adding synchronization via waitForStateChange utility—no unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/CodeUtils.ts (1)

400-434: Guard against word being null when triggering completions after .

context.matchBefore(/\w*/) can return null (for example when the last non-space char is . but it's not preceded by a word, such as " ." or ".."). In that case, the current guard:

if (lastNonSpaceChar !== '.' && (
    !word || (word.from === word.to && !context.explicit)
)) {
    return null;
}

will not return early (because lastNonSpaceChar === '.'), and the later use of word.text / word.from will throw at runtime.

Add a dedicated null check after the special-case . handling so you never dereference word when it is absent:

-        const word = context.matchBefore(/\w*/);
-        if (lastNonSpaceChar !== '.' && (
-            !word || (word.from === word.to && !context.explicit)
-        )) {
-            return null;
-        }
+        const word = context.matchBefore(/\w*/);
+        if (lastNonSpaceChar !== '.' && (
+            !word || (word.from === word.to && !context.explicit)
+        )) {
+            return null;
+        }
+
+        // Even when triggered by `.`, bail out if no word was found before the cursor.
+        if (!word) {
+            return null;
+        }

This preserves the desired “always allow completions after . when there is a preceding word” behavior, while avoiding a potential runtime error.

🧹 Nitpick comments (2)
workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/CodeUtils.ts (1)

417-433: Add error handling around async getCompletions to avoid breaking the completion source

getCompletions is now async and may rely on RPC or network calls. If it rejects, the exception will propagate into CodeMirror’s completion pipeline, potentially disabling completions or spamming the console.

Wrap the await in a try/catch and fail gracefully:

-        const completions = await getCompletions();
+        let completions: CompletionItem[];
+        try {
+            completions = await getCompletions();
+        } catch (error) {
+            // Swallow failures and let CodeMirror continue operating
+            console.error("Failed to fetch completions", error);
+            return null;
+        }

This makes the completion source robust to transient backend failures while keeping the rest of the editor responsive.

workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionEditor.tsx (1)

129-145: Clarify completionSource memoization and its dependency on props.completions

completionSource is memoized with:

const completionSource = useMemo(() => {
    return buildCompletionSource(waitForStateChange);
}, [props.completions]);

But the EditorView and its autocompletion({ override: [completionSource] }) extension are created only once in the useEffect with an empty dependency array (lines 219–267). This means:

  • CodeMirror will always use the first completionSource instance wired into the initial EditorState.
  • Subsequent re-renders and new completionSource instances (when props.completions changes) are not propagated to the existing EditorView.

Because completionSource internally closes over completionsRef and completionsFetchScheduledRef, which are mutable refs, you don’t actually need to recreate the function when props.completions changes—the single instance will see updated data via the refs.

To reduce confusion and avoid unnecessary allocations, you can simplify:

-    const completionSource = useMemo(() => {
-        return buildCompletionSource(waitForStateChange);
-    }, [props.completions]);
+    const completionSource = useMemo(
+        () => buildCompletionSource(waitForStateChange),
+        [] // stable for the life of this component
+    );

This better reflects how the EditorView is configured in practice and keeps the dependency story simpler.

Also applies to: 219-267

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a91be70 and 2617d7c.

📒 Files selected for processing (2)
  • workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/CodeUtils.ts (1 hunks)
  • workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionEditor.tsx (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionEditor.tsx (1)
workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/CodeUtils.ts (1)
  • buildCompletionSource (400-435)

Comment on lines +91 to 94
const completionsRef = useRef<CompletionItem[]>(props.completions);
const helperPaneToggleButtonRef = useRef<HTMLButtonElement>(null);
const completionsFetchScheduledRef = useRef<boolean>(false);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid unbounded requestAnimationFrame polling when completions don’t arrive

The handshake between completionsFetchScheduledRef and waitForStateChange is generally good, but there’s a failure mode:

  • On every doc change, handleChangeListner sets completionsFetchScheduledRef.current = true.
  • waitForStateChange then recursively polls via requestAnimationFrame until completionsFetchScheduledRef.current becomes false.
  • It is set back to false only in the useEffect that mirrors props.completions into completionsRef (line 303–305).

If, for any reason, props.completions isn’t updated (backend error, RPC being disabled, or a parent component choosing not to update on certain edits), completionsFetchScheduledRef.current will stay true and:

  • waitForStateChange’s Promise will never resolve, leaving CodeMirror’s completion request hanging.
  • requestAnimationFrame(checkState) will run indefinitely every frame, which is a subtle performance leak.

Consider adding a timeout/fallback and ensuring the scheduled flag is always cleared, even on failure:

-    const waitForStateChange = (): Promise<CompletionItem[]> => {
-        return new Promise((resolve) => {
-            const checkState = () => {
-                if (!completionsFetchScheduledRef.current) {
-                    resolve(completionsRef.current);
-                } else {
-                    requestAnimationFrame(checkState);
-                }
-            };
-            checkState();
-        });
-    };
+    const waitForStateChange = (timeoutMs = 500): Promise<CompletionItem[]> => {
+        const start = performance.now();
+        return new Promise((resolve) => {
+            const checkState = () => {
+                const timedOut = performance.now() - start > timeoutMs;
+                if (!completionsFetchScheduledRef.current || timedOut) {
+                    // On timeout, fall back to whatever completions we currently have.
+                    completionsFetchScheduledRef.current = false;
+                    resolve(completionsRef.current);
+                    return;
+                }
+                requestAnimationFrame(checkState);
+            };
+            checkState();
+        });
+    };

Additionally, make sure any completion-fetching code that can fail still drives props.completions (or explicitly flips completionsFetchScheduledRef.current back to false in its error path) so the editor doesn’t get stuck waiting.

Also applies to: 102-115, 129-145, 300-305

🤖 Prompt for AI Agents
In
workspaces/ballerina/ballerina-side-panel/src/components/editors/MultiModeExpressionEditor/ChipExpressionEditor/components/ChipExpressionEditor.tsx
around lines 91-94 (and similarly 102-115, 129-145, 300-305), the
requestAnimationFrame polling driven by completionsFetchScheduledRef can spin
forever if props.completions never updates; add a bounded timeout/fallback
inside waitForStateChange so the Promise resolves after a configurable max wait
(e.g., 500-1000ms) and ensure completionsFetchScheduledRef.current is cleared on
timeout or any error path; also update any completion-fetching error handlers to
flip completionsFetchScheduledRef.current = false so the editor never remains
stuck waiting.

@senithkay senithkay changed the base branch from main to bi-1.5.x November 20, 2025 08:06
@kanushka kanushka merged commit f1540d2 into wso2:bi-1.5.x Nov 20, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Show suggestions in the expression editor

2 participants