Skip to content

Commit 4de9bd8

Browse files
committed
wip: rewind mode
1 parent a37a3a6 commit 4de9bd8

28 files changed

+500
-240
lines changed

packages/jsrepl/src/app/repl/components/activity-bar.tsx

Lines changed: 51 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
11
import { useCallback, useContext } from 'react'
22
import { useTheme } from 'next-themes'
33
import Link from 'next/link'
4-
import { ResumeIcon } from '@radix-ui/react-icons'
5-
import {
6-
LucideChevronLeft,
7-
LucideChevronRight,
8-
LucideFiles,
9-
LucidePause,
10-
LucidePlay,
11-
} from 'lucide-react'
4+
import { ReplPayload } from '@jsrepl/shared-types'
5+
import { LucideFiles, LucidePlay, LucideRewind, LucideRotateCw } from 'lucide-react'
126
import { LucideEye, LucideMoon, LucidePalette, LucideShare2, LucideSun } from 'lucide-react'
13-
import IconPause from '~icons/mdi/pause.jsx'
147
import IconGithub from '~icons/simple-icons/github.jsx'
158
import Logo from '@/components/logo'
169
import ShareRepl from '@/components/share-repl'
@@ -25,72 +18,35 @@ import {
2518
DropdownMenuTrigger,
2619
} from '@/components/ui/dropdown-menu'
2720
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
28-
import { ReplHistoryModeContext } from '@/context/repl-history-mode-context'
2921
import { ReplPayloadsContext } from '@/context/repl-payloads-context'
30-
import { ReplStateContext } from '@/context/repl-state-context'
22+
import { ReplRewindModeContext } from '@/context/repl-rewind-mode-context'
3123
import { UserStateContext } from '@/context/user-state-context'
24+
import { useReplPreviewShown } from '@/hooks/useReplPreviewShown'
3225
import { Themes } from '@/lib/themes'
3326
import { cn } from '@/lib/utils'
3427

3528
export default function ActivityBar() {
3629
const { resolvedTheme: themeId, setTheme } = useTheme()
37-
const { replState, setReplState } = useContext(ReplStateContext)!
3830
const { userState, setUserState } = useContext(UserStateContext)!
39-
const { historyMode, setHistoryMode } = useContext(ReplHistoryModeContext)!
31+
const { rewindMode, setRewindMode } = useContext(ReplRewindModeContext)!
4032
const { payloads } = useContext(ReplPayloadsContext)!
41-
42-
const startRepl = useCallback(() => {
43-
window.dispatchEvent(new Event('jsrepl-start-repl'))
44-
}, [])
45-
46-
const toggleHistoryMode = useCallback(() => {
47-
const lastPayload = payloads[payloads.length - 1]
48-
if (!lastPayload) {
49-
return
50-
}
51-
52-
setHistoryMode((prev) => ({ ...prev, currentPayloadId: lastPayload.id }))
53-
}, [payloads, setHistoryMode])
54-
55-
const historyModeGoPrev = useCallback(() => {
56-
if (!historyMode) {
57-
return
58-
}
59-
60-
const currentIndex = payloads.findIndex(
61-
(payload) => payload.id === historyMode.currentPayloadId
62-
)
63-
if (currentIndex === -1) {
64-
return
65-
}
66-
67-
const prevPayload = payloads[currentIndex - 1]
68-
if (!prevPayload) {
69-
return
70-
}
71-
72-
setHistoryMode((prev) => ({ ...prev, currentPayloadId: prevPayload.id }))
73-
}, [payloads, setHistoryMode, historyMode])
74-
75-
const historyModeGoNext = useCallback(() => {
76-
if (!historyMode) {
77-
return
78-
}
79-
80-
const currentIndex = payloads.findIndex(
81-
(payload) => payload.id === historyMode.currentPayloadId
82-
)
83-
if (currentIndex === -1) {
84-
return
85-
}
86-
87-
const nextPayload = payloads[currentIndex + 1]
88-
if (!nextPayload) {
89-
return
90-
}
91-
92-
setHistoryMode((prev) => ({ ...prev, currentPayloadId: nextPayload.id }))
93-
}, [payloads, setHistoryMode, historyMode])
33+
const { previewEnabled, previewShown, setPreviewShown } = useReplPreviewShown()
34+
35+
const restartRepl = useCallback(() => {
36+
setRewindMode((prev) => ({ ...prev, active: false, currentPayloadId: null }))
37+
window.dispatchEvent(new Event('jsrepl-restart-repl'))
38+
}, [setRewindMode])
39+
40+
const toggleRewindMode = useCallback(() => {
41+
setRewindMode((prev) => {
42+
if (prev.active) {
43+
return { ...prev, active: false, currentPayloadId: null }
44+
} else {
45+
const lastPayload: ReplPayload | undefined = payloads[payloads.length - 1]
46+
return { ...prev, active: true, currentPayloadId: lastPayload ? lastPayload.id : null }
47+
}
48+
})
49+
}, [payloads, setRewindMode])
9450

9551
return (
9652
<div className="bg-activityBar flex flex-col gap-2 px-1 pb-2 pt-1 [grid-area:activity-bar]">
@@ -135,9 +91,10 @@ export default function ActivityBar() {
13591
variant="ghost"
13692
className={cn(
13793
'text-activityBar-foreground',
138-
replState.showPreview && 'bg-accent border-activityBar-foreground/30 border'
94+
previewShown && 'bg-accent border-activityBar-foreground/30 border'
13995
)}
140-
onClick={() => setReplState((prev) => ({ ...prev, showPreview: !prev.showPreview }))}
96+
disabled={!previewEnabled}
97+
onClick={() => setPreviewShown((prev) => !prev)}
14198
>
14299
<LucideEye size={20} />
143100
</Button>
@@ -155,21 +112,22 @@ export default function ActivityBar() {
155112
size="icon"
156113
variant="ghost"
157114
className="text-activityBar-foreground"
158-
onClick={startRepl}
115+
onClick={restartRepl}
159116
>
160117
<div className="relative">
161-
<LucidePlay size={20} />
162-
{!userState.autostartOnCodeChange && (
163-
<IconPause width={10} height={10} className="absolute -bottom-1 -right-0.5" />
118+
{userState.autostartOnCodeChange ? (
119+
<LucideRotateCw size={18} />
120+
) : (
121+
<LucidePlay size={19} />
164122
)}
165123
</div>
166124
</Button>
167125
</TooltipTrigger>
168126
<TooltipContent side="right" sideOffset={8} align="start">
169-
Start / Restart REPL
127+
{userState.autostartOnCodeChange ? 'Restart REPL' : 'Start REPL'}
170128
<div className="bg-secondary text-secondary-foreground border-primary -mx-2 -mb-1 mt-1 rounded-b border px-2 py-2">
171129
<label className="flex items-center gap-1">
172-
<span>Autostart on code change</span>
130+
<span>Restart on code change</span>
173131
<input
174132
type="checkbox"
175133
defaultChecked={userState.autostartOnCodeChange}
@@ -182,32 +140,24 @@ export default function ActivityBar() {
182140
</TooltipContent>
183141
</Tooltip>
184142

185-
<Button
186-
size="icon"
187-
variant="ghost"
188-
className="text-activityBar-foreground"
189-
onClick={toggleHistoryMode}
190-
>
191-
{historyMode ? <ResumeIcon width={18} height={18} /> : <LucidePause size={18} />}
192-
</Button>
193-
194-
<Button
195-
size="icon"
196-
variant="ghost"
197-
className="text-activityBar-foreground"
198-
onClick={historyModeGoPrev}
199-
>
200-
<LucideChevronLeft size={18} />
201-
</Button>
202-
203-
<Button
204-
size="icon"
205-
variant="ghost"
206-
className="text-activityBar-foreground"
207-
onClick={historyModeGoNext}
208-
>
209-
<LucideChevronRight size={18} />
210-
</Button>
143+
<Tooltip>
144+
<TooltipTrigger asChild>
145+
<Button
146+
size="icon"
147+
variant="ghost"
148+
className={cn(
149+
'text-activityBar-foreground',
150+
rewindMode.active && 'bg-accent border-activityBar-foreground/30 border'
151+
)}
152+
onClick={toggleRewindMode}
153+
>
154+
<LucideRewind size={18} />
155+
</Button>
156+
</TooltipTrigger>
157+
<TooltipContent side="right" sideOffset={8}>
158+
Rewind mode
159+
</TooltipContent>
160+
</Tooltip>
211161

212162
<div className="flex-1" />
213163

@@ -259,7 +209,7 @@ export default function ActivityBar() {
259209

260210
<DropdownMenuContent className="w-96" side="left" align="end">
261211
<DropdownMenuLabel className="text-foreground/80 text-sm font-normal">
262-
<ShareRepl setReplState={setReplState} />
212+
<ShareRepl />
263213
</DropdownMenuLabel>
264214
</DropdownMenuContent>
265215
</DropdownMenu>

packages/jsrepl/src/app/repl/components/code-editor-container.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { cn } from '@/lib/utils'
55
import CodeEditor from './code-editor'
66
import CodeEditorHeader from './code-editor-header'
77
import { ErrorsNotification } from './errors-notification'
8+
import RewindModePanel from './rewind-mode-panel'
89

910
export default function CodeEditorContainer({ className }: { className?: string }) {
1011
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
@@ -19,6 +20,7 @@ export default function CodeEditorContainer({ className }: { className?: string
1920
<MonacoEditorContext.Provider value={contextValue}>
2021
<div className={cn(className, 'relative flex min-w-24 flex-col [grid-area:editor]')}>
2122
<CodeEditorHeader />
23+
<RewindModePanel />
2224
<CodeEditor />
2325
<ErrorsNotification />
2426
</div>

packages/jsrepl/src/app/repl/components/code-editor-header.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react'
4-
import { LucideEllipsisVertical, LucideX } from 'lucide-react'
4+
import { LucideEllipsisVertical, LucideLock, LucideX } from 'lucide-react'
55
import IconPrettier from '~icons/simple-icons/prettier'
66
import { Button } from '@/components/ui/button'
77
import {
@@ -14,6 +14,7 @@ import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
1414
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
1515
import { MonacoEditorContext } from '@/context/monaco-editor-context'
1616
import { ReplInfoContext } from '@/context/repl-info-context'
17+
import { ReplModelsContext } from '@/context/repl-models-context'
1718
import { ReplStateContext } from '@/context/repl-state-context'
1819
import { cn } from '@/lib/utils'
1920
import { FileIcon } from './file-icon'
@@ -30,8 +31,14 @@ export default function CodeEditorHeader() {
3031
const { replState, setReplState } = useContext(ReplStateContext)!
3132
const { replInfo } = useContext(ReplInfoContext)!
3233
const { editorRef } = useContext(MonacoEditorContext)!
34+
const { readOnlyModels } = useContext(ReplModelsContext)!
35+
3336
const headerRef = useRef<HTMLDivElement>(null)
3437

38+
const isReadOnly = useMemo(() => {
39+
return readOnlyModels.has(replState.activeModel)
40+
}, [replState.activeModel, readOnlyModels])
41+
3542
useEffect(() => {
3643
requestAnimationFrame(() => {
3744
const activeModelElement = headerRef.current?.querySelector('[data-active="true"]')
@@ -141,6 +148,11 @@ export default function CodeEditorHeader() {
141148
{modelOption.labelDescription}
142149
</span>
143150
)}
151+
{readOnlyModels.has(modelOption.value) && (
152+
<span className="opacity-40 group-hover:opacity-80 group-data-[active=true]:opacity-80">
153+
<LucideLock size={14} />
154+
</span>
155+
)}
144156
</span>
145157
</Button>
146158

@@ -165,6 +177,7 @@ export default function CodeEditorHeader() {
165177
variant="ghost"
166178
size="icon-xs"
167179
className="text-muted-foreground"
180+
disabled={isReadOnly}
168181
onClick={() => {
169182
editorRef.current?.getAction('editor.action.formatDocument')?.run()
170183
}}

packages/jsrepl/src/app/repl/components/code-editor.module.css

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
--color: rgb(177, 35, 35);
8484
}
8585

86-
.jsreplDecorHighlighted {
87-
outline: 2px solid green;
86+
.jsreplDecorHighlighted::after {
87+
outline: 2px solid theme('colors.primary.DEFAULT');
88+
outline-offset: 1px;
8889
}

packages/jsrepl/src/app/repl/components/code-editor.tsx

Lines changed: 22 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,14 @@ import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
22
import { useTheme } from 'next-themes'
33
import * as monaco from 'monaco-editor'
44
import { MonacoEditorContext } from '@/context/monaco-editor-context'
5-
import { ReplHistoryModeContext } from '@/context/repl-history-mode-context'
5+
import { ReplModelsContext } from '@/context/repl-models-context'
66
import { ReplStateContext } from '@/context/repl-state-context'
77
import { UserStateContext } from '@/context/user-state-context'
88
import useCodeEditorDTS from '@/hooks/useCodeEditorDTS'
99
import useCodeEditorRepl from '@/hooks/useCodeEditorRepl'
10-
import { CodeEditorModel } from '@/lib/code-editor-model'
1110
import { getFileExtension } from '@/lib/fs-utils'
1211
import { loadMonacoTheme } from '@/lib/monaco-themes'
1312
import { PrettierFormattingProvider } from '@/lib/prettier-formatting-provider'
14-
import * as ReplFS from '@/lib/repl-fs'
15-
import { readOnlyFiles } from '@/lib/repl-fs-meta'
1613
import { Themes } from '@/lib/themes'
1714
import { cn } from '@/lib/utils'
1815
import styles from './code-editor.module.css'
@@ -25,37 +22,15 @@ if (process.env.NEXT_PUBLIC_NODE_ENV === 'test' && typeof window !== 'undefined'
2522
export default function CodeEditor() {
2623
const { replState, saveReplState } = useContext(ReplStateContext)!
2724
const { userState } = useContext(UserStateContext)!
28-
const { historyMode } = useContext(ReplHistoryModeContext)!
2925
const { editorRef, setEditor } = useContext(MonacoEditorContext)!
26+
const { models, readOnlyModels } = useContext(ReplModelsContext)!
3027

3128
const containerRef = useRef<HTMLDivElement>(null)
3229
const monacoModelRefs = useRef(new Set<monaco.editor.ITextModel>())
3330

3431
const [isThemeLoaded, setIsThemeLoaded] = useState(false)
3532
const { resolvedTheme: themeId } = useTheme()
36-
const theme = useMemo(() => Themes.find((theme) => theme.id === themeId) ?? Themes[0], [themeId])
37-
38-
const models = useMemo(() => {
39-
const map = new Map<string, InstanceType<typeof CodeEditorModel>>()
40-
console.log('models')
41-
42-
replState.fs.walk('/', (path, entry) => {
43-
if (entry.kind === ReplFS.Kind.File) {
44-
const uri = monaco.Uri.parse('file://' + path)
45-
const model = new CodeEditorModel(uri, entry)
46-
map.set(uri.path, model)
47-
}
48-
})
49-
50-
for (const monacoModel of monaco.editor.getModels()) {
51-
if (!map.has(monacoModel.uri.path)) {
52-
console.log('monacoModel dispose in models', monacoModel.uri.path)
53-
monacoModel.dispose()
54-
}
55-
}
56-
57-
return map
58-
}, [replState.fs])
33+
const theme = useMemo(() => Themes.find((theme) => theme.id === themeId) ?? Themes[0]!, [themeId])
5934

6035
useEffect(() => {
6136
const _monacoModelRefs = new Set<monaco.editor.ITextModel>()
@@ -111,17 +86,12 @@ export default function CodeEditor() {
11186
}, [models, saveReplState])
11287

11388
const isReadOnly = useMemo(() => {
114-
if (historyMode) {
115-
return true
116-
}
117-
118-
const path = replState.activeModel
119-
if (path && readOnlyFiles.has(path)) {
120-
return true
121-
}
89+
return readOnlyModels.has(replState.activeModel)
90+
}, [replState.activeModel, readOnlyModels])
12291

123-
return false
124-
}, [replState.activeModel, historyMode])
92+
const readOnlyMessage = useMemo(() => {
93+
return readOnlyModels.get(replState.activeModel)?.message
94+
}, [replState.activeModel, readOnlyModels])
12595

12696
const editorInitialOptions = useRef<monaco.editor.IStandaloneEditorConstructionOptions>({
12797
model: null,
@@ -153,11 +123,21 @@ export default function CodeEditor() {
153123
}, [models, replState.activeModel, editorRef])
154124

155125
useEffect(() => {
156-
editorInitialOptions.current.readOnly = isReadOnly
157-
if (isReadOnly !== editorRef.current?.getOption(monaco.editor.EditorOptions.readOnly.id)) {
158-
editorRef.current?.updateOptions({ readOnly: isReadOnly })
126+
const options = {
127+
readOnly: isReadOnly,
128+
readOnlyMessage: readOnlyMessage ? { value: readOnlyMessage } : undefined,
129+
}
130+
131+
Object.assign(editorInitialOptions.current, options)
132+
133+
if (
134+
options.readOnly !== editorRef.current?.getOption(monaco.editor.EditorOptions.readOnly.id) ||
135+
options.readOnlyMessage !==
136+
editorRef.current?.getOption(monaco.editor.EditorOptions.readOnlyMessage.id)
137+
) {
138+
editorRef.current?.updateOptions(options)
159139
}
160-
}, [isReadOnly, editorRef])
140+
}, [isReadOnly, editorRef, readOnlyMessage])
161141

162142
useEffect(() => {
163143
editorRef.current?.updateOptions({ fontSize: userState.editorFontSize })

0 commit comments

Comments
 (0)