Skip to content

Commit 544d780

Browse files
committed
Convert theme toggling to CodeMirror Extension
1 parent cea2392 commit 544d780

File tree

6 files changed

+62
-42
lines changed

6 files changed

+62
-42
lines changed

src/components/CommandBar/CommandBarKclInput.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ import type { CommandArgument, KclCommandValue } from '@src/lib/commandTypes'
2626
import { kclManager } from '@src/lib/singletons'
2727
import { useSettings } from '@src/lib/singletons'
2828
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
29-
import { getSystemTheme } from '@src/lib/theme'
29+
import { getResolvedTheme } from '@src/lib/theme'
3030
import { err } from '@src/lib/trap'
3131
import { useCalculateKclExpression } from '@src/lib/useCalculateKclExpression'
3232
import { roundOff, roundOffWithUnits } from '@src/lib/utils'
3333
import { varMentions } from '@src/lib/varCompletionExtension'
3434

3535
import { useModelingContext } from '@src/hooks/useModelingContext'
3636
import styles from './CommandBarKclInput.module.css'
37+
import { editorTheme } from '@src/lib/codeEditor'
3738

3839
// TODO: remove the need for this selector once we decouple all actors from React
3940
const machineContextSelector = (snapshot?: SnapshotFrom<AnyStateMachine>) =>
@@ -173,11 +174,9 @@ function CommandBarKclInput({
173174
? previouslySetValue.valueText.length
174175
: defaultValue.length,
175176
},
176-
theme:
177-
settings.app.theme.current === 'system'
178-
? getSystemTheme()
179-
: settings.app.theme.current,
180177
extensions: [
178+
// Typically we prefer to update CodeMirror outside of React, but this "micro-editor" doesn't exist outside of React.
179+
editorTheme[getResolvedTheme(settings.app.theme.current)],
181180
varMentionsExtension,
182181
EditorView.updateListener.of((vu: ViewUpdate) => {
183182
if (vu.docChanged) {

src/components/layout/areas/CodeEditor.tsx

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
} from 'react'
1212

1313
import { isArray } from '@src/lib/utils'
14-
import { editorTheme } from '@src/lib/codeEditor'
1514

1615
//reference: https://github.com/sachinraja/rodemirror/blob/main/src/use-first-render.ts
1716
const useFirstRender = () => {
@@ -34,7 +33,6 @@ interface CodeEditorProps {
3433
onCreateEditor?: (view: EditorView | null) => void
3534
initialDocValue?: EditorStateConfig['doc']
3635
extensions?: Extension
37-
theme: 'light' | 'dark'
3836
autoFocus?: boolean
3937
selection?: EditorStateConfig['selection']
4038
}
@@ -48,7 +46,6 @@ const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref) => {
4846
onCreateEditor,
4947
extensions = [],
5048
initialDocValue,
51-
theme,
5249
autoFocus = false,
5350
selection,
5451
} = props
@@ -59,7 +56,6 @@ const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref) => {
5956
onCreateEditor,
6057
extensions,
6158
initialDocValue,
62-
theme,
6359
autoFocus,
6460
selection,
6561
})
@@ -91,7 +87,6 @@ export function useCodeMirror(props: UseCodeMirror) {
9187
onCreateEditor,
9288
extensions = [],
9389
initialDocValue,
94-
theme,
9590
autoFocus = false,
9691
selection,
9792
} = props
@@ -104,14 +99,8 @@ export function useCodeMirror(props: UseCodeMirror) {
10499

105100
const targetExtensions = useMemo(() => {
106101
let exts = isExtensionArray(extensions) ? extensions : []
107-
if (theme === 'dark') {
108-
exts = [...exts, editorTheme.dark]
109-
} else if (theme === 'light') {
110-
exts = [...exts, editorTheme.light]
111-
}
112-
113102
return exts
114-
}, [extensions, theme])
103+
}, [extensions])
115104

116105
useEffect(() => {
117106
if (container && !state) {

src/components/layout/areas/KclEditorPane.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import type { PropsWithChildren } from 'react'
33
import { ActionIcon } from '@src/components/ActionIcon'
44
import { useConvertToVariable } from '@src/hooks/useToolbarGuards'
55
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
6-
import { commandBarActor, settingsActor } from '@src/lib/singletons'
6+
import {
7+
commandBarActor,
8+
getSettings,
9+
settingsActor,
10+
} from '@src/lib/singletons'
711
import { withSiteBaseURL } from '@src/lib/withBaseURL'
812
import toast from 'react-hot-toast'
913
import styles from './KclEditorMenu.module.css'
@@ -51,7 +55,6 @@ import { lineHighlightField } from '@src/editor/highlightextension'
5155
import { modelingMachineEvent } from '@src/lang/KclManager'
5256
import { kclManager } from '@src/lib/singletons'
5357
import { useSettings } from '@src/lib/singletons'
54-
import { Themes, getSystemTheme } from '@src/lib/theme'
5558
import { reportRejection, trap } from '@src/lib/trap'
5659
import { onMouseDragMakeANewNumber, onMouseDragRegex } from '@src/lib/utils'
5760
import {
@@ -61,8 +64,9 @@ import {
6164
} from '@src/machines/kclEditorMachine'
6265
import type { AreaTypeComponentProps } from '@src/lib/layout'
6366
import { LayoutPanel, LayoutPanelHeader } from '@src/components/layout/Panel'
64-
import { kclSyntaxHighlightingExtension } from '@src/lib/codeEditor'
67+
import { editorTheme, themeCompartment } from '@src/lib/codeEditor'
6568
import { CustomIcon } from '@src/components/CustomIcon'
69+
import { getResolvedTheme } from '@src/lib/theme'
6670

6771
export const editorShortcutMeta = {
6872
formatCode: {
@@ -97,10 +101,6 @@ export const KclEditorPaneContents = () => {
97101
const context = useSettings()
98102
const lastSelectionEvent = useSelector(kclEditorActor, selectionEventSelector)
99103
const editorIsMounted = useSelector(kclEditorActor, editorIsMountedSelector)
100-
const theme =
101-
context.app.theme.current === Themes.System
102-
? getSystemTheme()
103-
: context.app.theme.current
104104
const { copilotLSP, kclLSP } = useLspContext()
105105

106106
// When this component unmounts, we need to tell the machine that the editor
@@ -147,6 +147,9 @@ export const KclEditorPaneContents = () => {
147147

148148
const editorExtensions = useMemo(() => {
149149
const extensions = [
150+
themeCompartment.of(
151+
editorTheme[getResolvedTheme(getSettings().app.theme.current)]
152+
),
150153
drawSelection({
151154
cursorBlinkRate: cursorBlinking.current ? 1200 : 0,
152155
}),
@@ -188,7 +191,6 @@ export const KclEditorPaneContents = () => {
188191
closeBrackets(),
189192
highlightActiveLine(),
190193
highlightSelectionMatches(),
191-
kclSyntaxHighlightingExtension,
192194
rectangularSelection(),
193195
dropCursor(),
194196
interact({
@@ -226,7 +228,6 @@ export const KclEditorPaneContents = () => {
226228
<CodeEditor
227229
initialDocValue={initialCode.current}
228230
extensions={editorExtensions}
229-
theme={theme}
230231
onCreateEditor={(_editorView) => {
231232
kclManager.setEditorView(_editorView)
232233

src/lang/KclManager.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ import { bracket } from '@src/lib/exampleKcl'
9393
import { isDesktop } from '@src/lib/isDesktop'
9494
import toast from 'react-hot-toast'
9595
import { signal } from '@preact/signals-core'
96+
import {
97+
editorTheme,
98+
themeCompartment,
99+
appSettingsThemeEffect,
100+
settingsUpdateAnnotation,
101+
} from '@src/lib/codeEditor'
96102

97103
interface ExecuteArgs {
98104
ast?: Node<Program>
@@ -1096,6 +1102,21 @@ export class KclManager extends EventTarget {
10961102
})
10971103
}
10981104
}
1105+
setEditorTheme(theme: 'light' | 'dark') {
1106+
if (this._editorView) {
1107+
console.trace(`kclManager.setEditorTheme: ${theme}`)
1108+
this._editorView.dispatch({
1109+
effects: [
1110+
appSettingsThemeEffect.of(theme),
1111+
themeCompartment.reconfigure(editorTheme[theme]),
1112+
],
1113+
annotations: [
1114+
settingsUpdateAnnotation.of(null),
1115+
Transaction.addToHistory.of(false),
1116+
],
1117+
})
1118+
}
1119+
}
10991120
/**
11001121
* Given an array of Diagnostics remove any duplicates by hashing a key
11011122
* in the format of from + ' ' + to + ' ' + message.

src/lib/codeEditor.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'
2+
import {
3+
Annotation,
4+
Compartment,
5+
type Extension,
6+
StateEffect,
7+
} from '@codemirror/state'
28
import { tags } from '@lezer/highlight'
39
import { EditorView } from 'codemirror'
10+
import { Themes } from '@src/lib/theme'
411

512
export const normalizeLineEndings = (str: string, normalized = '\n') => {
613
return str.replace(/\r?\n/g, normalized)
@@ -90,6 +97,7 @@ const baseKclHighlights = HighlightStyle.define([
9097

9198
const darkKclHighlights = HighlightStyle.define(
9299
[
100+
...baseKclHighlights.specs,
93101
{
94102
tag: [tags.keyword, tags.annotation],
95103
color: colors.orange.dark,
@@ -149,23 +157,21 @@ const darkTheme = EditorView.theme(
149157
}
150158
)
151159

152-
/**
153-
* CodeMirror theme extensions for KCL syntax highlighting.
154-
*
155-
* Not included in package because it uses CSS variables local to ZDS.
156-
* TODO: Make application-agnostic maybe, if other clients want this theme.
157-
*/
158-
export const kclSyntaxHighlightingExtension = [
159-
// Overrides are provided by `darkKclHighlights` and must be first
160-
syntaxHighlighting(darkKclHighlights),
161-
syntaxHighlighting(baseKclHighlights),
162-
]
163-
164160
/**
165161
* Nearly-empty themes that just mark themselves as light or dark
166162
* so our {@link syntaxHighlightingExtension} can apply its styling.
167163
*/
168164
export const editorTheme = {
169-
light: lightTheme,
170-
dark: darkTheme,
171-
}
165+
[Themes.Light]: [lightTheme, syntaxHighlighting(baseKclHighlights)],
166+
[Themes.Dark]: [darkTheme, syntaxHighlighting(darkKclHighlights)],
167+
} satisfies Record<Exclude<Themes, 'system'>, Extension>
168+
/** Compartment to allow us to reconfigure CodeMirror's theme dynamically outside of React */
169+
export const themeCompartment = new Compartment()
170+
171+
/** Annotation to flag CodeMirror transactions as coming from an app settings update */
172+
export const settingsUpdateAnnotation = Annotation.define<null>()
173+
174+
/** StateEffect to dispatch to CodeMirror when the app theme setting changes */
175+
export const appSettingsThemeEffect = StateEffect.define<'light' | 'dark'>()
176+
/** StateEffect to dispatch to CodeMirror when the app blinkingCursor setting changes */
177+
export const appSettingsBlinkingCursorEffect = StateEffect.define<boolean>()

src/machines/settingsMachine.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
Themes,
4040
darkModeMatcher,
4141
getOppositeTheme,
42+
getResolvedTheme,
4243
getSystemTheme,
4344
setThemeClass,
4445
} from '@src/lib/theme'
@@ -208,13 +209,16 @@ export const settingsMachine = setup({
208209
const rootContext = self.system.get('root')?.getSnapshot().context
209210
const sceneInfra = rootContext?.sceneInfra
210211
const sceneEntitiesManager = rootContext?.sceneEntitiesManager
212+
const kclManager = rootContext?.kclManager
211213

212-
if (!sceneInfra || !sceneEntitiesManager) {
214+
if (!sceneInfra || !sceneEntitiesManager || !kclManager) {
213215
return
214216
}
217+
const resolvedTheme = getResolvedTheme(context.app.theme.current)
215218
const opposingTheme = getOppositeTheme(context.app.theme.current)
216219
sceneInfra.theme = opposingTheme
217220
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
221+
kclManager.setEditorTheme(resolvedTheme)
218222
},
219223
setAllowOrbitInSketchMode: ({ context, self }) => {
220224
const rootContext = self.system.get('root')?.getSnapshot().context

0 commit comments

Comments
 (0)