Skip to content

Show the number of errors and warnings in the Problems tab's title #1004

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
May 3, 2025
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d21e5e6
Use Int.toString instead of Belt.Int.toString for ReScript v11+
mediremi May 1, 2025
573b863
Use more padding and set max default content width in React and Conso…
mediremi May 1, 2025
06bf524
Show error and problem counts in 'Problems' tab title
mediremi May 1, 2025
c5e8621
Only show syntax selection if there are at least 2 syntaxes to choose…
mediremi May 1, 2025
fd86598
Update TailwindCSS to v3.4.17 to get extended min-width, max-width, a…
mediremi May 1, 2025
92bb2ea
Set min-width on problem counts
mediremi May 1, 2025
7a4ce15
Only reset warning flags, not other config, when clicking warning fla…
mediremi May 1, 2025
ec20549
Add missing warning flag description
mediremi May 1, 2025
9760eda
Remove warning flags that no longer exist
mediremi May 1, 2025
8553e6b
Improve styles for warnings flags input
mediremi May 1, 2025
39527e5
Remove warnings 61 and 109 from default warn flags
mediremi May 1, 2025
6d80d6c
Refactor ToggleSelection component
mediremi May 1, 2025
cfaa584
Remove outdated TODOs
mediremi May 1, 2025
dad68a1
Remove unused OCaml variant in RescriptCompilerApi.Lang.t
mediremi May 1, 2025
2f7cd58
Remove unused RescriptCompilerApi.Compiler.ocamlCompile
mediremi May 1, 2025
5e74246
Remove warning flag descriptions for flags that no longer exist
mediremi May 1, 2025
2384d4c
Merge branch 'master' into playground-tweaks
mediremi May 1, 2025
7beeea0
Move problem counts to after 'Problems' tab title
mediremi May 2, 2025
0afa602
Restore warnings flags reset value to support older compilers
mediremi May 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/ConsolePanel.res
Original file line number Diff line number Diff line change
@@ -25,9 +25,11 @@ let make = (~logs, ~appendLog) => {
<h2 className="font-bold text-gray-5/50 absolute right-2 top-2"> {React.string("Console")} </h2>
{switch logs {
| [] =>
React.string(
"Add some 'Console.log' to your code and click 'Run' or enable 'Auto-run' to see your logs here.",
)
<p className="p-4 max-w-prose">
{React.string(
"Add some 'Console.log' to your code and click 'Run' or enable 'Auto-run' to see your logs here.",
)}
</p>
| logs =>
let content =
logs
288 changes: 182 additions & 106 deletions src/Playground.res
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
/*
TODO / Idea list:
- Fix issue with Reason where a comment on the last line causes a infinite loop
- Add settings pane to set moduleSystem
- Add advanced mode for enabling OCaml output as well
More advanced tasks:
- Fix syntax convertion issue where comments are stripped on Reason <-> Res convertion
- Try to get Reason's recoverable errors working
*/

%%raw(`
if (typeof window !== "undefined" && typeof window.navigator !== "undefined") {
require("codemirror/mode/javascript/javascript");
@@ -42,6 +31,22 @@ module DropdownSelect = {
}

module ToggleSelection = {
module SelectionOption = {
@react.component
let make = (~label, ~isActive, ~disabled, ~onClick) => {
<button
className={"mr-1 px-2 py-1 rounded inline-block " ++ if isActive {
"bg-fire text-white font-bold"
} else {
"bg-gray-80 opacity-50 hover:opacity-80"
}}
onClick
disabled>
{React.string(label)}
</button>
}
}

@react.component
let make = (
~onChange: 'a => unit,
@@ -58,41 +63,21 @@ module ToggleSelection = {
values
}

let selectedIndex = switch Belt.Array.getIndexBy(values, lang => lang === selected) {
| Some(i) => i
| None => 0
}

let elements = Array.mapWithIndex(values, (value, i) => {
let active = i === selectedIndex ? "bg-fire text-white font-bold" : "bg-gray-80 opacity-50"
let label = toLabel(value)

let onMouseDown = evt => {
ReactEvent.Mouse.preventDefault(evt)
ReactEvent.Mouse.stopPropagation(evt)

if i !== selectedIndex {
switch values[i] {
| Some(value) => onChange(value)
| None => ()
<div className={(disabled ? "opacity-25" : "") ++ "flex w-full"}>
{values
->Array.map(value => {
let label = toLabel(value)
let isActive = value === selected
let onClick = _event => {
if !isActive {
onChange(value)
}
}
}

// Required for iOS Safari 12
let onClick = _ => ()

<button
disabled
onMouseDown
onClick
key=label
className={"mr-1 px-2 py-1 rounded inline-block " ++ active}>
{React.string(label)}
</button>
})

<div className={(disabled ? "opacity-25" : "") ++ "flex w-full"}> {React.array(elements)} </div>
<SelectionOption key={label} label isActive onClick disabled />
})
->React.array}
</div>
}
}

@@ -697,9 +682,9 @@ module WarningFlagsWidget = {
})
->React.array
->Some
| Typing(typing) =>
| Typing(typing) if typing.suggestion != NoSuggestion =>
let suggestions = switch typing.suggestion {
| NoSuggestion => React.string("Type + / - followed by a number or letter (e.g. +a+1)")
| NoSuggestion => React.null
| ErrorSuggestion(msg) => React.string(msg)
| FuzzySuggestions({precedingTokens, selected, results, modifier}) =>
Array.mapWithIndex(results, ((flag, desc), i) => {
@@ -753,7 +738,8 @@ module WarningFlagsWidget = {
})->React.array
}
Some(suggestions)
| HideSuggestion(_) => None

| Typing(_) | HideSuggestion(_) => None
}

let suggestionBox =
@@ -793,20 +779,12 @@ module WarningFlagsWidget = {
| []
| [{enabled: false, flag: "a"}] => React.null
| _ =>
let onMouseDown = evt => {
ReactEvent.Mouse.preventDefault(evt)
let onClick = _evt => {
onUpdate([{WarningFlagDescription.Parser.enabled: false, flag: "a"}])
}

// For iOS12 compat
let onClick = _ => ()
let onFocus = evt => {
ReactEvent.Focus.preventDefault(evt)
ReactEvent.Focus.stopPropagation(evt)
}

<button
onMouseDown
title="Clear all flags"
onClick
onFocus
tabIndex=0
@@ -837,17 +815,22 @@ module WarningFlagsWidget = {
<div className={"flex justify-between border p-2 " ++ activeClass}>
<div>
chips
<input
ref={ReactDOM.Ref.domRef(inputRef)}
className="outline-none bg-gray-90 placeholder-gray-20 placeholder-opacity-50"
placeholder="Flags"
type_="text"
tabIndex=0
value=inputValue
onChange
onFocus
onBlur
/>
<section className="mt-3">
<input
ref={ReactDOM.Ref.domRef(inputRef)}
className="inline-block p-1 max-w-20 outline-none bg-gray-90 placeholder-gray-20 placeholder-opacity-50"
placeholder="Flags"
type_="text"
tabIndex=0
value=inputValue
onChange
onFocus
onBlur
/>
<p className="mt-1 text-12">
{React.string("Type + / - followed by a number or letter (e.g. +a+1)")}
</p>
</section>
</div>
deleteButton
</div>
@@ -891,23 +874,11 @@ module Settings = {

let warnFlagTokens = WarningFlagDescription.Parser.parse(warn_flags)->Result.getOr([])

let onResetClick = evt => {
ReactEvent.Mouse.preventDefault(evt)

let open_modules = switch readyState.selected.apiVersion {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is gone? We still need this (for v11 at least).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed this callback to onWarningFlagsResetClick to make it clearer that this is triggered by clicking on [reset] in the 'Warnings flags' section of the settings panel.

Copy link
Contributor Author

@mediremi mediremi May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the open_modules update part of this function as:
i) there's no way for users to configure open modules (at least as far as I can tell)
ii) the updating of open_modules on compiler load and switch is actually managed by the useEffect in CompilerManagerHook - this was just duplicated logic from CompilerManagerHook.getOpenModules

| V1 | V2 | V3 | UnknownVersion(_) => None
| V4 | V5 =>
readyState.selected.libraries->Array.some(el => el === "@rescript/core")
? Some(["RescriptCore"])
: None
}

let defaultConfig = {
Api.Config.module_system: "nodejs",
let onWarningFlagsResetClick = _evt => {
setConfig({
...config,
warn_flags: "+a-4-9-20-40-41-42-50-61-102-109",
?open_modules,
}
setConfig(defaultConfig)
})
}

let onCompilerSelect = id => dispatch(SwitchToCompiler(id))
@@ -995,15 +966,19 @@ module Settings = {
}
</DropdownSelect>
</div>
<div className="mt-6">
<div className=titleClass> {React.string("Syntax")} </div>
<ToggleSelection
values=availableTargetLangs
toLabel={lang => lang->Api.Lang.toExt->String.toUpperCase}
selected=readyState.targetLang
onChange=onTargetLangSelect
/>
</div>
{if availableTargetLangs->Array.length > 1 {
<div className="mt-6">
<div className=titleClass> {React.string("Syntax")} </div>
<ToggleSelection
values=availableTargetLangs
toLabel={lang => lang->Api.Lang.toExt->String.toUpperCase}
selected=readyState.targetLang
onChange=onTargetLangSelect
/>
</div>
} else {
React.null
}}
<div className="mt-6">
<div className=titleClass> {React.string("Module-System")} </div>
<ToggleSelection
@@ -1024,7 +999,8 @@ module Settings = {
<div className="mt-8">
<div className=titleClass>
{React.string("Warning Flags")}
<button onMouseDown=onResetClick className={"ml-6 text-12 " ++ Text.Link.standalone}>
<button
onClick=onWarningFlagsResetClick className={"ml-6 text-12 " ++ Text.Link.standalone}>
{React.string("[reset]")}
</button>
</div>
@@ -1334,6 +1310,56 @@ module InitialContent = {
}
}
module App = {
@react.component
let make = () => {
let (count, setCount) = React.useState(() => 0)
let (username, setUsername) = React.useState(() => "Anonymous")
<div>
{React.string("Username: ")}
<input
type_="text"
value={username}
onChange={event => {
event->ReactEvent.Form.preventDefault
let eventTarget = event->ReactEvent.Form.target
let username = eventTarget["value"]
setUsername(_prev => username)
}}
/>
<button
onClick={_evt => {
setCount(prev => prev + 1)
}}>
{React.string("Click me")}
</button>
<button onClick={_evt => setCount(_ => 0)}> {React.string("Reset")} </button>
<CounterMessage count username />
</div>
}
}
`

let since_11 = `module CounterMessage = {
@react.component
let make = (~count, ~username=?) => {
let times = switch count {
| 1 => "once"
| 2 => "twice"
| n => Int.toString(n) ++ " times"
}
let name = switch username {
| Some("") => "Anonymous"
| Some(name) => name
| None => "Anonymous"
}
<div> {React.string(\`Hello \$\{name\}, you clicked me \` ++ times)} </div>
}
}
module App = {
@react.component
let make = () => {
@@ -1412,11 +1438,10 @@ let make = (~versions: array<string>) => {
let initialContent = switch (Dict.get(router.query, "code"), initialLang) {
| (Some(compressedCode), _) => LzString.decompressToEncodedURIComponent(compressedCode)
| (None, Reason) => initialReContent
| (None, Res)
| (None, _) =>
| (None, Res) =>
switch initialVersion {
| Some({major: 10, minor}) if minor >= 1 => InitialContent.since_10_1
| Some({major}) if major > 10 => InitialContent.since_10_1
| Some({major}) if major > 10 => InitialContent.since_11
| _ => InitialContent.original
}
}
@@ -1661,23 +1686,74 @@ let make = (~versions: array<string>) => {

let headers = Array.mapWithIndex(tabs, (tab, i) => {
let title = switch tab {
| Output => "Output"
| JavaScript => "JavaScript"
| Problems => "Problems"
| Settings => "Settings"
| Output => "Output"->React.string

| JavaScript => "JavaScript"->React.string

| Problems => {
let problemCounts: {"warnings": int, "errors": int} = switch compilerState {
| Compiling({state: ready})
| Ready(ready)
| Executing({state: ready})
| SwitchingCompiler(ready, _) => {
"warnings": switch ready.result {
| Comp(Success({warnings})) => warnings->Array.length
| _ => 0
},
"errors": switch ready.result {
| FinalResult.Comp(Fail(result)) =>
switch result {
| SyntaxErr(errors) | TypecheckErr(errors) | OtherErr(errors) => errors->Array.length
| WarningErr(errors) => errors->Array.length
| WarningFlagErr(_) => 1
}
| Conv(Fail({details})) => details->Array.length
| Comp(Success(_)) => 0
| Conv(Success(_)) => 0
| Comp(UnexpectedError(_))
| Conv(UnexpectedError(_))
| Comp(Unknown(_))
| Conv(Unknown(_)) => 1
| Nothing => 0
},
}

| SetupFailed(_) | Init => {
"warnings": 0,
"errors": 0,
}
}

<div className="inline-flex items-center gap-2">
{"Problems"->React.string}
{if problemCounts["errors"] > 0 {
<span className="inline-block min-w-4 text-fire bg-fire-100 px-0.5">
{problemCounts["errors"]->React.int}
</span>
} else {
React.null
}}
{if problemCounts["warnings"] > 0 {
<span className="inline-block min-w-4 text-orange bg-orange-15 px-0.5">
{problemCounts["warnings"]->React.int}
</span>
} else {
React.null
}}
</div>
}

| Settings => "Settings"->React.string
}
let onMouseDown = evt => {

let onClick = evt => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using onClick instead of onMouseDown makes the tabs keyboard accessible - buttons with a click handler can be triggered with Enter/Space.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might have been an optimisation so that event.preventDefault() gets invoked faster. Or maybe it was deliberate that it does not get focused.

@ryyppy do you still remember?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there was most likely a good reason. I think it had something to do with touch event delay. Ultimately one should consider adapting a accessibility component lib like react-aria to have a fully accessible tab bar etc.

ReactEvent.Mouse.preventDefault(evt)
ReactEvent.Mouse.stopPropagation(evt)
setCurrentTab(_ => tab)
}
let active = currentTab === tab
// For Safari iOS12
let onClick = _ => ()
let className = makeTabClass(active)
<button key={Int.toString(i) ++ ("-" ++ title)} onMouseDown onClick className disabled>
{React.string(title)}
</button>
<button key={Int.toString(i)} onClick className disabled> {title} </button>
})

<main className={"flex flex-col bg-gray-100 overflow-hidden"}>
8 changes: 5 additions & 3 deletions src/RenderPanel.res
Original file line number Diff line number Diff line change
@@ -4,9 +4,11 @@ let make = (~validReact) => {
<h2 className="font-bold text-gray-5/50 absolute right-2 top-2"> {React.string("React")} </h2>
{validReact
? React.null
: React.string(
"Create a React component called 'App' if you want to render it here, then click 'Run' or enable 'Auto-run'.",
)}
: <p className="p-4 max-w-prose">
{React.string(
"Create a React component called 'App' if you want to render it here, then click 'Run' or enable 'Auto-run'.",
)}
</p>}
<div className={validReact ? "h-full" : "h-0"}>
<EvalIFrame />
</div>
15 changes: 0 additions & 15 deletions src/bindings/RescriptCompilerApi.res
Original file line number Diff line number Diff line change
@@ -3,27 +3,23 @@
module Lang = {
type t =
| Reason
| OCaml
| Res

let toString = t =>
switch t {
| Res => "ReScript"
| Reason => "Reason"
| OCaml => "OCaml"
}

let toExt = t =>
switch t {
| Res => "res"
| Reason => "re"
| OCaml => "ml"
}

let decode = (json): t => {
open! Json.Decode
switch string(json) {
| "ml" => OCaml
| "re" => Reason
| "res" => Res
| other => raise(DecodeError(`Unknown language "${other}"`))
@@ -419,17 +415,6 @@ module Compiler = {
}
}

@send @scope("ocaml")
external ocamlCompile: (t, string) => JSON.t = "compile"

let ocamlCompile = (t, code): CompilationResult.t => {
let startTime = now()
let json = ocamlCompile(t, code)
let stopTime = now()

CompilationResult.decode(~time=stopTime -. startTime, json)
}

@send external getConfig: t => Config.t = "getConfig"

@send external setFilename: (t, string) => bool = "setFilename"
2 changes: 0 additions & 2 deletions src/bindings/RescriptCompilerApi.resi
Original file line number Diff line number Diff line change
@@ -9,7 +9,6 @@
module Lang: {
type t =
| Reason
| OCaml
| Res

let toString: t => string
@@ -192,7 +191,6 @@ module Compiler: {
* OCaml compiler actions (Note: no pretty print available for OCaml)
*/
let ocamlVersion: t => option<string>
let ocamlCompile: (t, string) => CompilationResult.t

/*
* Config setter / getters
3 changes: 0 additions & 3 deletions src/common/CompilerManagerHook.res
Original file line number Diff line number Diff line change
@@ -320,7 +320,6 @@ let useCompilerManager = (
let convResult = switch ready.targetLang {
| Res => instance->Compiler.resFormat(code)->Some
| Reason => instance->Compiler.reasonFormat(code)->Some
| _ => None
}

let result = switch convResult {
@@ -511,13 +510,11 @@ let useCompilerManager = (
let compResult = switch apiVersion {
| V1 =>
switch lang {
| Lang.OCaml => instance->Compiler.ocamlCompile(code)
| Lang.Reason => instance->Compiler.reasonCompile(code)
| Lang.Res => instance->Compiler.resCompile(code)
}
| V2 | V3 | V4 =>
switch lang {
| Lang.OCaml => instance->Compiler.ocamlCompile(code)
| Lang.Reason =>
CompilationResult.UnexpectedError(
`Reason not supported with API version "${apiVersion->RescriptCompilerApi.Version.toString}"`,
3 changes: 1 addition & 2 deletions src/common/WarningFlagDescription.res
Original file line number Diff line number Diff line change
@@ -7,7 +7,6 @@ let numeric = [
"Fragile pattern matching: matching that will remain complete even if additional constructors are added to one of the variant types matched.",
),
(5, "Partially applied function: expression whose result has function type and is ignored."),
(6, "Label omitted in function application."),
(8, "Partial match: missing cases in pattern-matching."),
(9, "Missing fields in a record pattern."),
(
@@ -41,6 +40,7 @@ let numeric = [
(37, "Unused constructor."),
(38, "Unused extension constructor."),
(39, "Unused rec flag."),
(41, "Ambiguous constructor or label name."),
(43, "Nonoptional label applied as optional."),
(44, "Open statement shadows an already defined identifier."),
(45, "Open statement shadows an already defined label or constructor."),
@@ -56,7 +56,6 @@ let numeric = [
(57, "Ambiguous or-pattern variables under guard"),
(59, "Assignment to non-mutable value"),
(60, "Unused module declaration"),
(61, "Unboxable type in primitive declaration"),
(62, "Type constraint on GADT type declaration"),
(101, "Unused bs attributes"),
(102, "Polymorphic comparison introduced (maybe unsafe)"),