Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Text Editing

  • textArea edits a Var Text. Use it for short text: notes, descriptions, commit messages.
  • textAreaDocument edits a Var TextDocument, which shares unchanged lines between edits. Use it for files and code buffers.

textArea for short text

notes = do
  draft <- useVar ""
  textArea draft

Every edit builds a new Text, which is cheap for a few kilobytes.

textAreaDocument for documents

editorView :: Var WidgetId -> Var TextDocument -> NanoUI ()
editorView editorId docVar = do
  resp <- textAreaDocumentWith' (grow . minW 320 . minH 200) docVar
  when (respChanged resp) markDirty
  writeVar editorId (respId resp)

Only the edited lines are copied; unchanged lines are shared. documentText materializes the whole text; call it when saving instead of on every edit.

save :: FilePath -> TextDocument -> NanoUI ()
save path doc = liftIO (BS.writeFile path (TE.encodeUtf8 (documentText doc)))

documentLineCount is cheap enough for a status bar.

One undo history

Keyboard edits, mouse selection, the right-click menu, and programmatic commands all dispatch TextCommands, so they share one undo history:

-- | Editing, navigation, selection, history, and clipboard operations on a
-- text field. Mutating commands record an undo step and replace any active
-- selection.
data TextCommand
  = -- | Replace the selection with text (typing, a snippet).
    InsertText !Text
  | -- | Delete the selection, or from the cursor to where the motion lands:
    -- @Delete CharLeft@ is Backspace, @Delete WordRight@ Ctrl+Delete.
    Delete !TextMotion
  | -- | Move the cursor, extending the selection when the flag is set.
    -- Without it, 'CharLeft' and 'CharRight' over a selection go to its start
    -- and its end.
    Move !TextMotion !Bool
  | SelectAll
  | -- | Select from the first position (the anchor) to the second (the
    -- cursor), clamped into the document.
    Select !Cursor !Cursor
  | -- | Replace the text between two positions, leaving the cursor after it.
    Replace !Cursor !Cursor !Text
  | -- | Replace the whole document as one undoable edit.
    ReplaceAll !Text
  | Undo
  | Redo
  | Cut
  | Copy
  | Paste
  deriving (Eq, Show)

Run a command against a field by its WidgetId:

editMenu :: WidgetId -> Text -> NanoUI ()
editMenu editor today = do
  whenM (menuItem "Undo") (runTextCommand editor Undo)
  whenM (menuItem "Insert date") (runTextCommand editor (InsertText today))

Query history state for menu enablement:

undoState :: Var WidgetId -> NanoUI (Bool, Bool)
undoState editorId = do
  editor <- readVar editorId
  canUndo <- textCanUndo editor
  canRedo <- textCanRedo editor
  pure (canUndo, canRedo)

A complete Edit menu built this way is in Build a Notepad.

Tip

Directly overwriting the Var Text or Var TextDocument from outside resets the undo stack. To make a programmatic edit undoable, issue a TextCommand (InsertText, ReplaceAll, and so on) instead of writing the Var.

In the primed variants’ Response, respChanged is True after an edit and False after a caret move alone, so use it to mark a document dirty. respId names the editor for commands and focus.