Skip to content

Commit

Permalink
Sync localStorage with readonly ranges, fix resetting code when there…
Browse files Browse the repository at this point in the history
… are locked lines (#7279)

* Make SolveExercisePageProps global

* Start adding readonly ranges

* Adjust more types

* Make things work
  • Loading branch information
dem4ron authored Jan 10, 2025
1 parent 375f5b4 commit 173c4fe
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 81 deletions.
3 changes: 2 additions & 1 deletion app/helpers/react_components/bootcamp/solve_exercise_page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def data
# rename to `value` or similar? code.code is a bit confusing
code: submission ? submission.code : exercise.stub,
stored_at: submission&.created_at,
readonly_ranges:
readonly_ranges:,
default_readonly_ranges: exercise.readonly_ranges
},
links: {
post_submission: Exercism::Routes.api_bootcamp_solution_submissions_url(solution_uuid: solution.uuid, only_path: true),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import * as Hook from './hooks'
import { INFO_HIGHLIGHT_COLOR } from './extensions/lineHighlighter'
import { debounce } from 'lodash'
import { jikiscript } from '@exercism/codemirror-lang-jikiscript'
import { getCodeMirrorFieldValue } from './getCodeMirrorFieldValue'
import { readOnlyRangesStateField } from './extensions/read-only-ranges/readOnlyRanges'

export const readonlyCompartment = new Compartment()

Expand Down Expand Up @@ -77,6 +79,7 @@ export const CodeMirror = forwardRef(function _CodeMirror(
setEditorLocalStorageValue: (value: {
code: string
storedAt: string
readonlyRanges?: { from: number; to: number }[]
}) => void
},
ref: ForwardedRef<EditorView | null>
Expand All @@ -100,15 +103,18 @@ export const CodeMirror = forwardRef(function _CodeMirror(
const [textarea, setTextarea] = useState<HTMLDivElement | null>(null)

const updateLocalStorageValueOnDebounce = useMemo(() => {
return debounce(
(value: string) =>
setEditorLocalStorageValue({
code: value,
storedAt: new Date().toISOString(),
}),
500
)
}, [setEditorLocalStorageValue])
return debounce((value: string, view) => {
const readonlyRanges = getCodeMirrorFieldValue(
view,
readOnlyRangesStateField
)
setEditorLocalStorageValue({
code: value,
storedAt: new Date().toISOString(),
readonlyRanges: readonlyRanges,
})
}, 500)
}, [setEditorLocalStorageValue, readOnlyRangesStateField])

let value = defaultCode

Expand Down Expand Up @@ -191,7 +197,7 @@ export const CodeMirror = forwardRef(function _CodeMirror(
onEditorChange(
() => setHighlightedLine(0),
(e) => {
updateLocalStorageValueOnDebounce(e.state.doc.toString())
updateLocalStorageValueOnDebounce(e.state.doc.toString(), e.view)
},
() => setHighlightedLineColor(INFO_HIGHLIGHT_COLOR),
() => setHasCodeBeenEdited(true),
Expand All @@ -203,11 +209,8 @@ export const CodeMirror = forwardRef(function _CodeMirror(
}
},
() => {
console.log('editor change callback')
if (onEditorChangeCallback) {
onEditorChangeCallback()
} else {
console.log('no editor callback')
}
}
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { EditorView } from 'codemirror'
import type { Handler } from './CodeMirror'
import { updateReadOnlyRangesEffect } from './extensions/read-only-ranges/readOnlyRanges'
import { useLocalStorage } from '@uidotdev/usehooks'
import useEditorStore from '../store/editorStore'

export function useEditorHandler({
links,
Expand All @@ -13,9 +14,14 @@ export function useEditorHandler({
}: Pick<SolveExercisePageProps, 'links' | 'code'> & { config: Config }) {
const editorHandler = useRef<Handler | null>(null)
const editorViewRef = useRef<EditorView | null>(null)
const [, setEditorLocalStorageValue] = useLocalStorage(
const { setDefaultCode } = useEditorStore()
const [editorLocalStorageValue, setEditorLocalStorageValue] = useLocalStorage(
'bootcamp-editor-value-' + config.title,
{ code: code.code, storedAt: code.storedAt }
{
code: code.code,
storedAt: code.storedAt,
readonlyRanges: code.readonlyRanges,
}
)

const [latestValueSnapshot, setLatestValueSnapshot] = useState<
Expand All @@ -24,7 +30,27 @@ export function useEditorHandler({

const handleEditorDidMount = (handler: Handler) => {
editorHandler.current = handler
setupEditor(editorViewRef.current, code)

if (
// if there is no stored at it means we have not submitted the code yet, ignore this, and keep using localStorage
// localStorage defaults to the stub code.
editorLocalStorageValue.storedAt &&
code.storedAt &&
// if the code on the server is newer than in localstorage, update the storage and load the code from the server
editorLocalStorageValue.storedAt < code.storedAt
) {
setEditorLocalStorageValue({
code: code.code,
storedAt: code.storedAt,
readonlyRanges: code.readonlyRanges,
})
setDefaultCode(code.code)
setupEditor(editorViewRef.current, code)
} else {
// otherwise we are using the code from the storage
setDefaultCode(editorLocalStorageValue.code)
setupEditor(editorViewRef.current, editorLocalStorageValue)
}
}

const onRunCode = useOnRunCode({
Expand All @@ -37,8 +63,13 @@ export function useEditorHandler({
setEditorLocalStorageValue({
code: code.stub,
storedAt: new Date().toISOString(),
readonlyRanges: code.readonlyRanges,
})
setupEditor(editorViewRef.current, { code: '', readonlyRanges: [] })
setupEditor(editorViewRef.current, {
code: code.stub,
readonlyRanges: code.defaultReadonlyRanges,
})
editorHandler.current.setValue(code.stub)
}
}

Expand All @@ -64,9 +95,26 @@ export function useEditorHandler({
}
}

function setupEditor(editorView: EditorView | null, code: Code) {
if (!editorView || !code || !code.readonlyRanges) return
editorView.dispatch({
effects: updateReadOnlyRangesEffect.of(code.readonlyRanges),
})
function setupEditor(
editorView: EditorView | null,
{
readonlyRanges,
code,
}: { readonlyRanges?: { from: number; to: number }[]; code: string }
) {
if (!editorView) return
if (code) {
editorView.dispatch({
changes: {
from: 0,
to: editorView.state.doc.length,
insert: code,
},
})
}
if (readonlyRanges) {
editorView.dispatch({
effects: updateReadOnlyRangesEffect.of(readonlyRanges),
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,15 @@ export default function SolveExercisePage({
localStorageId: 'solve-exercise-page-editor-height',
})

const [_, setEditorLocalStorageValue] = useLocalStorage(
'bootcamp-editor-value-' + exercise.config.title,
{ code: code.code, storedAt: code.storedAt }
)
const [_, setEditorLocalStorageValue] = useLocalStorage<{
code: string
storedAt: string | Date | null
readonlyRanges?: { from: number; to: number }[]
}>('bootcamp-editor-value-' + exercise.config.title, {
code: code.code,
storedAt: code.storedAt,
readonlyRanges: code.readonlyRanges,
})

return (
<SolveExercisePageContextWrapper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ export function useSetupStores({
}: Pick<SolveExercisePageProps, 'exercise' | 'code'>) {
const [editorLocalStorageValue, setEditorLocalStorageValue] = useLocalStorage(
'bootcamp-editor-value-' + exercise.config.title,
{ code: code.code, storedAt: code.storedAt }
{
code: code.code,
storedAt: code.storedAt,
readonlyRanges: code.readonlyRanges,
}
)
const { setDefaultCode } = useEditorStore()
const { initializeTasks } = useTaskStore()
const {
setPreviousTestSuiteResult,
Expand Down Expand Up @@ -81,20 +84,6 @@ export function useSetupStores({

initializeTasks(exercise.tasks, previousTestSuiteResult)
setFlatPreviewTaskTests(exercise.tasks.flatMap((task) => task.tests))

// ensure we always load the latest code to editor
if (
editorLocalStorageValue.storedAt &&
code.storedAt &&
// if the code on the server is newer than in localstorage, update the storage and load the code from the server
editorLocalStorageValue.storedAt < code.storedAt
) {
setEditorLocalStorageValue({ code: code.code, storedAt: code.storedAt })
setDefaultCode(code.code)
} else {
// otherwise we are using the code from the storage
setDefaultCode(editorLocalStorageValue.code)
}
}, [exercise, code])
}

Expand Down
79 changes: 41 additions & 38 deletions app/javascript/components/bootcamp/types/SolveExercisePage.d.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,50 @@
import { LanguageFeatures } from '@/interpreter/interpreter'

type Code = {
stub: string
code: string
storedAt: Date | string | null
readonlyRanges: { from: number; to: number }[]
}

type Solution = {
uuid: string
status: 'completed' | 'in_progress'
}
declare global {
type SolveExercisePageProps = {
solution: Solution
project: Project
exercise: Exercise
code: Code
links: {
postSubmission: string
completeSolution: string
projectsIndex: string
dashboardIndex: string
}
}

interface Exercise {
part: number
introductionHtml: string
config: Config
tasks: Task[]
testResults: Pick<TestSuiteResult<NewTestResult>, 'status'> & {
tests: (Pick<NewTestResult, 'status' | 'slug'> & { actual: string })[]
type Code = {
stub: string
code: string
storedAt: Date | string | null
readonlyRanges?: { from: number; to: number }[]
defaultReadonlyRanges?: { from: number; to: number }[]
}
}

type Project = { slug: string }
type Solution = {
uuid: string
status: 'completed' | 'in_progress'
}

declare type SolveExercisePageProps = {
solution: Solution
project: Project
exercise: Exercise
code: Code
links: {
postSubmission: string
completeSolution: string
projectsIndex: string
dashboardIndex: string
interface Exercise {
part: number
introductionHtml: string
config: Config
tasks: Task[]
testResults: Pick<TestSuiteResult<NewTestResult>, 'status'> & {
tests: (Pick<NewTestResult, 'status' | 'slug'> & { actual: string })[]
}
}
}

declare type Config = {
description: string
title: string
projectType: string
// tasks: Task[];
testsType: 'io' | 'state'
interpreterOptions: LanguageFeatures
type Project = { slug: string }

type Config = {
description: string
title: string
projectType: string
// tasks: Task[];
testsType: 'io' | 'state'
interpreterOptions: LanguageFeatures
}
}

0 comments on commit 173c4fe

Please sign in to comment.