rasa (empty) → 0.1.0.0
raw patch · 15 files changed
+1170/−0 lines, 15 filesdep +asyncdep +basedep +containerssetup-changed
Dependencies added: async, base, containers, data-default, lens, mtl, text, text-lens, transformers, yi-rope
Files
- LICENSE +21/−0
- README.md +115/−0
- Setup.hs +2/−0
- rasa.cabal +81/−0
- src/Rasa.hs +51/−0
- src/Rasa/Ext.hs +162/−0
- src/Rasa/Internal/Action.hs +55/−0
- src/Rasa/Internal/Buffer.hs +48/−0
- src/Rasa/Internal/Directive.hs +91/−0
- src/Rasa/Internal/Editor.hs +143/−0
- src/Rasa/Internal/Events.hs +47/−0
- src/Rasa/Internal/Extensions.hs +16/−0
- src/Rasa/Internal/Range.hs +194/−0
- src/Rasa/Internal/Scheduler.hs +121/−0
- src/Rasa/Internal/Text.hs +23/−0
+ LICENSE view
@@ -0,0 +1,21 @@+The MIT License++Copyright (c) 2016 Chris Penner++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.
+ README.md view
@@ -0,0 +1,115 @@+Rasa (Rah-zah)+==============++[](https://gitter.im/rasa-editor/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)++Embarrassingly modular customizable text editor built in Haskell.++What people are saying+----------------------++> Excessively Modular! - some bald guy++> I'm glad I'm unemployed so I have time to configure it! - my mate Steve++> You should go outside one of these days. - Mother++Installation+------------++At the moment you must build Rasa from source;++1. Install [stack](http://seanhess.github.io/2015/08/04/practical-haskell-getting-started.html)+2. Clone this repo and `cd` into the directory+3. Run `stack build && stack exec rasa` (you may want to alias this to `rasa`)++Getting started+---------------++Here's a great guide on building a copy-paste extension from scratch! I definitely+recommend checking it out!++### [Building Your First Extension](docs/Building-An-Extension.md)++\^ That guide will walk you through installation and getting running! Once+you're running rasa you can experiment with creating your own adaptations. You+should customize your keymap to add a few mappings you like. It's a short step+from here to developing your own extensions. `Action`s like you'd use in an+extension can be registered to hooks in your `Main.hs`. You can build and+experiment with entire extensions in your config file and extract them as a+package when you're ready, kind of like a vimrc file. Again, just read the +extension guide, it covers what you need to know!++If you have any issues (and I'm sure there'll be a few; it's a new project!)+please report them [here](https://github.com/ChrisPenner/rasa/issues).+++Core Principles+---------------++Rasa is meant to be about as modular as an editor can be. The goal is for as+much code as possible to be extracted into composable extensions. If the core+editing facilities can't be implemented as extensions, then the extension+interface isn't powerful enough. I've taken this to its extreme, for instance+the following features are implemented as rasa extensions that anyone in the+community could have written.++- Loading and saving files +- Key bindings+- Multiple cursors+- Rendering the editor to the terminal++This approach has some unique pros and cons:++### Pros++- Implementing most core functionality as extensions ensures a powerful and+ elegant extension interface.+- Flexibility; don't like the default cursor implementation? Write your own!+- Adaptability; the core of Rasa is miniscule, you can mix and match+ extensions to build any editor you want.+++### Cons++- Module cross-dependencies makes the community infrastructure more fragile;+ We'll likely have to develop a solution to this as a community as time+ goes on.+- Fragmentation; Not having a single implementation for a given feature means+ extensions that depend on a feature have to pick a specific implementation+ to augment. Over time data-structures and types will be standardized into+ Rasa's core to help alleviate this.+++Core Features+-------------++As stated above, the editor itself focuses primarily on easy extensibility, so it doesn't have a lot of editing+features built in, instead it focuses on standardizing a good extension API.+We focus on creating a simple system so people can pick it up quickly.++Here are some features of that API:++### Event Hook System++All actions in the editor are triggered via an event/listener system.+Extensions may subscribe to events from the editor, or from another extension+and perform an action in response. The Event which triggered the hook is+available as an argument). Extensions may also dispatch any kind of event at+any time which other extensions may listen for.++### Actions/BufActions++Extensions define things that they'd like to do using a powerful set of+functions which they can embed in an `Action`. Within an action an extension+may perform IO, access the available buffers, store and access extension state,+and edit text.++Contributing+============++Things are moving quickly, but I'd love a hand! You can get a rough idea of+where you can help out at the+[Roadmap](https://github.com/ChrisPenner/rasa/issues/2), feel free to leave a+comment there asking any questions, I'm often free to chat, join our [gitter+here](https://gitter.im/rasa-editor/Lobby)!
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ rasa.cabal view
@@ -0,0 +1,81 @@+name: rasa+version: 0.1.0.0+synopsis: A modular text editor+homepage: https://github.com/ChrisPenner/rasa#readme+license: MIT+license-file: LICENSE+author: Chris Penner+maintainer: christopher.penner@gmail.com+copyright: 2016 Chris Penner+category: Text Editor, Executable+build-type: Simple+extra-source-files: README.md+cabal-version: >=1.10+description: A modular text editor+ This is only a snippet, see the project's <https://github.com/ChrisPenner/rasa README>.+ .+ Rasa is a text editor project with a few interesting goals. For better or worse it attempts+ to be as modular as possible. This means that most functionality which would typically be considered to be 'core' in other editors+ is implemented as extensions in Rasa.+ This approach comes with its own share of pros and cons, for instance:+ .+ /Pros/+ .+ * Implementing most core functionality as extensions ensures a powerful and elegant extension interface.+ * Flexibility; don't like the default cursor implementation? Write your own!+ * Adaptability; the core of Rasa is miniscule, you can mix and match extensions to build any editor you want.+ .+ /Cons/+ .+ * Module cross-dependencies makes the community infrastructure more fragile; We'll likely have to develop a solution to this as a community as time goes on.+ * Fragmentation; Not having a single implementation for a given feature means extensions that depend on a feature have to pick a specific implementation to augment. Over time data-structures and types will be standardized into Rasa's core to help alleviate this.+ .+ While highly experimental, I've found the current API to be quite expressive and adaptable; + for instance I was able to implement the notion of multiple cursors using the extension API in less than a day. I hope to keep the learning curve low as development continues.+ .+ /Getting Started/+ .+ First clone the <https://github.com/ChrisPenner/rasa Github repo>+ and try running the example-config included there. + Once you get it running (see the <https://github.com/ChrisPenner/rasa/blob/master/README.md README>) then you+ can customize your keymap to add a few mappings you like. + Then I'd check out the + <https://github.com/ChrisPenner/rasa/blob/master/docs/Building-An-Extension.md Building your own extensions guide>.+ It goes in depth into everything you'd want to know!+ .+ If you have any issues (and I'm sure there'll be a few; it's a new project!) please report them <https://github.com/ChrisPenner/rasa/issues here> and we'll talk about it!++library+ hs-source-dirs: src+ exposed-modules:+ Rasa++ Rasa.Ext++ Rasa.Internal.Action+ Rasa.Internal.Buffer+ Rasa.Internal.Directive+ Rasa.Internal.Editor+ Rasa.Internal.Events+ Rasa.Internal.Extensions+ Rasa.Internal.Range+ Rasa.Internal.Scheduler+ Rasa.Internal.Text++ ghc-options: -Wall+ build-depends: base >= 4.7 && < 5+ , async+ , containers+ , data-default+ , lens+ , mtl+ , text+ , text-lens+ , transformers+ , yi-rope++ default-language: Haskell2010++source-repository head+ type: git+ location: https://github.com/ChrisPenner/rasa
+ src/Rasa.hs view
@@ -0,0 +1,51 @@+{-# language ExistentialQuantification, Rank2Types, ScopedTypeVariables #-}+module Rasa (rasa) where++import Rasa.Internal.Editor+import Rasa.Internal.Action+import Rasa.Internal.Events+import Rasa.Internal.Scheduler++import Control.Lens+import Control.Concurrent.Async+import Control.Monad+import Control.Monad.State+import Control.Monad.Reader+import Data.Default (def)+import Data.Foldable++-- | The main function to run rasa.+-- +-- @rasa eventProviders extensions@+-- +-- This should be imported by a user-config and called with extension event providers and extension event hooks as+-- arguments. e.g.:+-- +-- > rasa [slateEvent] $ do+-- > cursor+-- > vim+rasa :: [Action [Keypress]] -> Scheduler () -> IO ()+rasa eventProviders scheduler =+ evalAction def hooks $ do+ dispatchEvent Init+ eventLoop eventProviders+ dispatchEvent Exit+ where hooks = getHooks scheduler++-- | This is the main event loop, it runs recursively forever until something+-- sets 'Rasa.Editor.exiting'. It runs the pre-event hooks, then listens for an+-- event from the event providers, then runs the post event hooks and repeats.+eventLoop :: [Action [Keypress]] -> Action ()+eventLoop eventProviders = do+ dispatchEvent BeforeRender+ dispatchEvent OnRender+ dispatchEvent AfterRender+ dispatchEvent BeforeEvent+ currentEditor <- get+ hooks <- ask+ -- This is a little weird, but I think it needs to be this way to execute providers in parallel+ asyncEventProviders <- liftIO $ traverse (async.evalAction currentEditor hooks) eventProviders+ (_, nextEvents) <- liftIO $ waitAny asyncEventProviders+ traverse_ dispatchEvent nextEvents+ isExiting <- use exiting+ unless isExiting $ eventLoop eventProviders
+ src/Rasa/Ext.hs view
@@ -0,0 +1,162 @@+{-# LANGUAGE Rank2Types, FlexibleContexts #-}++----------------------------------------------------------------------------+-- |+-- Module : Rasa.Ext+-- Copyright : (C) 2016 Chris Penner+-- License : MIT+-- Maintainer : Chris Penner <christopher.penner@gmail.com>+--+-- This module contains the public API for building an extension for Rasa. It+-- re-exports the parts of rasa that are public API for creating extensions.+--+-- There are two main things that an extension can do, either react+-- to editor events, or expose useful actions and/or state for other extensions+-- to use.+--+-- To react to events an extension defines a 'Scheduler'+-- which the user puts in their config file. The 'Scheduler'+-- defines listeners for events which the extension will react to.+-- See 'Scheduler' for more detailed information.+--+-- Whether performing its own actions or being used by a different extension+-- an extension will want to define some 'Action's to perform. Actions+-- can operate over buffers or even perform IO and comprise the main way in which+-- extensons do what they need to do. Read more here: 'Action', 'BufAction'.+--+-- To sum it all up, Here's an example of a simple logging extension that+-- simply writes each event to a file.+--+-- > logger :: Scheduler ()+-- > logger = do+-- > onInit $ liftIO $ writeFile "logs" "==Logs==\n"+-- > onEvent $ do+-- > evts <- use events+-- > mapM_ (liftIO . appendFile "logs" . (++ "\n") . show) evts+-- > onExit $ liftIO $ appendFile "logs" "==Done=="+----------------------------------------------------------------------------++module Rasa.Ext+ (+ -- * Editor Actions+ Action+ , exit+ , addBuffer+ , addBufferThen+ , nextBuf+ , prevBuf++ -- * Buffer Actions+ , BufAction+ , bufDo+ , focusDo+ , overRange+ , replaceRange+ , deleteRange+ , insertAt+ , sizeOf++ -- * Persisting Extension State+ -- | Extension states for ALL the extensions installed are stored in the same+ -- map keyed by their 'Data.Typeable.TypeRep' so if more than one extension+ -- uses the same type then they'll conflict. This is easily solved by simply+ -- using a newtype around any types which other extensions may use (your own+ -- custom types don't need to be wrapped). For example if your extension stores+ -- a counter as an Int, wrap it in your own custom Counter newtype when storing+ -- it.+ --+ -- Because Extension states are stored by their TypeRep, they must define an+ -- instance of Typeable, luckily GHC can derive this for you.+ --+ -- It is also required for all extension states to define an instance of+ -- 'Data.Default.Default', this is because accessing an extension which has not+ -- yet been stored will result in the default value.+ --+ -- If there's no default value that makes sense for your type, you can define+ -- a default of 'Data.Maybe.Nothing' and pattern match on its value when you+ -- access it.+ --+ -- Extensions may store state persistently for later access or for other+ -- extensions to access. Because Rasa can't possibly know the types of the+ -- state that extensions will store it uses a clever workaround wherein+ -- extension states are stored in a map of 'Data.Typeable.TypeRep' -> Ext+ -- which is coerced into the proper type when it's extracted. The interface to+ -- extract or alter a given extension is to use the 'ext' and 'bufExt' lenses.+ -- Simply use them as though they were lenses to an object of your type and+ -- it'll all work out.+ --+ -- Since it's polymorphic, if ghc can't figure out the type the result is+ -- supposed to be then you'll need to help it out. In practice you won't+ -- typically need to do this unless you're doing something complicated.++ , ext+ , bufExt++ -- * Accessing/Editing Context+ , Buffer+ , text+ -- | A lens over the buffer's 'Data.Text.Text'. Use within a 'BufAction' as+ --+ -- > txt <- use text++ , exiting+ -- | A lens over the current 'exit' status of the editor, allows an extension to+ -- signal the editor to shutdown. If this is set the current events will finish processing, then the+ --+ -- 'Rasa.Ext.Events.Exit' event will be dispatched, then the editor will exit.+ -- Use within an 'Action'+ --+ -- > exiting .= True++ -- * Events+ , Keypress(..)+ , Mod(..)++ -- * Dealing with events+ , Scheduler+ , Hooks+ , Hook+ , dispatchEvent+ , eventListener++ -- * Built-in Event Hooks+ , onInit+ , beforeEvent+ , beforeRender+ , onRender+ , afterRender+ , onExit++ -- * Ranges+ , Range(..)+ , Coord(..)+ , Offset(..)+ , Span(..)+ , combineSpans+ , asCoord+ , clampCoord+ , clampRange+ , range+ , sizeOfR+ , afterC+ , beforeC+ , moveRange+ , moveRangeByN+ , moveCursorByN++ -- * Useful Utilities+ , asText+ , asString+ , asLines+ , rope+ , clamp+ ) where++import Rasa.Internal.Action+import Rasa.Internal.Directive+import Rasa.Internal.Editor+import Rasa.Internal.Events+import Rasa.Internal.Buffer+import Rasa.Internal.Text+import Rasa.Internal.Range+import Rasa.Internal.Scheduler
+ src/Rasa/Internal/Action.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE GeneralizedNewtypeDeriving, ExistentialQuantification #-}++module Rasa.Internal.Action where++import Control.Monad.State+import Control.Monad.Reader+import Data.Dynamic+import Data.Map++import Rasa.Internal.Buffer+import Rasa.Internal.Editor++-- | This is a monad-transformer stack for performing actions against the editor.+-- You register Actions to be run in response to events using 'Rasa.Internal.Scheduler.eventListener'+--+-- Within an Action you can:+--+-- * Use liftIO for IO+-- * Access/edit extensions that are stored globally, see 'ext'+-- * Embed any 'Action's exported other extensions+-- * Embed buffer actions using 'Rasa.Internal.Ext.Directive.bufDo' and 'Rasa.Internal.Ext.Directive.focusDo'+-- * Add\/Edit\/Focus buffers and a few other Editor-level things, see the 'Rasa.Internal.Ext.Directive' module.++newtype Action a = Action+ { runAct :: StateT Editor (ReaderT Hooks IO) a+ } deriving (Functor, Applicative, Monad, MonadState Editor, MonadReader Hooks, MonadIO)++-- | Unwrap and execute an Action (returning the editor state)+execAction :: Editor -> Hooks -> Action () -> IO Editor+execAction editor hooks action = flip runReaderT hooks $ execStateT (runAct action) editor++-- | Unwrap and evaluate an Action (returning the value)+evalAction :: Editor -> Hooks -> Action a ->IO a+evalAction editor hooks action = flip runReaderT hooks $ evalStateT (runAct action) editor++-- | This is a monad-transformer stack for performing actions on a specific buffer.+-- You register BufActions to be run by embedding them in a scheduled 'Action' via 'bufferDo' or 'focusDo'+--+-- Within a BufAction you can:+--+-- * Use liftIO for IO+-- * Access/edit buffer extensions; see 'bufExt'+-- * Embed and sequence any 'BufAction's from other extensions+-- * Access/Edit the buffer's 'text'+--+newtype BufAction a = BufAction+ { getBufAction::StateT Buffer (ReaderT Hooks IO) a+ } deriving (Functor, Applicative, Monad, MonadState Buffer, MonadReader Hooks, MonadIO)+++-- | A wrapper around event listeners so they can be stored in 'Hooks'.+data Hook = forall a. Hook a++-- | A map of Event types to a list of listeners for that event+type Hooks = Map TypeRep [Hook]
+ src/Rasa/Internal/Buffer.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE Rank2Types, TemplateHaskell, OverloadedStrings, ExistentialQuantification, ScopedTypeVariables,+ GeneralizedNewtypeDeriving, FlexibleInstances,+ StandaloneDeriving #-}++module Rasa.Internal.Buffer+ ( Buffer+ , Ext(..)+ , bufExts+ -- | A lens over the extensions stored for a buffer+ , text+ , rope+ -- | A lens over the text stored in a buffer (as a 'Y.YiString')+ , newBuffer+ ) where++import Rasa.Internal.Text+import Rasa.Internal.Extensions++import qualified Data.Text as T+import qualified Yi.Rope as Y+import Control.Lens hiding (matching)+import Data.Map++-- | A buffer, holds the text in the buffer and any extension states that are set on the buffer.+-- A buffer is the State of the 'Rasa.Internal.Action.BufAction' monad transformer stack,+-- so the type may be useful in defining lenses over your extension states.+data Buffer = Buffer+ { _rope :: Y.YiString+ , _bufExts :: ExtMap+ }++makeLenses ''Buffer++-- | A lens into the text of the given buffer. Use within a BufAction.+text :: Lens' Buffer T.Text+text = rope.asText++instance Show Buffer where+ show b = "<Buffer {text:" ++ show (b^..text . to (T.take 30)) ++ "...,\n"+ ++ "exts: " ++ show (b^.bufExts) ++ "}>\n"++-- | Creates a new buffer from the givven text.+newBuffer :: T.Text -> Buffer+newBuffer txt =+ Buffer+ { _rope = Y.fromText txt+ , _bufExts = empty+ }
+ src/Rasa/Internal/Directive.hs view
@@ -0,0 +1,91 @@+{-# language Rank2Types, OverloadedStrings #-}+module Rasa.Internal.Directive+ (+ -- * Performing Actions on Buffers+ bufDo+ , focusDo++ -- * Editor Actions+ , exit+ , addBuffer+ , addBufferThen+ , nextBuf+ , prevBuf++ -- * Buffer Actions+ , overRange+ , replaceRange+ , deleteRange+ , insertAt+ , sizeOf+ ) where++import Rasa.Internal.Text+import Rasa.Internal.Editor+import Rasa.Internal.Action+import Rasa.Internal.Range+import Rasa.Internal.Buffer as B++import Control.Lens+import qualified Data.Text as T++-- | This lifts a 'Rasa.Action.BufAction' to an 'Rasa.Action.Action' which+-- performs the 'Rasa.Action.BufAction' on the focused buffer.++focusDo :: BufAction a -> Action a+focusDo = Action . zoom focusedBuf . getBufAction++-- | This lifts a 'Rasa.Action.BufAction' to an 'Rasa.Action.Action' which+-- performs the 'Rasa.Action.BufAction' on every buffer and collects the return+-- values via 'mappend'++bufDo :: Monoid a => BufAction a -> Action a+bufDo = Action . zoom (buffers . traverse) . getBufAction++-- | This adds a new buffer with the given text.++addBuffer :: T.Text -> Action ()+addBuffer txt = buffers <>= [newBuffer txt]++-- | This adds a new buffer with the given text then performs the given+-- 'Rasa.Action.BufAction' agains that buffer.+addBufferThen :: T.Text -> BufAction a -> Action a+addBufferThen txt act = do+ buffers <>= [newBuffer txt]+ fmap (!! 0) . Action . zoom (buffers._last) . fmap (:[]) . getBufAction $ act++-- | This signals to the editor that you'd like to shutdown. The current events+-- will finish processing, then the 'Rasa.Ext.Scheduler.onExit' hook will run,+-- then the editor will exit.++exit :: Action ()+exit = exiting .= True++-- | Switches focus to the next buffer+nextBuf :: Action ()+nextBuf = do+ numBuffers <- use (buffers.to length)+ focused %= (`mod` numBuffers) . (+1)++-- | Switches focus to the previous buffer+prevBuf :: Action ()+prevBuf = do+ numBuffers <- use (buffers.to length)+ focused %= (`mod` numBuffers) . subtract 1++-- | Deletes the text in the given range from the buffer.+deleteRange :: Range -> BufAction ()+deleteRange r = range r.asText .= ""++-- | Replaces the text in the given range from the buffer.+replaceRange :: Range -> T.Text -> BufAction ()+replaceRange r txt = range r.asText .= txt++-- | Inserts text into the buffer at the given Coord.+insertAt :: Coord -> T.Text -> BufAction ()+insertAt c txt = range (Range c c).asText .= txt++-- | Runs the given function over the text in the range, replacing it with the results.+overRange :: Range -> (T.Text -> T.Text) -> BufAction ()+overRange r f = range r.asText %= f+
+ src/Rasa/Internal/Editor.hs view
@@ -0,0 +1,143 @@+{-# LANGUAGE TemplateHaskell, Rank2Types,+ ExistentialQuantification, ScopedTypeVariables,+ OverloadedStrings+ #-}++module Rasa.Internal.Editor+ (+ -- * Accessing/Storing state+ Editor++ , focused+-- | 'focused' is the index of the currently selected buffer (this will likely change)++ , buffers++-- |'buffers' is a lens into all buffers.++ , exiting++-- | 'exiting' Whether the editor is in the process of exiting. Can be set inside an 'Rasa.Internal.Action.Action':+--+-- > exiting .= True++ , ext+ , allBufExt+ , bufExt+ , focusedBuf+ , range+ ) where++import Rasa.Internal.Buffer+import Rasa.Internal.Extensions+import Rasa.Internal.Range++import Unsafe.Coerce+import Data.Dynamic+import Data.Default+import Data.Maybe+import Control.Lens+import qualified Yi.Rope as Y++-- | This is the primary state of the editor.+data Editor = Editor+ { _buffers :: [Buffer]+ , _focused :: Int+ , _exiting :: Bool+ , _extState :: ExtMap+ }+makeLenses ''Editor++instance Show Editor where+ show editor =+ "Buffers==============\n" ++ show (editor^.buffers) ++ "\n\n"+ ++ "Editor Extensions==============\n" ++ show (editor^.extState) ++ "\n\n"+ ++ "---\n\n"+++instance Default Editor where+ def =+ Editor+ { _extState = def+ , _buffers=fmap newBuffer [ "Buffer 0\nHey! How's it going over there?\nI'm having just a splended time!\nAnother line for you sir?"+ , "Buffer 1\nHey! How's it going over there?\nI'm having just a splended time!\nAnother line for you sir?" ]+ , _focused=0+ , _exiting=False+ }++-- | A lens over the extensions of all buffers.+-- This is useful for setting defaults or altering extension state across all buffers.+allBufExt+ :: forall a.+ (Show a, Typeable a)+ => Traversal' Editor (Maybe a)+allBufExt =+ buffers . traverse . bufExts . at (typeRep (Proxy :: Proxy a)) . mapping coerce+ where+ coerce = iso (\(Ext x) -> unsafeCoerce x) Ext++-- | 'bufExt' is a lens which will focus a given extension's state within a+-- buffer (within a 'Data.Action.BufAction'). The lens will automagically focus+-- the required extension by using type inference. It's a little bit of magic,+-- if you treat the focus as a member of your extension state it should just+-- work out.+--+-- This lens falls back on the extension's 'Data.Default.Default' instance (when getting) if+-- nothing has yet been stored.++bufExt+ :: forall a.+ (Show a, Typeable a, Default a)+ => Lens' Buffer a+bufExt = lens getter setter+ where+ getter buf =+ fromMaybe def $ buf ^. bufExts . at (typeRep (Proxy :: Proxy a)) .+ mapping coerce+ setter buf new =+ set+ (bufExts . at (typeRep (Proxy :: Proxy a)) . mapping coerce)+ (Just new)+ buf+ coerce = iso (\(Ext x) -> unsafeCoerce x) Ext++-- | 'ext' is a lens which will focus the extension state that matches the type+-- inferred as the focal point. It's a little bit of magic, if you treat the+-- focus as a member of your extension state it should just work out.+--+-- This lens falls back on the extension's 'Data.Default.Default' instance (when getting) if+-- nothing has yet been stored.++ext+ :: forall a.+ (Show a, Typeable a, Default a)+ => Lens' Editor a+ext = lens getter setter+ where+ getter editor =+ fromMaybe def $ editor ^. extState . at (typeRep (Proxy :: Proxy a)) .+ mapping coerce+ setter editor new =+ set+ (extState . at (typeRep (Proxy :: Proxy a)) . mapping coerce)+ (Just new)+ editor+ coerce = iso (\(Ext x) -> unsafeCoerce x) Ext++-- | 'focusedBuf' is a lens which focuses the currently selected buffer.+focusedBuf :: Lens' Editor Buffer+focusedBuf = lens getter setter+ where+ getter editor =+ let foc = editor ^. focused+ in editor ^?! buffers . ix foc+ setter editor new =+ let foc = editor ^. focused+ in editor & buffers . ix foc .~ new++-- | A lens over text which is encompassed by a 'Range'+range :: Range -> Lens' Buffer Y.YiString+range (Range start end) = lens getter setter+ where getter = view (rope . beforeC end . afterC start)+ setter old new = old & rope . beforeC end . afterC start .~ new+
+ src/Rasa/Internal/Events.hs view
@@ -0,0 +1,47 @@+{-# language ExistentialQuantification #-}+module Rasa.Internal.Events where++import Data.Dynamic++-- | The Event type represents a common denominator for all actions that could+-- occur Event transmitters express events that have occured as a member of this+-- type. At the moment it's quite sparse, but it will expand as new types of+-- events are needed.++-- | This event is dispatched exactly once when the editor starts up.+data Init = Init deriving (Show, Eq, Typeable)++-- | This event is dispatched immediately before dispatching any events from+-- asyncronous event listeners (like 'Keypress's)+data BeforeEvent = BeforeEvent deriving (Show, Eq, Typeable)++-- | This event is dispatched immediately before dispatching+-- the 'OnRender' event.+data BeforeRender = BeforeRender deriving (Show, Eq, Typeable)++-- | This event is dispatched when it's time for extensions to render to screen.+data OnRender = OnRender deriving (Show, Eq, Typeable)++-- | This event is dispatched immediately after dispatching 'OnRender'.+data AfterRender = AfterRender deriving (Show, Eq, Typeable)++-- | This event is dispatched before exiting the editor, listen for this to do+-- any clean-up (saving files, etc.)+data Exit = Exit deriving (Show, Eq, Typeable)++-- | This event is dispatched in response to keyboard key presses. It contains both+-- the char that was pressed and any modifiers ('Mod') that where held when the key was pressed.+data Keypress+ = Keypress Char+ [Mod]+ | Esc+ | BS+ | Enter+ deriving (Show, Eq, Typeable)++-- | This represents each modifier key that could be pressed along with a key.+data Mod+ = Ctrl+ | Alt+ | Shift+ deriving (Show, Eq)
+ src/Rasa/Internal/Extensions.hs view
@@ -0,0 +1,16 @@+{-# language ExistentialQuantification, ScopedTypeVariables #-}+module Rasa.Internal.Extensions+ ( Ext(..)+ , ExtMap+ ) where++import Data.Map+import Data.Dynamic++-- | A wrapper around an extension of any type so it can be stored in an 'ExtMap'+data Ext = forall a. Show a => Ext a+instance Show Ext where+ show (Ext a) = show a++-- | A map of extension types to their current value.+type ExtMap = Map TypeRep Ext
+ src/Rasa/Internal/Range.hs view
@@ -0,0 +1,194 @@+{-# LANGUAGE Rank2Types, OverloadedStrings, DeriveFunctor, ScopedTypeVariables #-}+module Rasa.Internal.Range+ ( Coord(..)+ , Offset(..)+ , asCoord+ , clampCoord+ , clampRange+ , Range(..)+ , sizeOf+ , sizeOfR+ , moveRange+ , moveRangeByN+ , moveCursorByN+ , moveCursor+ , Span(..)+ , combineSpans+ , clamp+ , beforeC+ , afterC+ ) where++import Control.Lens+import Data.Maybe+import Data.Monoid+import Data.List+import Rasa.Internal.Text+import qualified Yi.Rope as Y+++-- | This represents a range between two coordinates ('Coord')+data Range =+ Range Coord Coord+ deriving (Show, Eq)++instance Ord Range where+ Range start end <= Range start' end' + | end == end' = start <= start'+ | otherwise = end <= end'++-- | (Coord Row Column) represents a char in a block of text. (zero indexed)+-- e.g. Coord 0 0 is the first character in the text,+-- Coord 2 1 is the second character of the third row+data Coord =+ Coord Int+ Int+ deriving (Show, Eq)++instance Ord Coord where+ Coord a b <= Coord a' b'+ | a < a' = True+ | a > a' = False+ | otherwise = b <= b'++-- | An 'Offset' represents an exact position in a file as a number of characters from the start.+newtype Offset =+ Offset Int+ deriving (Show, Eq)++-- | Moves a 'Range' by a given 'Coord'+-- It may be unintuitive, but for (Coord row col) a given range will be moved down by row and to the right by col.+moveRange :: Coord -> Range -> Range+moveRange amt (Range start end) =+ Range (moveCursor amt start) (moveCursor amt end)++-- | Moves a range forward by the given amount+moveRangeByN :: Int -> Range -> Range+moveRangeByN amt (Range start end) =+ Range (moveCursorByN amt start) (moveCursorByN amt end)++-- | Moves a 'Coord' forward by the given amount of columns+moveCursorByN :: Int -> Coord -> Coord+moveCursorByN amt (Coord row col) = Coord row (col + amt)++-- | Adds the rows and columns of the given two 'Coord's.+moveCursor :: Coord -> Coord -> Coord+moveCursor (Coord row col) (Coord row' col') = Coord (row + row') (col + col')++instance Num Coord where+ Coord row col + Coord row' col' = Coord (row + row') (col + col')+ Coord row col - Coord row' col' = Coord (row - row') (col - col')+ Coord row col * Coord row' col' = Coord (row * row') (col * col')+ abs (Coord row col) = Coord (abs row) (abs col)+ fromInteger i = Coord 0 (fromInteger i)+ signum (Coord row col) = Coord (signum row) (signum col)++-- | Given the text you're operating over, creates an iso from an 'Offset' to a 'Coord'.+asCoord :: Y.YiString -> Iso' Offset Coord+asCoord txt = iso (toCoord txt) (toOffset txt)++-- | Given the text you're operating over, converts a 'Coord' to an 'Offset'.+toOffset :: Y.YiString -> Coord -> Offset+toOffset txt (Coord row col) = Offset $ lenRows + col+ where+ lenRows = Y.length . Y.concat . take row . Y.lines' $ txt++-- | Given the text you're operating over, converts an 'Offset' to a 'Coord'.+toCoord :: Y.YiString -> Offset -> Coord+toCoord txt (Offset offset) = Coord numRows numColumns+ where+ numRows = Y.countNewLines . Y.take offset $ txt+ numColumns = (offset -) . Y.length . Y.concat . take numRows . Y.lines' $ txt++-- | This will restrict a given 'Coord' to a valid one which lies within the given text.+clampCoord :: Y.YiString -> Coord -> Coord+clampCoord txt (Coord row col) =+ Coord (clamp 0 maxRow row) (clamp 0 maxColumn col)+ where+ maxRow = Y.countNewLines txt+ maxColumn = fromMaybe col (txt ^? to Y.lines' . ix row . to Y.length)++-- | This will restrict a given 'Range' to a valid one which lies within the given text.+clampRange :: Y.YiString -> Range -> Range+clampRange txt (Range start end) =+ Range (clampCoord txt start) (clampCoord txt end)++-- | A span which maps a piece of Monoidal data over a range.+data Span a = Span+ { _getRange :: Range+ , _data :: a+ } deriving (Show, Eq, Functor)++-- | A Helper only used when combining many spans.+data Marker+ = Start+ | End+ deriving (Show, Eq)++type ID = Int+-- | Combines a list of spans containing some monoidal data into a list of offsets with+-- with the data that applies from each Offset forwards.+combineSpans+ :: forall a.+ Monoid a+ => [Span a] -> [(Coord, a)]+combineSpans spans = combiner [] $ sortOn (view _3) (splitStartEnd idSpans)+ where+ idSpans :: [(ID, Span a)]+ idSpans = zip [1 ..] spans++ splitStartEnd :: [(ID, Span a)] -> [(Marker, ID, Coord, a)]+ splitStartEnd [] = []+ splitStartEnd ((i, Span (Range s e) d):rest) =+ (Start, i, s, d) : (End, i, e, d) : splitStartEnd rest++ withoutId :: ID -> [(ID, a)] -> [(ID, a)]+ withoutId i = filter ((/= i) . fst)++ combiner :: [(ID, a)] -> [(Marker, ID, Coord, a)] -> [(Coord, a)]+ combiner _ [] = []+ combiner cur ((Start, i, crd, mData):rest) =+ let dataSum = foldMap snd cur <> mData+ newData = (i, mData) : cur+ in (crd, dataSum) : combiner newData rest+ combiner cur ((End, i, crd, _):rest) =+ let dataSum = foldMap snd newData+ newData = withoutId i cur+ in (crd, dataSum) : combiner newData rest++-- | @clamp min max val@ restricts val to be within min and max (inclusive)+clamp :: Int -> Int -> Int -> Int+clamp mn mx n+ | n < mn = mn+ | n > mx = mx+ | otherwise = n++-- | Returns the number of rows and columns that a 'Range' spans as a 'Coord'+sizeOfR :: Range -> Coord+sizeOfR (Range start end) = end - start++-- | Returns the number of rows and columns that a chunk of text spans as a 'Coord'+sizeOf :: Y.YiString -> Coord+sizeOf txt = Coord (Y.countNewLines txt) (Y.length (txt ^. asLines . _last))++-- | A lens over text before a given 'Coord'+beforeC :: Coord -> Lens' Y.YiString Y.YiString+beforeC c@(Coord row col) = lens getter setter+ where getter txt =+ txt ^.. asLines . taking (row + 1) traverse+ & _last %~ Y.take col+ & Y.unlines++ setter old new = let suffix = old ^. afterC c+ in new <> suffix++-- | A lens over text after a given 'Coord'+afterC :: Coord -> Lens' Y.YiString Y.YiString+afterC c@(Coord row col) = lens getter setter+ where getter txt =+ txt ^.. asLines . dropping row traverse+ & _head %~ Y.drop col+ & Y.unlines++ setter old new = let prefix = old ^. beforeC c+ in prefix <> new
+ src/Rasa/Internal/Scheduler.hs view
@@ -0,0 +1,121 @@+{-# LANGUAGE DeriveFunctor, GeneralizedNewtypeDeriving,+ ExistentialQuantification, ScopedTypeVariables #-}++module Rasa.Internal.Scheduler+ ( Scheduler(..)+ , Hook+ , Hooks+ , afterRender+ , beforeEvent+ , beforeRender+ , dispatchEvent+ , eventListener+ , getHooks+ , matchingHooks+ , onExit+ , onInit+ , onRender+ ) where+++import Rasa.Internal.Action+import Rasa.Internal.Events++import Control.Lens+import Control.Monad.Reader+import Control.Monad.State+import Data.Dynamic+import Data.Foldable+import Data.Map+import Unsafe.Coerce++-- | The Scheduler is how you can register your extension's actions to run+-- at different points in the editor's event cycle.+--+-- The event cycle proceeds as follows:+--+-- @+-- Init (Runs ONCE)+--+-- -- The following loops until an exit is triggered:+-- BeforeEvent -> (any event) -> BeforeRender -> OnRender -> AfterRender+--+-- Exit (Runs ONCE)+-- @+--+-- Each extension which wishes to perform actions exports a @'Scheduler' ()@+-- which the user inserts in their config file.+newtype Scheduler a = Scheduler+ { runSched :: State Hooks a+ } deriving (Functor, Applicative, Monad, MonadState Hooks)++-- | Use this to dispatch an event of any type, any hooks which are listening for this event will be triggered+-- with the provided event. Use this within an Action.+dispatchEvent :: Typeable a => a -> Action ()+dispatchEvent evt = do+ hooks <- ask+ traverse_ ($ evt) (matchingHooks hooks)++-- | This is a helper which extracts and coerces a hook from its wrapper back into the proper event handler type.+getHook :: forall a. Hook -> (a -> Action ())+getHook = coerce+ where+ coerce :: Hook -> (a -> Action ())+ coerce (Hook x) = unsafeCoerce x++-- | This extracts all event listener hooks from a map of hooks which match the type of the provided event.+matchingHooks :: forall a. Typeable a => Hooks -> [a -> Action ()]+matchingHooks hooks = getHook <$> (hooks^.at (typeRep (Proxy :: Proxy a))._Just)++-- | This registers an event listener hook, as long as the listener is well-typed similar to this:+--+-- @MyEventType -> Action ()@ then it will be registered to listen for dispatched events of that type.+-- Use within the 'Rasa.Internal.Scheduler.Scheduler' and add have the user add it to their config.+eventListener :: forall a. Typeable a => (a -> Action ()) -> Scheduler ()+eventListener hook = modify $ insertWith mappend (typeRep (Proxy :: Proxy a)) [Hook hook]++-- | Transform a 'Rasa.Internal.Scheduler.Scheduler' monad into a 'Hooks' map.+getHooks :: Scheduler () -> Hooks+getHooks = flip execState mempty . runSched+++-- | Registers an action to be performed during the Initialization phase.+--+-- This phase occurs exactly ONCE when the editor starts up.+onInit :: Action () -> Scheduler ()+onInit action = eventListener (const action :: Init -> Action ())++-- | Registers an action to be performed BEFORE each event phase.+beforeEvent :: Action () -> Scheduler ()+beforeEvent action = eventListener (const action :: BeforeEvent -> Action ())++-- | Registers an action to be performed BEFORE each render phase.+--+-- This is a good spot to add information useful to the renderer+-- since all actions have been performed. Only cosmetic changes should+-- occur during this phase.+beforeRender :: Action () -> Scheduler ()+beforeRender action = eventListener (const action :: BeforeRender -> Action ())++-- | Registers an action to be performed during each render phase.+--+-- This phase should only be used by extensions which actually render something.+onRender :: Action () -> Scheduler ()+onRender action = eventListener (const action :: OnRender -> Action ())++-- | Registers an action to be performed AFTER each render phase.+--+-- This is useful for cleaning up extension state that was registered for the+-- renderer, but needs to be cleared before the next iteration.+afterRender :: Action () -> Scheduler ()+afterRender action = eventListener (const action :: AfterRender -> Action ())++-- | Registers an action to be performed during the exit phase.+--+-- This is only triggered exactly once when the editor is shutting down. It+-- allows an opportunity to do clean-up, kill any processes you've started, or+-- save any data before the editor terminates.++onExit :: Action () -> Scheduler ()+onExit action = eventListener (const action :: Exit -> Action ())+
+ src/Rasa/Internal/Text.hs view
@@ -0,0 +1,23 @@+{-# language Rank2Types #-}+module Rasa.Internal.Text+ ( asText+ , asString+ , asLines+ ) where++import Control.Lens+import qualified Yi.Rope as Y+import qualified Data.Text as T++-- | An iso which converts to/from 'Y.YiString' -> Text+asText :: Iso' Y.YiString T.Text+asText = iso Y.toText Y.fromText++-- | An iso which converts to/from 'Y.YiString' -> String+asString :: Iso' Y.YiString String+asString = iso Y.toString Y.fromString+++-- | An iso which converts to/from 'Y.YiString' -> ['Y.YiString']+asLines :: Iso' Y.YiString [Y.YiString]+asLines = iso Y.lines Y.unlines