Skip to content

Commit

Permalink
Support localStorage persistence
Browse files Browse the repository at this point in the history
  • Loading branch information
albertprz committed Apr 28, 2024
1 parent b359457 commit 9a3bd19
Show file tree
Hide file tree
Showing 39 changed files with 728 additions and 261 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,21 @@ for use for enterprise, personal & educational purposes.

- [✔️] Provide a no frills, minimalistic GUI supporting all basic spreadsheet functionality regarding navigation, cell management, formula evaluation & automatic cell updates.

- [✔️] Develop a high level pure functional dynamic formula language interpreted at the browser, with expresiveness similar to the term level language in Haskell or Purescript, albeit with familiar syntax and idioms to popular spreadsheet applications and mainstream languages.
- [✔️] Develop a high level pure functional dynamic formula language interpreted at the browser, that supports currying, patterns and guards with expresiveness similar to the term level language in Haskell or Purescript, albeit with familiar syntax and idioms to popular spreadsheet applications and mainstream languages.

- [✔️] Implement a prelude library with commonly used functions and combinators, loaded at startup.

- [✔️] Support formula edition with syntax highlighting and function signatures for the current function at the cursor.

- [✔️] Support IDE like autocomplete for imported and module aliased top-level functions and operators.

- [✔️] Expose an Explorer view to query by function name, signature or a input / output example (in a similar fashion to Hoogle / Pursuit) and see global functions & operators along with their documentation on a per module basis
- [✔️] Expose an Explorer view to query by function name, signature or a input / output example (in a similar fashion to Hoogle / Pursuit) and see global functions & operators along with their documentation on a per module basis.

- Add the capability for a user to create, modify or delete global functions & operators through the Explorer view.
- [✔️] Support Go to documentation on the formula editor for modules, functions & operators.

- [✔️] Persist user defined formulas, spreadsheet data, functions & operators in the browser local storage.

- Add the capability for a user to create, modify or delete global functions & operators through an auxiliary modal in the Explorer view.

- Enable the use of formulas for filtering & sorting rows.

Expand Down
5 changes: 4 additions & 1 deletion spago.dhall
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{ name = "puresheet"
, dependencies =
[ "aff"
, "argonaut"
, "argonaut-codecs"
, "argonaut-generic"
, "arrays"
, "bifunctors"
, "bookhound"
Expand Down Expand Up @@ -34,7 +37,6 @@
, "partial"
, "point-free"
, "prelude"
, "profunctor"
, "psci-support"
, "record"
, "record-extra"
Expand All @@ -55,6 +57,7 @@
, "web-dom"
, "web-events"
, "web-html"
, "web-storage"
, "web-uievents"
]
, packages = ./packages.dhall
Expand Down
104 changes: 99 additions & 5 deletions src/AppStore.purs
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,38 @@ module App.AppStore where
import FatPrelude
import Prim hiding (Row)

import App.Components.Spreadsheet.Cell (Cell, CellValue)
import App.Components.Spreadsheet.Formula (Formula, FormulaId)
import App.Evaluator.Common (LocalFormulaCtx)
import App.Interpreter.Module (reloadModule)
import App.SyntaxTree.Common (Module, QVar, QVarOp, preludeModule)
import App.SyntaxTree.FnDef (FnInfo, OpInfo)
import Data.Argonaut.Decode (fromJsonString)
import Data.Argonaut.Encode (toJsonString)
import Data.HashMap as HashMap
import Data.Set as Set
import Data.Set.NonEmpty as NonEmptySet
import Data.Tree.Zipper (fromTree)
import Effect.Console (log)
import Effect.Console as Logger
import Foreign (readArray, readString, unsafeToForeign)
import Foreign.Index ((!))
import Record (merge)
import Record.Extra (pick)
import Web.HTML (window)
import Web.HTML.Window (localStorage)
import Web.Storage.Storage as Storage

type StoreRow =
( fnsMap :: HashMap QVar FnInfo
, operatorsMap :: HashMap QVarOp OpInfo
, aliasedModulesMap :: HashMap (Module /\ Module) (Set Module)
, importedModulesMap :: HashMap Module (Set Module)
, modules :: Set Module
, tableData :: HashMap Cell CellValue
, tableFormulas :: HashMap Cell FormulaId
, tableDependencies :: HashMap Cell (NonEmptySet FormulaId)
, formulaCache :: HashMap FormulaId Formula
)

type Store = Record StoreRow
Expand All @@ -35,25 +48,32 @@ emptyStore =
, aliasedModulesMap: HashMap.empty
, importedModulesMap: HashMap.empty
, modules: Set.empty
, tableData: HashMap.empty
, tableFormulas: HashMap.empty
, tableDependencies: HashMap.empty
, formulaCache: HashMap.empty
}

reduce :: Store -> StoreAction -> Store
reduce store k = k store

mkLocalContext :: Store -> LocalFormulaCtx
mkLocalContext store =
merge store
{ tableData: HashMap.empty
pick $ merge store
{ module': preludeModule
, localFnsMap: HashMap.empty
, argsMap: HashMap.empty
, module': preludeModule
, scope: zero
, scopeLoc: fromTree $ mkLeaf zero
, lambdaCount: zero
}

getInitialStore
:: forall m. MonadEffect m => m Store
getCachedStore :: forall m. MonadEffect m => m Store
getCachedStore = do
store <- getFromLocalStorage
flip fromMaybe store <$> getInitialStore

getInitialStore :: forall m. MonadEffect m => m Store
getInitialStore = liftEffect do
loadedModules <- getLoadedModules
(errors /\ newStore) <- runStateT (traverse reloadModule loadedModules)
Expand All @@ -73,3 +93,77 @@ getInitialStore = liftEffect do
=<< (_ ! "loadedModules")
=<< unsafeToForeign
<$> liftEffect window

getFromLocalStorage :: forall m. MonadEffect m => m (Maybe Store)
getFromLocalStorage = liftEffect do
storage <- localStorage =<< window
map (deserializeStore =<< _) $ Storage.getItem storageKey storage

persistInLocalStorage :: forall m. MonadEffect m => Store -> m Unit
persistInLocalStorage store = liftEffect do
storage <- localStorage =<< window
Storage.setItem storageKey (serializeStore store) storage

serializeStore :: Store -> String
serializeStore store = toJsonString
{ fnsMap: HashMap.toArrayBy Tuple $ store.fnsMap
, operatorsMap: HashMap.toArrayBy Tuple $ store.operatorsMap
, aliasedModulesMap: HashMap.toArrayBy Tuple $ store.aliasedModulesMap
, importedModulesMap: HashMap.toArrayBy Tuple $ store.importedModulesMap
, modules: store.modules
, tableData: HashMap.toArrayBy Tuple $ store.tableData
, tableFormulas: HashMap.toArrayBy Tuple $ store.tableFormulas
, tableDependencies: HashMap.toArrayBy Tuple $ map Set.fromFoldable $
store.tableDependencies
, formulaCache: HashMap.toArrayBy Tuple
$ map (\x -> merge { affectedCells: Set.fromFoldable x.affectedCells } x)
store.formulaCache
}

deserializeStore :: String -> Maybe Store
deserializeStore = hush <<< map go <<< fromJsonString
where
go :: SerialStore -> Store
go obj =
{ fnsMap: HashMap.fromArray obj.fnsMap
, operatorsMap: HashMap.fromArray obj.operatorsMap
, aliasedModulesMap: HashMap.fromArray obj.aliasedModulesMap
, importedModulesMap: HashMap.fromArray obj.importedModulesMap
, modules: obj.modules
, tableData: HashMap.fromArray obj.tableData
, tableFormulas: HashMap.fromArray obj.tableFormulas
, tableDependencies: map (unsafeFromJust <<< NonEmptySet.fromSet)
$ HashMap.fromArray obj.tableDependencies
, formulaCache:
map
( \x ->
{ affectedCells:
unsafeFromJust $ NonEmptySet.fromSet x.affectedCells
, formulaText: x.formulaText
, startingCell: x.startingCell
}
)
$ HashMap.fromArray obj.formulaCache
}

type SerialStore =
{ fnsMap :: Array (QVar /\ FnInfo)
, operatorsMap :: Array (QVarOp /\ OpInfo)
, aliasedModulesMap :: Array ((Module /\ Module) /\ (Set Module))
, importedModulesMap :: Array (Module /\ (Set Module))
, modules :: Set Module
, tableData :: Array (Cell /\ CellValue)
, tableFormulas :: Array (Cell /\ FormulaId)
, tableDependencies :: Array (Cell /\ (Set FormulaId))
, formulaCache ::
Array
( FormulaId /\
{ formulaText :: String
, affectedCells :: Set Cell
, startingCell :: Cell
}
)
}

storageKey :: String
storageKey = "puresheetTableState"
3 changes: 3 additions & 0 deletions src/CSS/ClassNames.purs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ operatorSyntax = ClassName "operator-syntax"
functionSyntax :: ClassName
functionSyntax = ClassName "function-syntax"

moduleSyntax :: ClassName
moduleSyntax = ClassName "module-syntax"

regularSyntax :: ClassName
regularSyntax = ClassName "regular-syntax"

Expand Down
9 changes: 7 additions & 2 deletions src/CSS/Editor.purs
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,16 @@ css = do

span &. functionSyntax ? Rule.do
color := blue
cursor := pointer

span &. symbolSyntax ? Rule.do
span &. operatorSyntax ? Rule.do
color := red
cursor := pointer

span &. operatorSyntax ? Rule.do
span &. moduleSyntax ? Rule.do
cursor := pointer

span &. symbolSyntax ? Rule.do
color := red

span &. cellSyntax ? Rule.do
Expand Down
46 changes: 40 additions & 6 deletions src/Components/Application.purs
Original file line number Diff line number Diff line change
@@ -1,41 +1,75 @@
module App.Components.Application where

import FatPrelude
import FatPrelude hiding (div)

import App.AppM (AppM)
import App.AppStore (Store, persistInLocalStorage)
import App.CSS.Application as Application
import App.Components.Explorer (_explorer)
import App.Components.Explorer as Explorer
import App.Components.HeaderMenu (_headerMenu)
import App.Components.HeaderMenu as HeaderMenu
import App.Components.Spreadsheet (_spreadsheet)
import App.Components.Spreadsheet as Spreadsheet
import App.Routes (Route(..), useRouter')
import App.Routes (Route(..), lastRoute, nextRoute, useRouter')
import App.Utils.Dom (withPrevent)
import App.Utils.Event (ctrlKey)
import App.Utils.KeyCode (KeyCode(..), mkKeyAction)
import Halogen (Component)
import Halogen.HTML (div_, slot_)
import Halogen.Hooks (useLifecycleEffect, useTickEffect)
import Halogen.HTML (div, slot_)
import Halogen.HTML.Events (onKeyDown)
import Halogen.Hooks (HookM, useLifecycleEffect, useTickEffect)
import Halogen.Hooks as Hooks
import Halogen.Query (SubscriptionId)
import Halogen.Query.Event (eventListener)
import Halogen.Store.Monad (class MonadStore, getStore)
import Tecton.Halogen (styleSheet)
import Web.DOM.Element (setAttribute)
import Web.Event.Event (EventType(..))
import Web.HTML (window)
import Web.HTML as HTML
import Web.HTML.HTMLDocument as Document
import Web.HTML.HTMLElement (toElement)
import Web.HTML.Window as Window

component :: forall q. Component q Unit Unit AppM
component = Hooks.component \_ _ -> Hooks.do
route /\ { navigate } <- useRouter'
useLifecycleEffect (Nothing <$ navigate SpreadsheetView)

useLifecycleEffect do
navigate SpreadsheetView
subscriptionId <- subscribeWindowUnload
pure $ Just $ Hooks.unsubscribe subscriptionId

Hooks.captures { route } useTickEffect do
toogleOverflow route *> mempty
let
handleKeyDown keyCode ev
| ctrlKey ev && keyCode == CharKeyCode 'J' =
withPrevent ev $ navigate $ nextRoute route
| ctrlKey ev && keyCode == CharKeyCode 'K' =
withPrevent ev $ navigate $ lastRoute route
| otherwise = pure unit
Hooks.pure do
div_
div
[ onKeyDown $ mkKeyAction handleKeyDown ]

[ styleSheet Application.css
, slot_ _headerMenu unit HeaderMenu.component unit
, slot_ _spreadsheet unit Spreadsheet.component { route }
, slot_ _explorer unit Explorer.component { route }
]

subscribeWindowUnload
:: forall m a. MonadEffect m => MonadStore a Store m => HookM m SubscriptionId
subscribeWindowUnload = do
window <- liftEffect HTML.window
Hooks.subscribe do
eventListener
(EventType "beforeunload")
(Window.toEventTarget window)
(const $ Just (persistInLocalStorage =<< getStore))

toogleOverflow :: forall m. MonadEffect m => Route -> m Unit
toogleOverflow route = liftEffect do
body <- Document.body =<< Window.document =<< window
Expand Down
25 changes: 18 additions & 7 deletions src/Components/Editor/Handler.purs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import FatPrelude

import App.AppM (AppM)
import App.CSS.Ids (formulaCellInputId)
import App.Components.Editor.HandlerHelpers (displayFnSig, displayFnSuggestions, getEditorContent, insertEditorNewLine, performAutoComplete, performSyntaxHighlight, subscribeSelectionChange, updateEditorContent)
import App.Components.Editor.HandlerHelpers (displayFnSig, displayFnSuggestions, getEditorContent, getTermAtCaret, insertEditorNewLine, performAutoComplete, performSyntaxHighlight, subscribeSelectionChange, updateEditorContent)
import App.Components.Editor.Models (EditorAction(..), EditorOutput(..), EditorQuery(..), EditorState)
import App.Components.Spreadsheet.Formula (FormulaState(..))
import App.Routes (Route(..))
import App.Utils.Dom (focusById, withPrevent)
import App.Utils.Event (ctrlKey, shiftKey, toEvent)
import App.Utils.KeyCode (KeyCode(..), isModifierKeyCode)
import App.Utils.Selection as Selection
import Data.Array as Array
import Halogen (HalogenM, raise)
import Halogen.Router.Class (navigate)
import Halogen.Store.Monad (getStore)
import Web.Event.Event (target)
import Web.HTML (window)
Expand Down Expand Up @@ -58,13 +60,22 @@ handleAction (KeyDown _ Tab ev) =
handleAction (KeyDown _ (CharKeyCode 'G') ev)
| ctrlKey ev = withPrevent ev $ focusById formulaCellInputId

handleAction (KeyDown _ (CharKeyCode 'D') ev)
| ctrlKey ev = withPrevent ev do
traverse_ (navigate <<< ExplorerView <<< { selectedTerm: _ } <<< Just)
=<< getTermAtCaret

handleAction (KeyDown _ _ _) =
modify_ _ { formulaState = UnknownFormula }

handleAction (KeyUp keyCode _) =
unless (isModifierKeyCode keyCode)
performSyntaxHighlight

handleAction (MouseDown ev) = when (ctrlKey ev) do
traverse_ (navigate <<< ExplorerView <<< { selectedTerm: _ } <<< Just)
=<< getTermAtCaret

handleAction (FocusIn ev) = do
selection <- liftEffect $ Selection.getSelection =<< window
liftEffect $ traverse_ (Selection.moveToEnd selection) formulaBox
Expand All @@ -73,18 +84,18 @@ handleAction (FocusIn ev) = do
formulaBox =
toNode <$> (fromEventTarget =<< target (toEvent ev))

handleAction SelectionChange = do
st <- get
store <- getStore
displayFnSuggestions
displayFnSig st store

handleAction (ClickSuggestion suggestion ev) =
withPrevent ev $ traverse_ performAutoComplete suggestion

handleAction (HoverSuggestion suggestionId _) =
modify_ _ { selectedSuggestionId = suggestionId }

handleAction SelectionChange = do
st <- get
store <- getStore
displayFnSuggestions
displayFnSig st store

handleAction Initialize =
subscribeSelectionChange

Expand Down
Loading

0 comments on commit 9a3bd19

Please sign in to comment.