Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type * as monacoNs from 'monaco-editor'

/**
* Custom Monaco context key that reflects whether AI inline completions and the
* STruC++ LSP suggest widget are allowed to coexist (i.e. AI is enabled,
* consented and inline completions are turned on). All Tab overrides below are
* gated on this key so they only take effect while coexistence is active —
* toggling AI off restores Monaco's default Tab=accept behaviour without needing
* to remount the editor.
*/
const COEXISTENCE_CONTEXT_KEY = 'openplcAiLspCoexistence'

export type AiLspCoexistenceController = {
/** Enable/disable the coexistence Tab overrides at runtime. */
setActive: (active: boolean) => void
}

/**
* Wires Tab/Enter so the STruC++ LSP dropdown and the AI ghost text can be shown
* at the same time:
*
* - Enter (and arrow-key selection) accept the LSP suggest widget — Monaco's
* default, left untouched.
* - Tab commits the AI inline suggestion, even while the suggest widget is open
* (Monaco's default reserves Tab for the dropdown when both are visible).
* - While the suggest widget is open but no AI ghost text is present, Tab is
* swallowed (reserved for AI) instead of accepting the highlighted LSP item.
*
* The two overrides are registered once and gated on {@link COEXISTENCE_CONTEXT_KEY};
* standalone keybindings added via `addCommand` are registered as overrides that
* take precedence over Monaco's built-in keybindings when their `when` clause
* matches.
*/
export function installAiLspCoexistenceKeybindings(
editor: monacoNs.editor.IStandaloneCodeEditor,
monaco: typeof monacoNs,
): AiLspCoexistenceController {
const active = editor.createContextKey<boolean>(COEXISTENCE_CONTEXT_KEY, false)

// Tab commits the AI inline suggestion even when the LSP dropdown is visible.
editor.addCommand(
monaco.KeyCode.Tab,
() => {
editor.trigger('openplc-ai-lsp', 'editor.action.inlineSuggest.commit', {})
},
`${COEXISTENCE_CONTEXT_KEY} && inlineSuggestionVisible && !editorReadonly`,
)

// While the LSP dropdown is open without any AI ghost text, Tab is reserved for
// AI: swallow it so it never accepts the highlighted LSP item (Enter does that).
editor.addCommand(
monaco.KeyCode.Tab,
() => {
/* no-op: Tab is reserved for AI completions while coexistence is active */
},
`${COEXISTENCE_CONTEXT_KEY} && suggestWidgetVisible && !inlineSuggestionVisible`,
)

return {
setActive: (value: boolean) => active.set(value),
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import './configs'

import { Editor as PrimitiveEditor } from '@monaco-editor/react'
Expand All @@ -17,6 +17,7 @@
import { Modal, ModalContent, ModalTitle } from '../../../../_molecules/modal'
import { toast } from '../../../[app]/toast/use-toast'
import { renderDiffReview } from './ai-diff-review'
import { type AiLspCoexistenceController, installAiLspCoexistenceKeybindings } from './ai-lsp-coexistence'
import {
arduinoApiCompletion,
cppSignatureHelp,
Expand Down Expand Up @@ -127,6 +128,7 @@
const editorRef = useRef<null | monaco.editor.IStandaloneCodeEditor>(null)
const monacoRef = useRef<null | typeof monaco>(null)
const focusDisposables = useRef<{ onFocus?: monaco.IDisposable; onBlur?: monaco.IDisposable }>({})
const coexistenceRef = useRef<AiLspCoexistenceController | null>(null)
const [editorMounted, setEditorMounted] = useState(false)
const [modelVersion, setModelVersion] = useState(0)
const isSyncingModelRef = useRef(false)
Expand Down Expand Up @@ -986,6 +988,12 @@
)
}

// Tab/Enter split so AI ghost text and the LSP dropdown can coexist. The
// overrides are gated on a context key we drive from `inlineCompletionsActive`
// (see the effect below), so they're inert while AI is off.
coexistenceRef.current = installAiLspCoexistenceKeybindings(editorInstance, monacoInstance)
coexistenceRef.current.setActive(inlineCompletionsActive)

// Manual trigger suggest
const handleKeyUp = (e: KeyboardEvent) => {
const isMac = navigator.platform.toUpperCase().includes('MAC')
Expand Down Expand Up @@ -1220,13 +1228,13 @@
// Editor options
// -----------------------------------------------------------------------

// Inline AI completions take over the suggest widget only while they are
// actually active (same gate as the provider registration above). When the
// user turns inline completions off, fall back to Monaco's normal quick
// suggestions (the auto-dropdown). Ctrl+Space still triggers the suggest
// widget manually in both modes — `quickSuggestions` only governs the
// automatic popup, and `suppressSuggestions` only suppresses the auto popup
// while an inline suggestion is showing.
// AI inline completions and the STruC++ LSP suggest widget COEXIST: the
// LSP dropdown still auto-opens (fast, deterministic, great for variables and
// struct members) while the AI ghost text renders alongside it. Acceptance is
// split by key — Enter/arrows accept the LSP dropdown, Tab commits the AI
// suggestion (see `installAiLspCoexistenceKeybindings`). `suppressSuggestions`
// is therefore false so the dropdown is NOT hidden while ghost text shows.
// Ctrl+Space still triggers the suggest widget manually in both modes.
const inlineCompletionsActive =
capabilities.hasAIAssistant &&
aiState.isEnabled &&
Expand All @@ -1249,7 +1257,9 @@
tabSize: 4,
insertSpaces: true,
detectIndentation: false,
quickSuggestions: inlineCompletionsActive ? false : undefined,
// Let the LSP dropdown auto-open in both modes — even with AI on, we want
// the fast LSP completions visible (the user accepts them with Enter/arrows).
quickSuggestions: undefined,
// Pinned for cross-platform consistency with the variables-code-editor.
// Monaco's default is platform-dependent (12 on macOS, 14 elsewhere) —
// without this both surfaces would mismatch on Linux/Windows even
Expand All @@ -1274,11 +1284,20 @@
...(inlineCompletionsActive && {
inlineSuggest: {
enabled: true,
suppressSuggestions: true,
// Keep the LSP dropdown visible alongside the AI ghost text instead of
// suppressing it — coexistence is the whole point here.
suppressSuggestions: false,
},
}),
}

// Keep the coexistence Tab overrides in sync with AI state so toggling AI on/off
// takes effect without remounting the editor. `editorInstanceId` re-asserts it
// after a remount (the mount handler also sets it, this is belt-and-braces).
useEffect(() => {
coexistenceRef.current?.setActive(inlineCompletionsActive)
}, [inlineCompletionsActive, editorInstanceId])

// -----------------------------------------------------------------------
// Drag-and-drop
// -----------------------------------------------------------------------
Expand Down
Loading