blob: 6d123c1f259e4f316ff88c25c7a51aa7b052ae4e [file] [log] [blame]
-- Copyright 2020 The Fuchsia Authors. All rights reserved.
-- Use of this source code is governed by a BSD-style license that can be
-- found in the LICENSE file.
module Editors exposing
( Editor(..)
, Model
, Msg
, decode
, encode
, init
, initCmd
, subscriptions
, update
, view
)
{-| This module implements the input/output editors.
Input and output editors are not independent. Anything to do with an output mode
-- its active/nonactive state, its content, its options -- is always relative to
an input mode. For example, FIDL Text -> Bytes and Bytes -> Bytes have distinct
output state even though both are Bytes.
This module does not keep track of the Ace editor state. That is done in
JavaScript-land because it's easier there.
-}
import Form exposing (Form)
import Html exposing (Html)
import Html.Attributes as Attributes
import Html.Events as Events
import Json.Decode as Decode
import Json.Encode as Encode
import Mode exposing (Mode)
import News
import Ports
------------- MODEL ------------------------------------------------------------
type alias Model =
{ activeInput : Mode
, inputMap : Mode.Map InputState
, fidlSyntax : FidlSyntax
}
type alias InputState =
{ activeOutput : Mode
, outputMap : Mode.Map OutputState
}
type alias OutputState =
{ options : Options
}
type alias Options =
Form.Model
type alias Active =
{ input : Mode
, output : Mode
, options : Options
}
type FidlSyntax
= UnknownSyntax
| OldSyntax
init : Model
init =
let
input =
Mode.defaultInput
inputState =
defaultInputState input
in
{ activeInput = input
, inputMap = Mode.singleton input inputState
, fidlSyntax = UnknownSyntax
}
initCmd : Model -> Cmd msg
initCmd model =
updateEditors (getActive model)
defaultInputState : Mode -> InputState
defaultInputState input =
let
output =
Mode.defaultOutput input
outputState =
defaultOutputState input output
in
{ activeOutput = output
, outputMap = Mode.singleton output outputState
}
defaultOutputState : Mode -> Mode -> OutputState
defaultOutputState input output =
{ options = Form.empty }
getInputState : Mode -> Model -> InputState
getInputState input model =
Maybe.withDefault
(defaultInputState input)
(Mode.get input model.inputMap)
getOutputState : Mode -> Mode -> InputState -> OutputState
getOutputState input output inputState =
Maybe.withDefault
(defaultOutputState input output)
(Mode.get output inputState.outputMap)
getActive : Model -> Active
getActive model =
let
input =
model.activeInput
inputState =
getInputState input model
output =
inputState.activeOutput
outputState =
getOutputState input output inputState
options =
outputState.options
in
Active input output options
------------- UPDATE -----------------------------------------------------------
type Msg
= NoOp
| -- Switch to this input mode. This also implies switching to a new output
-- mode (the one that was most recently used for that input).
ActivateInput Mode
-- Switch to this output mode, keeping the same input mode.
| ActivateOutput Mode
-- Update options for the current input/output mode combination.
| UpdateOptions Form.Msg
-- Assume FIDL input is using the given syntax.
| AssumeSyntax FidlSyntax
-- Tell the evaluator to convert to the new FIDL syntax.
| ConvertSyntax
update : Msg -> Model -> ( Model, Cmd msg )
update msg model =
let
newModel =
case msg of
NoOp ->
model
ActivateInput input ->
activateInput input model
ActivateOutput output ->
activateOutput output model
UpdateOptions formMsg ->
updateOptions (Form.update formMsg) model
AssumeSyntax syntax ->
{ model | fidlSyntax = syntax }
ConvertSyntax ->
model
cmd =
case msg of
NoOp ->
Cmd.none
AssumeSyntax _ ->
Cmd.none
ConvertSyntax ->
Ports.convertToNewSyntax
_ ->
updateEditors (getActive newModel)
in
( newModel, cmd )
activateInput : Mode -> Model -> Model
activateInput input model =
let
inputState =
getInputState input model
newInputMap =
model.inputMap |> Mode.insert input inputState
in
{ model | activeInput = input, inputMap = newInputMap }
activateOutput : Mode -> Model -> Model
activateOutput output model =
let
input =
model.activeInput
inputState =
getInputState input model
outputState =
getOutputState input output inputState
newOutputMap =
inputState.outputMap |> Mode.insert output outputState
newInputState =
{ inputState | activeOutput = output, outputMap = newOutputMap }
newInputMap =
model.inputMap |> Mode.insert input newInputState
in
{ model | inputMap = newInputMap }
updateOptions : (Options -> Options) -> Model -> Model
updateOptions alter model =
let
input =
model.activeInput
inputState =
getInputState input model
output =
inputState.activeOutput
outputState =
getOutputState input output inputState
newOutputState =
{ options = alter outputState.options }
newOutputMap =
inputState.outputMap |> Mode.insert output newOutputState
newInputState =
{ inputState | outputMap = newOutputMap }
newInputMap =
model.inputMap |> Mode.insert input newInputState
in
{ model | inputMap = newInputMap }
updateEditors : Active -> Cmd msg
updateEditors active =
Ports.updateEditors
(Mode.encode active.input)
(Mode.encode active.output)
(Form.encodeEffective
(Mode.options active.input active.output)
active.options
)
------------- SUBSCRIPTIONS ----------------------------------------------------
subscriptions : Sub Msg
subscriptions =
Ports.syntaxDetected NoOp (Decode.map AssumeSyntax decodeFidlSyntax)
------------- VIEW -------------------------------------------------------------
type Editor
= Input
| Output
view : Editor -> (Msg -> msg) -> String -> Model -> Html msg
view editor toMsg themeClass model =
let
active =
getActive model
{ modes, activeMode, msg, id, optionsPart } =
case editor of
Input ->
{ modes = Mode.inputs
, activeMode = active.input
, msg = ActivateInput
, id = "InputEditor"
, optionsPart =
case ( active.input, model.fidlSyntax ) of
( Mode.Fidl, OldSyntax ) ->
[ convertToNewSyntaxView toMsg ]
_ ->
[]
}
Output ->
let
form =
Mode.options active.input active.output
in
{ modes = Mode.outputs active.input
, activeMode = active.output
, msg = ActivateOutput
, id = "OutputEditor"
, optionsPart =
if List.isEmpty form.inputs then
[]
else
[ optionsView form active.options toMsg ]
}
navbarPart =
navbarView modes activeMode (toMsg << msg) themeClass
editorPart =
Html.div
[ Attributes.id id
, Attributes.class "editor"
]
[]
in
Html.div
[ Attributes.class "editor-wrapper"
, Events.on "keydown" (keyDownEvent editor active toMsg)
]
(navbarPart :: editorPart :: optionsPart)
navbarView : List Mode -> Mode -> (Mode -> msg) -> String -> Html msg
navbarView modes activeMode msg themeClass =
let
navItem mode =
-- We provide keyboard shortcuts on the parent div to navigate
-- between modes. This replaces tab navigation, hence tabindex -1.
-- We need to use button tags so that they can be focused and the
-- parent div will receive keyboard events.
Html.button
[ Attributes.type_ "button"
, Attributes.tabindex -1
, Attributes.class "navbar-item"
, if mode == activeMode then
Attributes.class ("navbar-item--active " ++ themeClass)
else
Events.onClick (msg mode)
]
[ Html.text (Mode.title mode) ]
in
Html.div
[ Attributes.class "navbar" ]
(List.map navItem modes)
optionsView : Form -> Form.Model -> (Msg -> msg) -> Html msg
optionsView form options toMsg =
Html.div
[ Attributes.class "editor-options" ]
[ Form.view
form
[]
(toMsg << UpdateOptions)
options
]
convertToNewSyntaxView : (Msg -> msg) -> Html msg
convertToNewSyntaxView toMsg =
Html.div
[ Attributes.class "editor-options" ]
[ Html.div [ Attributes.class "form-control" ]
[ Html.text "Looks like you are using the "
, Html.a
[ Attributes.href News.newSyntax2021Url ]
[ Html.text "old FIDL syntax" ]
, Html.text "."
, Html.button
[ Attributes.class "button form-button"
, Events.onClick (toMsg ConvertSyntax)
]
[ Html.text "Convert to new syntax" ]
]
]
keyDownEvent : Editor -> Active -> (Msg -> msg) -> Decode.Decoder msg
keyDownEvent editor active toMsg =
let
( activate, previous, next ) =
case editor of
Input ->
( ActivateInput
, Mode.previousInput active.input
, Mode.nextInput active.input
)
Output ->
( ActivateOutput
, Mode.previousOutput active.input active.output
, Mode.nextOutput active.input active.output
)
chooseMsg ctrl key =
if ctrl && key == "[" then
activate previous
else if ctrl && key == "]" then
activate next
else
NoOp
in
Decode.map toMsg <|
Decode.map2 chooseMsg
(Decode.field "ctrlKey" Decode.bool)
(Decode.field "key" Decode.string)
------------- ENCODE / DECODE --------------------------------------------------
encode : Model -> Encode.Value
encode model =
Encode.object
[ ( "activeInput", Mode.encode model.activeInput )
, ( "inputMap", Mode.encodeMap encodeInputState model.inputMap )
]
encodeInputState : InputState -> Encode.Value
encodeInputState inputState =
Encode.object
[ ( "activeOutput", Mode.encode inputState.activeOutput )
, ( "outputMap", Mode.encodeMap encodeOutputState inputState.outputMap )
]
encodeOutputState : OutputState -> Encode.Value
encodeOutputState outputState =
Encode.object
[ ( "options", Form.encode outputState.options ) ]
decode : Decode.Decoder Model
decode =
Decode.map3 Model
(Decode.field "activeInput" Mode.decode)
(Decode.field "inputMap" (Mode.decodeMap decodeInputState))
(Decode.succeed UnknownSyntax)
decodeInputState : Decode.Decoder InputState
decodeInputState =
Decode.map2 InputState
(Decode.field "activeOutput" Mode.decode)
(Decode.field "outputMap" (Mode.decodeMap decodeOutputState))
decodeOutputState : Decode.Decoder OutputState
decodeOutputState =
Decode.map OutputState
(Decode.field "options" Form.decode)
decodeFidlSyntax : Decode.Decoder FidlSyntax
decodeFidlSyntax =
Decode.string
|> Decode.andThen
(\string ->
case string of
"unknown" ->
Decode.succeed UnknownSyntax
"old" ->
Decode.succeed OldSyntax
_ ->
Decode.fail ("Invalid syntax name: " ++ string)
)