From 9d3a72f5515d87e3ca2c95e864941afdf4f71a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Tsnobiladz=C3=A9?= Date: Tue, 11 Feb 2025 11:12:15 +0100 Subject: [PATCH 1/6] use dispatch for execution --- src/ConsolePanel.res | 9 +- src/OutputPanel.res | 19 +- src/Playground.res | 46 +++-- src/RenderPanel.res | 65 +------ src/RenderPanel.resi | 6 +- src/common/CompilerManagerHook.res | 281 ++++++++++++++++++---------- src/common/CompilerManagerHook.resi | 8 +- 7 files changed, 240 insertions(+), 194 deletions(-) diff --git a/src/ConsolePanel.res b/src/ConsolePanel.res index 04f11d9b8..770cd2094 100644 --- a/src/ConsolePanel.res +++ b/src/ConsolePanel.res @@ -3,22 +3,23 @@ type logLevel = [ | #warn | #error ] +type log = {level: logLevel, content: array} @react.component -let make = (~logs, ~setLogs) => { +let make = (~logs, ~appendLog) => { React.useEffect(() => { let cb = e => { let data = e["data"] switch data["type"] { | #...logLevel as logLevel => let args: array = data["args"] - setLogs(previous => previous->Array.concat([(logLevel, args)])) + appendLog(logLevel, args) | _ => () } } Webapi.Window.addEventListener("message", cb) Some(() => Webapi.Window.removeEventListener("message", cb)) - }, []) + }, [appendLog])

{React.string("Console")}

@@ -30,7 +31,7 @@ let make = (~logs, ~setLogs) => { | logs => let content = logs - ->Array.mapWithIndex(((logLevel, log), i) => { + ->Array.mapWithIndex(({level: logLevel, content: log}, i) => { let log = Array.join(log, " ")
 {
+let make = (~compilerState, ~appendLog) => {
+  let validReact = switch compilerState {
+  | CompilerManagerHook.Executing({state: {validReactCode: true}})
+  | Compiling({validReactCode: true})
+  | Ready({validReactCode: true}) => true
+  | _ => false
+  }
+
+  let logs = switch compilerState {
+  | CompilerManagerHook.Executing({state: {logs}})
+  | Compiling({logs})
+  | Ready({logs}) => logs
+  | _ => []
+  }
   
- setLogs(_ => [])} /> +
- +
} diff --git a/src/Playground.res b/src/Playground.res index 5749a1bbc..80a8dd61e 100644 --- a/src/Playground.res +++ b/src/Playground.res @@ -1130,21 +1130,27 @@ module ControlPanel = { ~state: CompilerManagerHook.state, ~dispatch: CompilerManagerHook.action => unit, ~editorCode: React.ref, - ~runOutput, - ~toggleRunOutput, ) => { let children = switch state { | Init => React.string("Initializing...") | SwitchingCompiler(_ready, _version) => React.string("Switching Compiler...") - | Compiling(_, _) + | Compiling(_) + | Executing(_) | Ready(_) => let onFormatClick = evt => { ReactEvent.Mouse.preventDefault(evt) dispatch(Format(editorCode.current)) } + let autoRun = switch state { + | CompilerManagerHook.Executing({state: {autoRun: true}}) + | Compiling({autoRun: true}) + | Ready({autoRun: true}) => true + | _ => false + } +
- toggleRunOutput()}> + dispatch(ToggleAutoRun)}> {React.string("Auto-run")} @@ -1176,7 +1182,6 @@ module OutputPanel = { ~compilerState: CompilerManagerHook.state, ~editorCode: React.ref, ~currentTab: tab, - ~runOutput, ) => { /* We need the prevState to understand different @@ -1199,8 +1204,9 @@ module OutputPanel = { } | (_, Ready({result: Comp(Success(_)) as result})) => ControlPanel.codeFromResult(result)->Some - | (Ready({result: Comp(Success(_)) as result}), Compiling(_, _)) => + | (Ready({result: Comp(Success(_)) as result}), Compiling(_)) => ControlPanel.codeFromResult(result)->Some + | (_, Executing({jsCode})) => Some(jsCode) | _ => None } | None => @@ -1213,8 +1219,9 @@ module OutputPanel = { prevState.current = Some(compilerState) let resultPane = switch compilerState { - | Compiling(ready, _) - | Ready(ready) => + | Compiling(ready) + | Ready(ready) + | Executing({state: ready}) => switch ready.result { | Comp(Success(_)) | Conv(Success(_)) => React.null @@ -1246,8 +1253,9 @@ module OutputPanel = {
let errorPane = switch compilerState { - | Compiling(ready, _) + | Compiling(ready) | Ready(ready) + | Executing({state: ready}) | SwitchingCompiler(ready, _) => let config = ready.selected.config let setConfig = config => compilerDispatch(UpdateConfig(config)) @@ -1273,7 +1282,9 @@ module OutputPanel = { let prevSelected = React.useRef(0) let selected = switch compilerState { - | Compiling(_, _) => prevSelected.current + | Executing(_) + | Compiling(_) => + prevSelected.current | Ready(ready) => switch ready.result { | Comp(Success(_)) @@ -1285,10 +1296,10 @@ module OutputPanel = { prevSelected.current = selected - let (logs, setLogs) = React.useState(_ => []) + let appendLog = (level, content) => compilerDispatch(AppendLog({level, content})) let tabs = [ - (Output, ), + (Output, ), (JavaScript, output), (Problems, errorPane), (Settings, settingsPane), @@ -1483,7 +1494,7 @@ let make = (~versions: array) => { } None - }, [compilerState]) + }, (compilerState, compilerDispatch)) let (layout, setLayout) = React.useState(_ => Webapi.Window.innerWidth < breakingPoint ? Column : Row @@ -1693,17 +1704,12 @@ let make = (~versions: array) => { }) - let (runOutput, setRunOutput) = React.useState(() => false) - let toggleRunOutput = () => setRunOutput(prev => !prev) -
) => { {React.array(headers)}
- +
diff --git a/src/RenderPanel.res b/src/RenderPanel.res index 8cb391e2e..15dc46893 100644 --- a/src/RenderPanel.res +++ b/src/RenderPanel.res @@ -1,68 +1,5 @@ -let wrapReactApp = code => - `(function () { - ${code} - window.reactRoot.render(React.createElement(App.make, {})); -})();` - -let capitalizeFirstLetter = string => { - let firstLetter = string->String.charAt(0)->String.toUpperCase - `${firstLetter}${string->String.sliceToEnd(~start=1)}` -} - @react.component -let make = (~compilerState: CompilerManagerHook.state, ~clearLogs, ~runOutput) => { - let (validReact, setValidReact) = React.useState(() => false) - React.useEffect(() => { - if runOutput { - switch compilerState { - | CompilerManagerHook.Ready({selected, result: Comp(Success({js_code}))}) => - clearLogs() - open Babel - - let ast = Parser.parse(js_code, {sourceType: "module"}) - let {entryPointExists, code, imports} = PlaygroundValidator.validate(ast) - let imports = imports->Dict.mapValues(path => { - let filename = path->String.sliceToEnd(~start=9) // the part after "./stdlib/" - let filename = switch selected.id { - | {major: 12, minor: 0, patch: 0, preRelease: Some(Alpha(alpha))} if alpha < 8 => - let filename = if filename->String.startsWith("core__") { - filename->String.sliceToEnd(~start=6) - } else { - filename - } - capitalizeFirstLetter(filename) - | {major} if major < 12 && filename->String.startsWith("core__") => - capitalizeFirstLetter(filename) - | _ => filename - } - let compilerVersion = switch selected.id { - | {major: 12, minor: 0, patch: 0, preRelease: Some(Alpha(alpha))} if alpha < 8 => { - Semver.major: 12, - minor: 0, - patch: 0, - preRelease: Some(Alpha(8)), - } - | {major, minor} if (major === 11 && minor < 2) || major < 11 => { - major: 11, - minor: 2, - patch: 0, - preRelease: Some(Beta(1)), - } - | version => version - } - CompilerManagerHook.CdnMeta.getStdlibRuntimeUrl(compilerVersion, filename) - }) - - entryPointExists - ? code->wrapReactApp->EvalIFrame.sendOutput(imports) - : EvalIFrame.sendOutput(code, imports) - setValidReact(_ => entryPointExists) - | _ => () - } - } - None - }, (compilerState, runOutput)) - +let make = (~validReact) => {

{React.string("React")}

{validReact diff --git a/src/RenderPanel.resi b/src/RenderPanel.resi index 7a5e6d8bb..cf39cd0ee 100644 --- a/src/RenderPanel.resi +++ b/src/RenderPanel.resi @@ -1,6 +1,2 @@ @react.component -let make: ( - ~compilerState: CompilerManagerHook.state, - ~clearLogs: unit => unit, - ~runOutput: bool, -) => Jsx.element +let make: (~validReact: bool) => Jsx.element diff --git a/src/common/CompilerManagerHook.res b/src/common/CompilerManagerHook.res index 4236657ef..41168f463 100644 --- a/src/common/CompilerManagerHook.res +++ b/src/common/CompilerManagerHook.res @@ -136,6 +136,17 @@ let attachCompilerAndLibraries = async (~version, ~libraries: array, ()) } } +let wrapReactApp = code => + `(function () { + ${code} + window.reactRoot.render(React.createElement(App.make, {})); +})();` + +let capitalizeFirstLetter = string => { + let firstLetter = string->String.charAt(0)->String.toUpperCase + `${firstLetter}${string->String.sliceToEnd(~start=1)}` +} + type error = | SetupError(string) | CompilerLoadingError(string) @@ -157,6 +168,9 @@ type ready = { targetLang: Lang.t, errors: array, // For major errors like bundle loading result: FinalResult.t, + autoRun: bool, + validReactCode: bool, + logs: array, } type state = @@ -164,7 +178,8 @@ type state = | SetupFailed(string) | SwitchingCompiler(ready, Semver.t) // (ready, targetId, libraries) | Ready(ready) - | Compiling(ready, (Lang.t, string)) + | Compiling(ready) + | Executing({state: ready, jsCode: string}) type action = | SwitchToCompiler(Semver.t) // id @@ -172,6 +187,8 @@ type action = | Format(string) | CompileCode(Lang.t, string) | UpdateConfig(Config.t) + | AppendLog(ConsolePanel.log) + | ToggleAutoRun let createUrl = (pathName, ready) => { let params = switch ready.targetLang { @@ -213,119 +230,128 @@ let useCompilerManager = ( // Dispatch method for the public interface let dispatch = (action: action): unit => { Option.forEach(onAction, cb => cb(action)) - switch action { - | SwitchToCompiler(id) => - switch state { - | Ready(ready) => - // TODO: Check if libraries have changed as well - if ready.selected.id !== id { - setState(_ => SwitchingCompiler(ready, id)) - } else { - () + setState(state => + switch action { + | SwitchToCompiler(id) => + switch state { + | Ready(ready) if ready.selected.id !== id => + // TODO: Check if libraries have changed as well + SwitchingCompiler(ready, id) + | _ => state } - | _ => () - } - | UpdateConfig(config) => - switch state { - | Ready(ready) => - ready.selected.instance->Compiler.setConfig(config) - setState(_ => { + | UpdateConfig(config) => + switch state { + | Ready(ready) => + ready.selected.instance->Compiler.setConfig(config) let selected = {...ready.selected, config} - Compiling({...ready, selected}, (ready.targetLang, ready.code)) - }) - | _ => () - } - | CompileCode(lang, code) => - switch state { - | Ready(ready) => setState(_ => Compiling({...ready, code}, (lang, code))) - | _ => () - } - | SwitchLanguage({lang, code}) => - switch state { - | Ready(ready) => - let instance = ready.selected.instance - let availableTargetLangs = Version.availableLanguages(ready.selected.apiVersion) - - let currentLang = ready.targetLang - - Array.find(availableTargetLangs, l => l === lang)->Option.forEach(lang => { - // Try to automatically transform code - let (result, targetLang) = switch ready.selected.apiVersion { - | V1 => - let convResult = switch (currentLang, lang) { - | (Reason, Res) => - instance->Compiler.convertSyntax(~fromLang=Reason, ~toLang=Res, ~code)->Some - | (Res, Reason) => - instance->Compiler.convertSyntax(~fromLang=Res, ~toLang=Reason, ~code)->Some - | _ => None - } + Compiling({...ready, selected}) + | _ => state + } + | CompileCode(lang, code) => + switch state { + | Ready(ready) => Compiling({...ready, code, targetLang: lang}) + | _ => state + } + | SwitchLanguage({lang, code}) => + switch state { + | Ready(ready) => + let instance = ready.selected.instance + let availableTargetLangs = Version.availableLanguages(ready.selected.apiVersion) + + let currentLang = ready.targetLang + + Array.find(availableTargetLangs, l => l === lang) + ->Option.map(lang => { + // Try to automatically transform code + let (result, targetLang) = switch ready.selected.apiVersion { + | V1 => + let convResult = switch (currentLang, lang) { + | (Reason, Res) => + instance->Compiler.convertSyntax(~fromLang=Reason, ~toLang=Res, ~code)->Some + | (Res, Reason) => + instance->Compiler.convertSyntax(~fromLang=Res, ~toLang=Reason, ~code)->Some + | _ => None + } - /* + /* Syntax convertion works the following way: If currentLang -> otherLang is not valid, try to pretty print the code with the otherLang, in case we e.g. want to copy paste or otherLang code in the editor and quickly switch to it */ - switch convResult { - | Some(result) => - switch result { - | ConversionResult.Fail(_) - | Unknown(_, _) - | UnexpectedError(_) => - let secondTry = - instance->Compiler.convertSyntax(~fromLang=lang, ~toLang=lang, ~code) - switch secondTry { + switch convResult { + | Some(result) => + switch result { | ConversionResult.Fail(_) | Unknown(_, _) - | UnexpectedError(_) => (FinalResult.Conv(secondTry), lang) - | Success(_) => (Conv(secondTry), lang) + | UnexpectedError(_) => + let secondTry = + instance->Compiler.convertSyntax(~fromLang=lang, ~toLang=lang, ~code) + switch secondTry { + | ConversionResult.Fail(_) + | Unknown(_, _) + | UnexpectedError(_) => (FinalResult.Conv(secondTry), lang) + | Success(_) => (Conv(secondTry), lang) + } + | ConversionResult.Success(_) => (Conv(result), lang) } - | ConversionResult.Success(_) => (Conv(result), lang) + | None => (Nothing, lang) } - | None => (Nothing, lang) + | _ => (Nothing, lang) } - | _ => (Nothing, lang) - } - setState(_ => Ready({...ready, result, errors: [], targetLang})) - }) - | _ => () - } - | Format(code) => - switch state { - | Ready(ready) => - let instance = ready.selected.instance - let convResult = switch ready.targetLang { - | Res => instance->Compiler.resFormat(code)->Some - | Reason => instance->Compiler.reasonFormat(code)->Some - | _ => None + Ready({...ready, result, errors: [], targetLang}) + }) + ->Option.getOr(state) + | _ => state } + | Format(code) => + switch state { + | Ready(ready) => + let instance = ready.selected.instance + let convResult = switch ready.targetLang { + | Res => instance->Compiler.resFormat(code)->Some + | Reason => instance->Compiler.reasonFormat(code)->Some + | _ => None + } - let result = switch convResult { - | Some(result) => - switch result { - | ConversionResult.Success(success) => - // We will only change the result to a ConversionResult - // in case the reformatting has actually changed code - // otherwise we'd loose previous compilationResults, although - // the result should be the same anyways - if code !== success.code { + let result = switch convResult { + | Some(result) => + switch result { + | ConversionResult.Success(success) => + // We will only change the result to a ConversionResult + // in case the reformatting has actually changed code + // otherwise we'd loose previous compilationResults, although + // the result should be the same anyways + if code !== success.code { + FinalResult.Conv(result) + } else { + ready.result + } + | ConversionResult.Fail(_) + | Unknown(_, _) + | UnexpectedError(_) => FinalResult.Conv(result) - } else { - ready.result } - | ConversionResult.Fail(_) - | Unknown(_, _) - | UnexpectedError(_) => - FinalResult.Conv(result) + | None => ready.result } - | None => ready.result - } - setState(_ => Ready({...ready, result, errors: []})) - | _ => () + Ready({...ready, result, errors: []}) + | _ => state + } + | AppendLog(log) => + switch state { + | Ready(ready) => Ready({...ready, logs: Array.concat(ready.logs, [log])}) + | _ => state + } + | ToggleAutoRun => + switch state { + | Ready({autoRun: true} as ready) => Ready({...ready, autoRun: false}) + | Ready({autoRun: false} as ready) => Compiling({...ready, autoRun: true}) + | _ => state + } } - } + ) } let dispatchError = (err: error) => @@ -392,6 +418,9 @@ let useCompilerManager = ( versions, errors: [], result: FinalResult.Nothing, + logs: [], + autoRun: false, + validReactCode: false, })) | Error(errs) => let msg = Array.join(errs, "; ") @@ -442,13 +471,16 @@ let useCompilerManager = ( versions: ready.versions, errors: [], result: FinalResult.Nothing, + autoRun: ready.autoRun, + validReactCode: ready.validReactCode, + logs: [], })) | Error(errs) => let msg = Array.join(errs, "; ") dispatchError(CompilerLoadingError(msg)) } - | Compiling(ready, (lang, code)) => + | Compiling({targetLang: lang, code, autoRun} as ready) => let apiVersion = ready.selected.apiVersion let instance = ready.selected.instance @@ -478,8 +510,55 @@ let useCompilerManager = ( `Can't handle result of compiler API version "${version}"`, ) } + let ready = {...ready, result: FinalResult.Comp(compResult)} + setState(_ => + switch (ready.result, autoRun) { + | (FinalResult.Comp(Success({js_code})), true) => + Executing({state: ready, jsCode: js_code}) + | _ => Ready(ready) + } + ) + | Executing({state, jsCode}) => + open Babel + + let ast = Parser.parse(jsCode, {sourceType: "module"}) + let {entryPointExists, code, imports} = PlaygroundValidator.validate(ast) + let imports = imports->Dict.mapValues(path => { + let filename = path->String.sliceToEnd(~start=9) // the part after "./stdlib/" + let filename = switch state.selected.id { + | {major: 12, minor: 0, patch: 0, preRelease: Some(Alpha(alpha))} if alpha < 8 => + let filename = if filename->String.startsWith("core__") { + filename->String.sliceToEnd(~start=6) + } else { + filename + } + capitalizeFirstLetter(filename) + | {major} if major < 12 && filename->String.startsWith("core__") => + capitalizeFirstLetter(filename) + | _ => filename + } + let compilerVersion = switch state.selected.id { + | {major: 12, minor: 0, patch: 0, preRelease: Some(Alpha(alpha))} if alpha < 8 => { + Semver.major: 12, + minor: 0, + patch: 0, + preRelease: Some(Alpha(8)), + } + | {major, minor} if (major === 11 && minor < 2) || major < 11 => { + major: 11, + minor: 2, + patch: 0, + preRelease: Some(Beta(1)), + } + | version => version + } + CdnMeta.getStdlibRuntimeUrl(compilerVersion, filename) + }) - setState(_ => Ready({...ready, result: FinalResult.Comp(compResult)})) + entryPointExists + ? code->wrapReactApp->EvalIFrame.sendOutput(imports) + : EvalIFrame.sendOutput(code, imports) + setState(_ => Ready({...state, logs: [], validReactCode: entryPointExists})) | SetupFailed(_) => () | Ready(ready) => let url = createUrl(router.route, ready) @@ -487,9 +566,17 @@ let useCompilerManager = ( } } - updateState()->ignore + updateState()->Promise.done None - }, [state]) + }, ( + state, + dispatchError, + initialVersion, + initialModuleSystem, + initialLang, + versions, + router.route, + )) (state, dispatch) } diff --git a/src/common/CompilerManagerHook.resi b/src/common/CompilerManagerHook.resi index 5e9f8a826..640494160 100644 --- a/src/common/CompilerManagerHook.resi +++ b/src/common/CompilerManagerHook.resi @@ -25,6 +25,9 @@ type ready = { targetLang: Lang.t, errors: array, // For major errors like bundle loading result: FinalResult.t, + autoRun: bool, + validReactCode: bool, + logs: array, } module CdnMeta: { @@ -36,7 +39,8 @@ type state = | SetupFailed(string) | SwitchingCompiler(ready, Semver.t) // (ready, targetId, libraries) | Ready(ready) - | Compiling(ready, (Lang.t, string)) + | Compiling(ready) + | Executing({state: ready, jsCode: string}) type action = | SwitchToCompiler(Semver.t) // id @@ -44,6 +48,8 @@ type action = | Format(string) | CompileCode(Lang.t, string) | UpdateConfig(Config.t) + | AppendLog(ConsolePanel.log) + | ToggleAutoRun let useCompilerManager: ( ~initialVersion: Semver.t=?, From 4b7dc8be46b5d480599218cabb4f469487dc9470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Tsnobiladz=C3=A9?= Date: Tue, 11 Feb 2025 11:25:11 +0100 Subject: [PATCH 2/6] add a 'Run' button --- src/ConsolePanel.res | 2 +- src/Playground.res | 1 + src/RenderPanel.res | 2 +- src/common/CompilerManagerHook.res | 7 +++++++ src/common/CompilerManagerHook.resi | 1 + 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/ConsolePanel.res b/src/ConsolePanel.res index 770cd2094..03861c714 100644 --- a/src/ConsolePanel.res +++ b/src/ConsolePanel.res @@ -26,7 +26,7 @@ let make = (~logs, ~appendLog) => { {switch logs { | [] => React.string( - "Add some 'Console.log' to your code and enable 'Auto-run' to see your logs here.", + "Add some 'Console.log' to your code and click 'Run' or enable 'Auto-run' to see your logs here.", ) | logs => let content = diff --git a/src/Playground.res b/src/Playground.res index 80a8dd61e..dcfc3aa68 100644 --- a/src/Playground.res +++ b/src/Playground.res @@ -1153,6 +1153,7 @@ module ControlPanel = { dispatch(ToggleAutoRun)}> {React.string("Auto-run")} +
diff --git a/src/RenderPanel.res b/src/RenderPanel.res index 15dc46893..961430cf0 100644 --- a/src/RenderPanel.res +++ b/src/RenderPanel.res @@ -5,7 +5,7 @@ let make = (~validReact) => { {validReact ? React.null : React.string( - "Create a React component called 'App' if you want to render it here, then enable 'Auto-run'.", + "Create a React component called 'App' if you want to render it here, then click 'Run' or enable 'Auto-run'.", )}
diff --git a/src/common/CompilerManagerHook.res b/src/common/CompilerManagerHook.res index 41168f463..b52c67bf6 100644 --- a/src/common/CompilerManagerHook.res +++ b/src/common/CompilerManagerHook.res @@ -189,6 +189,7 @@ type action = | UpdateConfig(Config.t) | AppendLog(ConsolePanel.log) | ToggleAutoRun + | RunCode let createUrl = (pathName, ready) => { let params = switch ready.targetLang { @@ -350,6 +351,12 @@ let useCompilerManager = ( | Ready({autoRun: false} as ready) => Compiling({...ready, autoRun: true}) | _ => state } + | RunCode => + switch state { + | Ready({result: Comp(Success({js_code}))} as ready) => + Executing({state: {...ready, autoRun: false}, jsCode: js_code}) + | _ => state + } } ) } diff --git a/src/common/CompilerManagerHook.resi b/src/common/CompilerManagerHook.resi index 640494160..461a45a62 100644 --- a/src/common/CompilerManagerHook.resi +++ b/src/common/CompilerManagerHook.resi @@ -50,6 +50,7 @@ type action = | UpdateConfig(Config.t) | AppendLog(ConsolePanel.log) | ToggleAutoRun + | RunCode let useCompilerManager: ( ~initialVersion: Semver.t=?, From af092fe7163b9f08fb76e9a413737387bef15b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Tsnobiladz=C3=A9?= Date: Tue, 11 Feb 2025 11:48:22 +0100 Subject: [PATCH 3/6] add keyboard shortcut (Meta + E) to execute code --- src/Playground.res | 29 ++++++++++++++++++++++++++++- src/bindings/Webapi.res | 4 ++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Playground.res b/src/Playground.res index dcfc3aa68..7da4e4528 100644 --- a/src/Playground.res +++ b/src/Playground.res @@ -1149,11 +1149,38 @@ module ControlPanel = { | _ => false } + let onKeyDown = event => { + switch ( + event->ReactEvent.Keyboard.metaKey || event->ReactEvent.Keyboard.ctrlKey, + event->ReactEvent.Keyboard.key, + ) { + | (true, "e") => dispatch(RunCode) + | _ => () + } + } + + React.useEffect(() => { + Webapi.Window.addEventListener("keydown", onKeyDown) + Some(() => Webapi.Window.removeEventListener("keydown", onKeyDown)) + }, []) + + let runButtonText = { + let userAgent = Webapi.Window.Navigator.userAgent + let run = "Run" + if userAgent->String.includes("iPhone") || userAgent->String.includes("Android") { + run + } else if userAgent->String.includes("Mac") { + `${run} (⌘ + E)` + } else { + `${run} (Ctrl + E)` + } + } +
dispatch(ToggleAutoRun)}> {React.string("Auto-run")} - +
diff --git a/src/bindings/Webapi.res b/src/bindings/Webapi.res index 55d5eecc8..0cbc09f93 100644 --- a/src/bindings/Webapi.res +++ b/src/bindings/Webapi.res @@ -62,6 +62,10 @@ module Window = { module Location = { @scope(("window", "location")) @val external href: string = "href" } + + module Navigator = { + @scope(("window", "navigator")) @val external userAgent: string = "userAgent" + } } module Fetch = { From d4b9e4dd8eabf8e09bf06941d8240c9f23538b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Tsnobiladz=C3=A9?= Date: Tue, 11 Feb 2025 12:17:49 +0100 Subject: [PATCH 4/6] simplify states --- src/OutputPanel.res | 4 +- src/Playground.res | 101 +++++--------------------- src/bindings/RescriptCompilerApi.res | 10 ++- src/bindings/RescriptCompilerApi.resi | 6 +- src/common/CompilerManagerHook.res | 34 ++++++--- src/common/CompilerManagerHook.resi | 2 +- 6 files changed, 55 insertions(+), 102 deletions(-) diff --git a/src/OutputPanel.res b/src/OutputPanel.res index 14ab5794a..956e51620 100644 --- a/src/OutputPanel.res +++ b/src/OutputPanel.res @@ -2,14 +2,14 @@ let make = (~compilerState, ~appendLog) => { let validReact = switch compilerState { | CompilerManagerHook.Executing({state: {validReactCode: true}}) - | Compiling({validReactCode: true}) + | Compiling({state: {validReactCode: true}}) | Ready({validReactCode: true}) => true | _ => false } let logs = switch compilerState { | CompilerManagerHook.Executing({state: {logs}}) - | Compiling({logs}) + | Compiling({state: {logs}}) | Ready({logs}) => logs | _ => [] } diff --git a/src/Playground.res b/src/Playground.res index 7da4e4528..f5810ce00 100644 --- a/src/Playground.res +++ b/src/Playground.res @@ -1038,20 +1038,6 @@ module Settings = { } module ControlPanel = { - let codeFromResult = (result: FinalResult.t): string => { - open Api - switch result { - | FinalResult.Comp(comp) => - switch comp { - | CompilationResult.Success({js_code}) => js_code - | UnexpectedError(_) - | Unknown(_, _) - | Fail(_) => "/* No JS code generated */" - } - | Nothing - | Conv(_) => "/* No JS code generated */" - } - } module Button = { @react.component let make = (~children, ~onClick=?) => @@ -1144,7 +1130,7 @@ module ControlPanel = { let autoRun = switch state { | CompilerManagerHook.Executing({state: {autoRun: true}}) - | Compiling({autoRun: true}) + | Compiling({state: {autoRun: true}}) | Ready({autoRun: true}) => true | _ => false } @@ -1211,77 +1197,24 @@ module OutputPanel = { ~editorCode: React.ref, ~currentTab: tab, ) => { - /* - We need the prevState to understand different - state transitions, and to be able to keep displaying - old results until those transitions are done. - - Goal was to reduce the UI flickering during different - state transitions - */ - let prevState = React.useRef(None) - - let cmCode = switch prevState.current { - | Some(prev) => - switch (prev, compilerState) { - | (_, Ready({result: Nothing})) => None - | (Ready(prevReady), Ready(ready)) => - switch (prevReady.result, ready.result) { - | (_, Comp(Success(_))) => ControlPanel.codeFromResult(ready.result)->Some - | _ => None - } - | (_, Ready({result: Comp(Success(_)) as result})) => - ControlPanel.codeFromResult(result)->Some - | (Ready({result: Comp(Success(_)) as result}), Compiling(_)) => - ControlPanel.codeFromResult(result)->Some - | (_, Executing({jsCode})) => Some(jsCode) - | _ => None - } - | None => - switch compilerState { - | Ready(ready) => ControlPanel.codeFromResult(ready.result)->Some - | _ => None - } - } - - prevState.current = Some(compilerState) - - let resultPane = switch compilerState { - | Compiling(ready) - | Ready(ready) - | Executing({state: ready}) => - switch ready.result { - | Comp(Success(_)) - | Conv(Success(_)) => React.null - | _ => - - } - - | _ => React.null - } - - let (code, showCm) = switch cmCode { - | None => ("", false) - | Some(code) => (code, true) - } - - let codeElement = -
-        {HighlightJs.renderHLJS(~code, ~darkmode=true, ~lang="js", ())}
-      
- let output =
- resultPane - codeElement + {switch compilerState { + | Compiling({previousJsCode: Some(jsCode)}) + | Executing({jsCode}) + | Ready({result: Comp(Success({jsCode}))}) => +
+            {HighlightJs.renderHLJS(~code=jsCode, ~darkmode=true, ~lang="js", ())}
+          
+ | Ready({result: Conv(Success(_))}) => React.null + | Ready({result, targetLang, selected}) => + + | _ => React.null + }}
let errorPane = switch compilerState { - | Compiling(ready) + | Compiling({state: ready}) | Ready(ready) | Executing({state: ready}) | SwitchingCompiler(ready, _) => @@ -1296,7 +1229,7 @@ module OutputPanel = { let settingsPane = switch compilerState { | Ready(ready) - | Compiling(ready) + | Compiling({state: ready}) | Executing({state: ready}) | SwitchingCompiler(ready, _) => let config = ready.selected.config @@ -1671,8 +1604,8 @@ let make = (~versions: array) => { } let cmHoverHints = switch compilerState { - | Ready({result: FinalResult.Comp(Success({type_hints}))}) => - Array.map(type_hints, hint => { + | Ready({result: FinalResult.Comp(Success({typeHints}))}) => + Array.map(typeHints, hint => { switch hint { | TypeDeclaration({start, end, hint}) | Binding({start, end, hint}) diff --git a/src/bindings/RescriptCompilerApi.res b/src/bindings/RescriptCompilerApi.res index 027da6a3e..5d953825f 100644 --- a/src/bindings/RescriptCompilerApi.res +++ b/src/bindings/RescriptCompilerApi.res @@ -237,18 +237,20 @@ module TypeHint = { module CompileSuccess = { type t = { - js_code: string, + @as("js_code") + jsCode: string, warnings: array, - type_hints: array, + @as("type_hints") + typeHints: array, time: float, // total compilation time } let decode = (~time: float, json): t => { open Json.Decode { - js_code: field("js_code", string, json), + jsCode: field("js_code", string, json), warnings: field("warnings", array(Warning.decode, ...), json), - type_hints: withDefault([], field("type_hints", array(TypeHint.decode, ...), ...), json), + typeHints: withDefault([], field("type_hints", array(TypeHint.decode, ...), ...), json), time, } } diff --git a/src/bindings/RescriptCompilerApi.resi b/src/bindings/RescriptCompilerApi.resi index 4852d197b..de8c069d4 100644 --- a/src/bindings/RescriptCompilerApi.resi +++ b/src/bindings/RescriptCompilerApi.resi @@ -104,9 +104,11 @@ module TypeHint: { module CompileSuccess: { type t = { - js_code: string, + @as("js_code") + jsCode: string, warnings: array, - type_hints: array, // Not supported in older versions <= 9.0.1 (will always be []) + @as("type_hints") + typeHints: array, // Not supported in older versions <= 9.0.1 (will always be []) time: float, // total compilation time } diff --git a/src/common/CompilerManagerHook.res b/src/common/CompilerManagerHook.res index b52c67bf6..3114d434b 100644 --- a/src/common/CompilerManagerHook.res +++ b/src/common/CompilerManagerHook.res @@ -178,7 +178,7 @@ type state = | SetupFailed(string) | SwitchingCompiler(ready, Semver.t) // (ready, targetId, libraries) | Ready(ready) - | Compiling(ready) + | Compiling({state: ready, previousJsCode: option}) | Executing({state: ready, jsCode: string}) type action = @@ -245,12 +245,19 @@ let useCompilerManager = ( | Ready(ready) => ready.selected.instance->Compiler.setConfig(config) let selected = {...ready.selected, config} - Compiling({...ready, selected}) + Compiling({state: {...ready, selected}, previousJsCode: None}) | _ => state } | CompileCode(lang, code) => switch state { - | Ready(ready) => Compiling({...ready, code, targetLang: lang}) + | Ready(ready) => + Compiling({ + state: {...ready, code, targetLang: lang}, + previousJsCode: switch ready.result { + | Comp(Success({jsCode})) => Some(jsCode) + | _ => None + }, + }) | _ => state } | SwitchLanguage({lang, code}) => @@ -348,13 +355,23 @@ let useCompilerManager = ( | ToggleAutoRun => switch state { | Ready({autoRun: true} as ready) => Ready({...ready, autoRun: false}) - | Ready({autoRun: false} as ready) => Compiling({...ready, autoRun: true}) + | Ready({autoRun: false} as ready) => + Compiling({ + state: { + ...ready, + autoRun: true, + }, + previousJsCode: switch ready.result { + | Comp(Success({jsCode})) => Some(jsCode) + | _ => None + }, + }) | _ => state } | RunCode => switch state { - | Ready({result: Comp(Success({js_code}))} as ready) => - Executing({state: {...ready, autoRun: false}, jsCode: js_code}) + | Ready({result: Comp(Success({jsCode}))} as ready) => + Executing({state: {...ready, autoRun: false}, jsCode}) | _ => state } } @@ -487,7 +504,7 @@ let useCompilerManager = ( dispatchError(CompilerLoadingError(msg)) } - | Compiling({targetLang: lang, code, autoRun} as ready) => + | Compiling({state: {targetLang: lang, code, autoRun} as ready}) => let apiVersion = ready.selected.apiVersion let instance = ready.selected.instance @@ -520,8 +537,7 @@ let useCompilerManager = ( let ready = {...ready, result: FinalResult.Comp(compResult)} setState(_ => switch (ready.result, autoRun) { - | (FinalResult.Comp(Success({js_code})), true) => - Executing({state: ready, jsCode: js_code}) + | (FinalResult.Comp(Success({jsCode})), true) => Executing({state: ready, jsCode}) | _ => Ready(ready) } ) diff --git a/src/common/CompilerManagerHook.resi b/src/common/CompilerManagerHook.resi index 461a45a62..113a5c7b7 100644 --- a/src/common/CompilerManagerHook.resi +++ b/src/common/CompilerManagerHook.resi @@ -39,7 +39,7 @@ type state = | SetupFailed(string) | SwitchingCompiler(ready, Semver.t) // (ready, targetId, libraries) | Ready(ready) - | Compiling(ready) + | Compiling({state: ready, previousJsCode: option}) | Executing({state: ready, jsCode: string}) type action = From 9f822931f84dcd47e2a7f66d2d9fd2375252ab35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Tsnobiladz=C3=A9?= Date: Tue, 11 Feb 2025 12:40:15 +0100 Subject: [PATCH 5/6] move to the Output panel when clicking run or auto-run --- src/Playground.res | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Playground.res b/src/Playground.res index f5810ce00..dd7bd535a 100644 --- a/src/Playground.res +++ b/src/Playground.res @@ -1116,6 +1116,7 @@ module ControlPanel = { ~state: CompilerManagerHook.state, ~dispatch: CompilerManagerHook.action => unit, ~editorCode: React.ref, + ~setCurrentTab: (tab => tab) => unit, ) => { let children = switch state { | Init => React.string("Initializing...") @@ -1135,12 +1136,17 @@ module ControlPanel = { | _ => false } + let runCode = () => { + setCurrentTab(_ => Output) + dispatch(RunCode) + } + let onKeyDown = event => { switch ( event->ReactEvent.Keyboard.metaKey || event->ReactEvent.Keyboard.ctrlKey, event->ReactEvent.Keyboard.key, ) { - | (true, "e") => dispatch(RunCode) + | (true, "e") => runCode() | _ => () } } @@ -1163,10 +1169,18 @@ module ControlPanel = { }
- dispatch(ToggleAutoRun)}> + { + switch state { + | Ready({autoRun: false}) => setCurrentTab(_ => Output) + | _ => () + } + dispatch(ToggleAutoRun) + }}> {React.string("Auto-run")} - +
@@ -1671,6 +1685,7 @@ let make = (~versions: array) => { state=compilerState dispatch=compilerDispatch editorCode + setCurrentTab />
Date: Tue, 11 Feb 2025 13:52:06 +0100 Subject: [PATCH 6/6] prevent default on shortcut key down --- src/Playground.res | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Playground.res b/src/Playground.res index dd7bd535a..c506e6c1c 100644 --- a/src/Playground.res +++ b/src/Playground.res @@ -1146,7 +1146,9 @@ module ControlPanel = { event->ReactEvent.Keyboard.metaKey || event->ReactEvent.Keyboard.ctrlKey, event->ReactEvent.Keyboard.key, ) { - | (true, "e") => runCode() + | (true, "e") => + event->ReactEvent.Keyboard.preventDefault + runCode() | _ => () } }