Skip to content

Commit

Permalink
refactor: extract observables to constants and use more operators
Browse files Browse the repository at this point in the history
  • Loading branch information
exuanbo committed Dec 6, 2023
1 parent 172074c commit e25b08a
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 76 deletions.
6 changes: 2 additions & 4 deletions src/app/stateSaver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useEffect } from 'react'

import { saveState as saveStateToLocalStorage } from './localStorage'
import { selectStateToPersist, type StateToPersist } from './persist'
import { applySelector } from './selector'
import { store } from './store'
import { subscribe } from './subscribe'
import { saveState as saveStateToUrl } from './url'
Expand All @@ -14,8 +13,7 @@ const saveState = (state: StateToPersist): void => {

export const useStateSaver = (): void => {
useEffect(() => {
const stateToPersist = applySelector(selectStateToPersist)
saveState(stateToPersist)
return subscribe(store.onState(selectStateToPersist), saveState)
const stateToPersist$ = store.onState(selectStateToPersist, { initial: true })
return subscribe(stateToPersist$, saveState)
}, [])
}
12 changes: 8 additions & 4 deletions src/features/controller/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ export const useController = (): Controller => {
const controller = useSingleton(() => new Controller())

useEffect(() => {
return subscribe(store.onAction(setEditorInput), controller.resetSelf)
const setEditorInput$ = store.onAction(setEditorInput)
return subscribe(setEditorInput$, controller.resetSelf)
}, [controller])

useEffect(() => {
const setAutoAssemble$ = store.onAction(setAutoAssemble)
return subscribe(
store.onAction(setAutoAssemble).pipe(
setAutoAssemble$.pipe(
debounceTime(UPDATE_TIMEOUT_MS),
filter((shouldAutoAssemble) => shouldAutoAssemble && !applySelector(selectIsAssembled)),
),
Expand All @@ -35,12 +37,14 @@ export const useController = (): Controller => {
}, [controller])

useEffect(() => {
return subscribe(store.onAction(setAssemblerState), controller.resetSelf)
const setAssemblerState$ = store.onAction(setAssemblerState)
return subscribe(setAssemblerState$, controller.resetSelf)
}, [controller])

useEffect(() => {
const runtimeConfiguration$ = store.onState(selectRuntimeConfiguration)
return subscribe(
store.onState(selectRuntimeConfiguration).pipe(
runtimeConfiguration$.pipe(
filter(() => {
// `setSuspended` action listener will resume the main loop with new configuration
// so we skip calling `stopAndRun` if cpu is suspended
Expand Down
12 changes: 12 additions & 0 deletions src/features/editor/codemirror/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { EditorState, TransactionSpec } from '@codemirror/state'

export const replaceContent = (state: EditorState, content: string): TransactionSpec => {
const endIndex = state.doc.length
return {
changes: {
from: 0,
to: endIndex,
insert: content,
},
}
}
21 changes: 17 additions & 4 deletions src/features/editor/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import keyboardInput from './keyboard_input.asm?raw'
import procedures from './procedures.asm?raw'
import sevenSegmentDisplay from './seven_segment_display.asm?raw'
import softwareInterrupts from './software_interrupts.asm?raw'
import __template from './template.asm?raw'
import template from './template.asm?raw'
import trafficLights from './traffic_lights.asm?raw'
import visualDisplayUnit from './visual_display_unit.asm?raw'

Expand All @@ -16,11 +16,24 @@ interface Example {
content: string
}

export const template: Example = {
title: getTitleFrom(__template),
content: __template,
const templateExample: Example = {
title: getTitleFrom(template),
content: template,
}

export { templateExample as template }

export const isTemplate = (value: string) => value === templateExample.content

export const templateSelection = (() => {
const { title, content } = templateExample
const titleIndex = content.indexOf(title)
return {
anchor: titleIndex,
head: titleIndex + title.length,
}
})()

export const examples: readonly Example[] = [
procedures,
softwareInterrupts,
Expand Down
122 changes: 59 additions & 63 deletions src/features/editor/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Transaction } from '@codemirror/state'
import { addUpdateListener } from '@codemirror-toolkit/extensions'
import { mapRangeSetToArray, rangeSetsEqual } from '@codemirror-toolkit/utils'
import { useEffect } from 'react'
import { debounceTime, filter, first, map, merge, of, switchMap, timer } from 'rxjs'
import { debounceTime, filter, first, map, merge, of, switchMap, tap, timer } from 'rxjs'

import { applySelector, useSelector } from '@/app/selector'
import { store } from '@/app/store'
Expand All @@ -24,6 +24,7 @@ import { BreakpointEffect, getBreakpointMarkers } from './codemirror/breakpoints
import { HighlightLineEffect } from './codemirror/highlightLine'
import { useViewEffect } from './codemirror/react'
import { onUpdate } from './codemirror/rx'
import { replaceContent } from './codemirror/state'
import { lineLocAt, lineRangesEqual } from './codemirror/text'
import { disableVim, enableVim, initVim$ } from './codemirror/vim'
import { WavyUnderlineEffect } from './codemirror/wavyUnderline'
Expand All @@ -41,22 +42,21 @@ import {
setEditorInput,
setEditorMessage,
} from './editorSlice'
import { template } from './examples'
import { isTemplate, templateSelection } from './examples'

export const useVimKeybindings = (): void => {
useViewEffect((view) => {
const vimKeybindings$ = store.onState(selectVimKeybindings, { initial: true })
return subscribe(
store.onState(selectVimKeybindings, { initial: true }).pipe(
vimKeybindings$.pipe(
switchMap((shouldEnable) => {
if (shouldEnable) {
return initVim$.pipe(map(() => enableVim))
}
return of(disableVim)
}),
),
(action) => {
action(view)
},
(action) => action(view),
)
}, [])
}
Expand All @@ -70,79 +70,67 @@ const isSyncFromState = hasStringAnnotation(AnnotationValue.SyncFromState)

export const useSyncInput = (): void => {
useViewEffect((view) => {
const viewUpdate$ = onUpdate(view)
return subscribe(
onUpdate(view).pipe(
viewUpdate$.pipe(
filter((update) => update.docChanged),
debounceTime(UPDATE_TIMEOUT_MS),
filter((update) => {
// only consider the first transaction
const transaction = update.transactions[0] as Transaction | undefined
return !transaction || !isSyncFromState(transaction)
}),
map((update) => update.state.doc.toString()),
map((value) => setEditorInput({ value })),
),
(update) => {
const input = update.state.doc.toString()
store.dispatch(setEditorInput({ value: input }))
},
(action) => store.dispatch(action),
)
}, [])

useViewEffect((view) => {
const setEditorInput$ = store.onAction(setEditorInput)
return subscribe(
store.onAction(setEditorInput).pipe(filter(({ isFromFile }) => isFromFile)),
({ value }) => {
view.dispatch(
syncFromState({
changes: {
from: 0,
to: view.state.doc.length,
insert: value,
},
}),
)
},
setEditorInput$.pipe(
filter(({ isFromFile }) => isFromFile),
map(({ value }) => replaceContent(view.state, value)),
map((spec) => syncFromState(spec)),
),
(transaction) => view.dispatch(transaction),
)
}, [])
}

export const useAutoFocus = (): void => {
useViewEffect((view) => {
const setEditorInput$ = store.onAction(setEditorInput)
return subscribe(
store.onAction(setEditorInput).pipe(
setEditorInput$.pipe(
filter(({ isFromFile }) => isFromFile),
filter(({ value }) => value === template.content),
filter(({ value }) => isTemplate(value)),
tap(() => view.focus()),
map(() => ({ selection: templateSelection })),
),
() => {
view.focus()
const { title, content } = template
const titleIndex = content.indexOf(title)
view.dispatch({
selection: {
anchor: titleIndex,
head: titleIndex + title.length,
},
})
},
(transaction) => view.dispatch(transaction),
)
}, [])
}

export const useAutoAssemble = (): void => {
useViewEffect((view) => {
const initialAssembleTimeoutId = window.setTimeout(() => {
if (applySelector(selectAutoAssemble)) {
const input = view.state.doc.toString()
assembleFrom(input)
}
}, UPDATE_TIMEOUT_MS)
return () => {
window.clearTimeout(initialAssembleTimeoutId)
}
const initialAssemble$ = timer(UPDATE_TIMEOUT_MS)
return subscribe(
initialAssemble$.pipe(
filter(() => applySelector(selectAutoAssemble)),
map(() => view.state.doc.toString()),
),
assembleFrom,
)
}, [])

useEffect(() => {
const setEditorInput$ = store.onAction(setEditorInput)
return subscribe(
store.onAction(setEditorInput).pipe(
setEditorInput$.pipe(
filter(() => applySelector(selectAutoAssemble)),
switchMap(({ value, isFromFile }) => {
if (isFromFile) {
Expand All @@ -158,42 +146,50 @@ export const useAutoAssemble = (): void => {

export const useAssemblerError = (): void => {
useViewEffect((view) => {
const viewUpdate$ = onUpdate(view)
return subscribe(
onUpdate(view).pipe(
viewUpdate$.pipe(
filter((update) => update.docChanged),
filter(() => !!applySelector(selectAssemblerError)),
map(() => clearAssemblerError()),
),
() => {
store.dispatch(clearAssemblerError())
},
(action) => store.dispatch(action),
)
}, [])

useViewEffect((view) => {
return subscribe(store.onState(selectAssemblerErrorRange), (errorRange) => {
const hasError = errorRange !== undefined
view.dispatch({
effects: WavyUnderlineEffect.of({
add: errorRange,
filter: () => hasError,
const assemblerErrorRange$ = store.onState(selectAssemblerErrorRange)
return subscribe(
assemblerErrorRange$.pipe(
map((errorRange) => {
const hasError = errorRange !== undefined
return WavyUnderlineEffect.of({
add: errorRange,
filter: () => hasError,
})
}),
})
})
map((effect) => ({ effects: effect })),
),
(transaction) => view.dispatch(transaction),
)
}, [])
}

export const useHighlightLine = (): void => {
useEffect(() => {
const setEditorInput$ = store.onAction(setEditorInput)
return subscribe(
store.onAction(setEditorInput).pipe(filter(({ isFromFile }) => isFromFile)),
() => {
store.dispatch(clearEditorHighlightRange())
},
setEditorInput$.pipe(
filter(({ isFromFile }) => isFromFile),
map(() => clearEditorHighlightRange()),
),
(action) => store.dispatch(action),
)
}, [])

useViewEffect((view) => {
return subscribe(store.onState(curryRight2(selectEditorHighlightLinePos)(view)), (linePos) => {
const highlightLinePos$ = store.onState(curryRight2(selectEditorHighlightLinePos)(view))
return subscribe(highlightLinePos$, (linePos) => {
const shouldAddHighlight = linePos !== undefined
view.dispatch({
effects: shouldAddHighlight
Expand Down
2 changes: 1 addition & 1 deletion src/features/io/VisualDisplayUnit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const VisualDisplayUnit = (): JSX.Element | null => {
switchMap(() => memoryData$.pipe(first())),
map(setVduDataFrom),
),
store.dispatch,
(action) => store.dispatch(action),
)
}, [])

Expand Down

0 comments on commit e25b08a

Please sign in to comment.