Skip to content

Commit c9f5d44

Browse files
committed
use CM editor for the source of truth of code
allows formatting without losing cursor position
1 parent f9951e5 commit c9f5d44

File tree

5 files changed

+273
-270
lines changed

5 files changed

+273
-270
lines changed

src/Playground.res

Lines changed: 92 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,14 +1180,29 @@ module ControlPanel = {
11801180
}
11811181
}
11821182

1183+
let commandWithKeyboardShortcut = (commandName, ~key) => {
1184+
let userAgent = window.navigator.userAgent
1185+
if userAgent->String.includes("iPhone") || userAgent->String.includes("Android") {
1186+
commandName
1187+
} else if userAgent->String.includes("Mac") {
1188+
`${commandName} (⌘ + ${key})`
1189+
} else {
1190+
`${commandName} (Ctrl + ${key})`
1191+
}
1192+
}
1193+
11831194
@react.component
11841195
let make = (
11851196
~actionIndicatorKey: string,
11861197
~state: CompilerManagerHook.state,
11871198
~dispatch: CompilerManagerHook.action => unit,
1188-
~editorCode: React.ref<string>,
1199+
~editorRef: React.ref<option<CodeMirror.editorInstance>>,
11891200
~setCurrentTab: (tab => tab) => unit,
11901201
) => {
1202+
let format = () =>
1203+
editorRef.current->Option.forEach(editorInstance =>
1204+
dispatch(Format(CodeMirror.editorGetValue(editorInstance)))
1205+
)
11911206
React.useEffect(() => {
11921207
switch state {
11931208
| Ready(_)
@@ -1202,6 +1217,9 @@ module ControlPanel = {
12021217
event->ReactEvent.Keyboard.preventDefault
12031218
setCurrentTab(_ => Output)
12041219
dispatch(RunCode)
1220+
| (true, "s") =>
1221+
event->ReactEvent.Keyboard.preventDefault
1222+
format()
12051223
| _ => ()
12061224
}
12071225
}
@@ -1220,7 +1238,7 @@ module ControlPanel = {
12201238
| Ready(_) =>
12211239
let onFormatClick = evt => {
12221240
ReactEvent.Mouse.preventDefault(evt)
1223-
dispatch(Format(editorCode.current))
1241+
format()
12241242
}
12251243

12261244
let autoRun = switch state {
@@ -1230,18 +1248,6 @@ module ControlPanel = {
12301248
| _ => false
12311249
}
12321250

1233-
let runButtonText = {
1234-
let userAgent = window.navigator.userAgent
1235-
let run = "Run"
1236-
if userAgent->String.includes("iPhone") || userAgent->String.includes("Android") {
1237-
run
1238-
} else if userAgent->String.includes("Mac") {
1239-
`${run} (⌘ + E)`
1240-
} else {
1241-
`${run} (Ctrl + E)`
1242-
}
1243-
}
1244-
12451251
<div className="flex flex-row gap-x-2" dataTestId="control-panel">
12461252
<ToggleButton
12471253
checked=autoRun
@@ -1261,9 +1267,11 @@ module ControlPanel = {
12611267
dispatch(RunCode)
12621268
}}
12631269
>
1264-
{React.string(runButtonText)}
1270+
{React.string(commandWithKeyboardShortcut("Run", ~key="E"))}
1271+
</Button>
1272+
<Button onClick=onFormatClick>
1273+
{React.string(commandWithKeyboardShortcut("Format", ~key="S"))}
12651274
</Button>
1266-
<Button onClick=onFormatClick> {React.string("Format")} </Button>
12671275
<ShareButton actionIndicatorKey />
12681276
</div>
12691277
| _ => React.null
@@ -1513,6 +1521,8 @@ let initialReContent = `Js.log("Hello Reason 3.6!");`
15131521
@react.component
15141522
let make = (~bundleBaseUrl: string, ~versions: array<string>) => {
15151523
let (searchParams, _) = ReactRouter.useSearchParams()
1524+
let containerRef = React.useRef(Nullable.null)
1525+
let editorRef: React.ref<option<CodeMirror.editorInstance>> = React.useRef(None)
15161526

15171527
let versions =
15181528
versions
@@ -1584,7 +1594,10 @@ let make = (~bundleBaseUrl: string, ~versions: array<string>) => {
15841594
// We don't count to infinity. This value is only required to trigger
15851595
// rerenders for specific components (ActivityIndicator)
15861596
let (actionCount, setActionCount) = React.useState(_ => 0)
1587-
let onAction = _ => setActionCount(prev => prev > 1000000 ? 0 : prev + 1)
1597+
let onAction = React.useCallback(
1598+
_ => setActionCount(prev => prev > 1000000 ? 0 : prev + 1),
1599+
[setActionCount],
1600+
)
15881601
let (compilerState, compilerDispatch) = useCompilerManager(
15891602
~bundleBaseUrl,
15901603
~initialVersion?,
@@ -1594,10 +1607,11 @@ let make = (~bundleBaseUrl: string, ~versions: array<string>) => {
15941607
~initialLang,
15951608
~onAction,
15961609
~versions,
1597-
(),
15981610
)
15991611

16001612
let (keyMap, setKeyMap) = React.useState(() => CodeMirror.KeyMap.Default)
1613+
let typingTimer = React.useRef(None)
1614+
let timeoutCompile = React.useRef(_ => ())
16011615

16021616
React.useEffect(() => {
16031617
setKeyMap(_ =>
@@ -1609,50 +1623,82 @@ let make = (~bundleBaseUrl: string, ~versions: array<string>) => {
16091623
None
16101624
}, [])
16111625

1626+
React.useEffect(() => {
1627+
switch containerRef.current {
1628+
| Value(parent) =>
1629+
let mode = switch compilerState {
1630+
| Ready({targetLang: Reason}) => "reason"
1631+
| Ready({targetLang: Res}) => "rescript"
1632+
| _ => "rescript"
1633+
}
1634+
let config: CodeMirror.editorConfig = {
1635+
parent,
1636+
initialValue: initialContent,
1637+
mode,
1638+
readOnly: false,
1639+
lineNumbers: true,
1640+
lineWrapping: false,
1641+
keyMap: CodeMirror.KeyMap.Default,
1642+
errors: [],
1643+
hoverHints: [],
1644+
onChange: {
1645+
value => {
1646+
switch typingTimer.current {
1647+
| None => ()
1648+
| Some(timer) => clearTimeout(timer)
1649+
}
1650+
let timer = setTimeout(~handler=() => {
1651+
timeoutCompile.current(value)
1652+
typingTimer.current = None
1653+
}, ~timeout=100)
1654+
typingTimer.current = Some(timer)
1655+
}
1656+
},
1657+
}
1658+
let editor = CodeMirror.createEditor(config)
1659+
editorRef.current = Some(editor)
1660+
Some(() => CodeMirror.editorDestroy(editor))
1661+
| Null | Undefined => None
1662+
}
1663+
}, [])
1664+
16121665
React.useEffect(() => {
16131666
Dom.Storage2.localStorage->Dom.Storage2.setItem("vimMode", CodeMirror.KeyMap.toString(keyMap))
1667+
editorRef.current->Option.forEach(CodeMirror.editorSetKeyMap(_, keyMap))
16141668
None
16151669
}, [keyMap])
16161670

16171671
let editorCode = React.useRef(initialContent)
16181672

1619-
/* In case the compiler did some kind of syntax conversion / reformatting,
1620-
we take any success results and set the editor code to the new formatted code */
1621-
switch compilerState {
1622-
| Ready({result: FinalResult.Nothing} as ready) =>
1623-
try {
1624-
compilerDispatch(CompileCode(ready.targetLang, editorCode.current))
1625-
} catch {
1626-
| err => Console.error(err)
1627-
}
1628-
| Ready({result: FinalResult.Conv(Api.ConversionResult.Success({code}))}) =>
1629-
editorCode.current = code
1630-
| _ => ()
1631-
}
1632-
16331673
/*
1634-
The codemirror state and the compilerState are not dependent on eachother,
1674+
The codemirror state and the compilerState are not dependent on each other,
16351675
so we need to sync a timeoutCompiler function with our compilerState to be
16361676
able to do compilation on code changes.
16371677
16381678
The typingTimer is a debounce mechanism to prevent compilation during editing
16391679
and will be manipulated by the codemirror onChange function.
16401680
*/
1641-
let typingTimer = React.useRef(None)
1642-
let timeoutCompile = React.useRef(() => ())
1643-
16441681
React.useEffect(() => {
1645-
timeoutCompile.current = () =>
1682+
timeoutCompile.current = code =>
16461683
switch compilerState {
1647-
| Ready(ready) =>
1648-
try {
1649-
compilerDispatch(CompileCode(ready.targetLang, editorCode.current))
1650-
} catch {
1651-
| err => Console.error(err)
1652-
}
1684+
| Ready({targetLang}) => compilerDispatch(CompileCode(targetLang, code))
16531685
| _ => ()
16541686
}
16551687

1688+
switch (compilerState, editorRef.current) {
1689+
| (Ready({result: FinalResult.Nothing, targetLang}), Some(editorInstance)) =>
1690+
try {
1691+
compilerDispatch(CompileCode(targetLang, CodeMirror.editorGetValue(editorInstance)))
1692+
} catch {
1693+
| err => Console.error(err)
1694+
}
1695+
| (
1696+
Ready({result: FinalResult.Conv(Api.ConversionResult.Success({code}))}),
1697+
Some(editorInstance),
1698+
) =>
1699+
CodeMirror.editorSetValue(editorInstance, code)
1700+
| _ => ()
1701+
}
16561702
None
16571703
}, (compilerState, compilerDispatch))
16581704

@@ -1856,12 +1902,6 @@ let make = (~bundleBaseUrl: string, ~versions: array<string>) => {
18561902
| _ => []
18571903
}
18581904

1859-
let mode = switch compilerState {
1860-
| Ready({targetLang: Reason}) => "reason"
1861-
| Ready({targetLang: Res}) => "rescript"
1862-
| _ => "rescript"
1863-
}
1864-
18651905
let (currentTab, setCurrentTab) = React.useState(_ => JavaScript)
18661906

18671907
let disabled = false
@@ -1951,8 +1991,8 @@ let make = (~bundleBaseUrl: string, ~versions: array<string>) => {
19511991
actionIndicatorKey={Int.toString(actionCount)}
19521992
state=compilerState
19531993
dispatch=compilerDispatch
1954-
editorCode
19551994
setCurrentTab
1995+
editorRef
19561996
/>
19571997
<div
19581998
className={`flex ${layout == Column ? "flex-col" : "flex-row"}`}
@@ -1965,26 +2005,9 @@ let make = (~bundleBaseUrl: string, ~versions: array<string>) => {
19652005
? "w-full"
19662006
: "w-[50%]"}`}
19672007
>
1968-
<CodeMirror
2008+
<div
19692009
className="bg-gray-100 h-full"
1970-
mode
1971-
hoverHints=cmHoverHints
1972-
errors=cmErrors
1973-
value={editorCode.current}
1974-
onChange={value => {
1975-
editorCode.current = value
1976-
1977-
switch typingTimer.current {
1978-
| None => ()
1979-
| Some(timer) => clearTimeout(timer)
1980-
}
1981-
let timer = setTimeout(~handler=() => {
1982-
timeoutCompile.current()
1983-
typingTimer.current = None
1984-
}, ~timeout=100)
1985-
typingTimer.current = Some(timer)
1986-
}}
1987-
keyMap
2010+
ref={ReactDOM.Ref.domRef((Obj.magic(containerRef): React.ref<Nullable.t<Dom.element>>))}
19882011
/>
19892012
</div>
19902013
// Separator

src/common/CompilerManagerHook.res

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -249,13 +249,12 @@ let useCompilerManager = (
249249
~initialLang: Lang.t=Res,
250250
~onAction: option<action => unit>=?,
251251
~versions: array<Semver.t>,
252-
(),
253252
) => {
254253
let (state, setState) = React.useState(_ => Init)
255254
let {pathname} = ReactRouter.useLocation()
256255

257256
// Dispatch method for the public interface
258-
let dispatch = (action: action): unit => {
257+
let dispatch = React.useCallback((action: action): unit => {
259258
Option.forEach(onAction, cb => cb(action))
260259
setState(state =>
261260
switch action {
@@ -295,47 +294,49 @@ let useCompilerManager = (
295294
let currentLang = ready.targetLang
296295

297296
Array.find(availableTargetLangs, l => l === lang)
298-
->Option.map(lang => {
299-
// Try to automatically transform code
300-
let (result, targetLang) = switch ready.selected.apiVersion {
301-
| V1 =>
302-
let convResult = switch (currentLang, lang) {
303-
| (Reason, Res) =>
304-
instance->Compiler.convertSyntax(~fromLang=Reason, ~toLang=Res, ~code)->Some
305-
| (Res, Reason) =>
306-
instance->Compiler.convertSyntax(~fromLang=Res, ~toLang=Reason, ~code)->Some
307-
| _ => None
308-
}
297+
->Option.map(
298+
lang => {
299+
// Try to automatically transform code
300+
let (result, targetLang) = switch ready.selected.apiVersion {
301+
| V1 =>
302+
let convResult = switch (currentLang, lang) {
303+
| (Reason, Res) =>
304+
instance->Compiler.convertSyntax(~fromLang=Reason, ~toLang=Res, ~code)->Some
305+
| (Res, Reason) =>
306+
instance->Compiler.convertSyntax(~fromLang=Res, ~toLang=Reason, ~code)->Some
307+
| _ => None
308+
}
309309

310-
/*
310+
/*
311311
Syntax convertion works the following way:
312312
If currentLang -> otherLang is not valid, try to pretty print the code
313313
with the otherLang, in case we e.g. want to copy paste or otherLang code
314314
in the editor and quickly switch to it
315315
*/
316-
switch convResult {
317-
| Some(result) =>
318-
switch result {
319-
| ConversionResult.Fail(_)
320-
| Unknown(_, _)
321-
| UnexpectedError(_) =>
322-
let secondTry =
323-
instance->Compiler.convertSyntax(~fromLang=lang, ~toLang=lang, ~code)
324-
switch secondTry {
316+
switch convResult {
317+
| Some(result) =>
318+
switch result {
325319
| ConversionResult.Fail(_)
326320
| Unknown(_, _)
327-
| UnexpectedError(_) => (FinalResult.Conv(secondTry), lang)
328-
| Success(_) => (Conv(secondTry), lang)
321+
| UnexpectedError(_) =>
322+
let secondTry =
323+
instance->Compiler.convertSyntax(~fromLang=lang, ~toLang=lang, ~code)
324+
switch secondTry {
325+
| ConversionResult.Fail(_)
326+
| Unknown(_, _)
327+
| UnexpectedError(_) => (FinalResult.Conv(secondTry), lang)
328+
| Success(_) => (Conv(secondTry), lang)
329+
}
330+
| ConversionResult.Success(_) => (Conv(result), lang)
329331
}
330-
| ConversionResult.Success(_) => (Conv(result), lang)
332+
| None => (Nothing, lang)
331333
}
332-
| None => (Nothing, lang)
334+
| _ => (Nothing, lang)
333335
}
334-
| _ => (Nothing, lang)
335-
}
336336

337-
Ready({...ready, result, errors: [], targetLang})
338-
})
337+
Ready({...ready, result, errors: [], targetLang})
338+
},
339+
)
339340
->Option.getOr(state)
340341
| _ => state
341342
}
@@ -401,7 +402,7 @@ let useCompilerManager = (
401402
}
402403
}
403404
)
404-
}
405+
}, [onAction])
405406

406407
let dispatchError = (err: error) =>
407408
setState(prev => {

src/common/CompilerManagerHook.resi

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ let useCompilerManager: (
6969
~initialLang: Lang.t=?,
7070
~onAction: action => unit=?,
7171
~versions: array<Semver.t>,
72-
unit,
7372
) => (state, action => unit)
7473

7574
let createUrl: (string, ready) => string

0 commit comments

Comments
 (0)