packages feed

ribosome-0.9.9.9: lib/Ribosome/Internal/Scratch.hs

{-# options_haddock prune #-}

-- |Internal logic for 'Ribosome.Scratch'.
module Ribosome.Internal.Scratch where

import qualified Data.Map.Strict as Map
import Data.MessagePack (Object)
import Exon (exon)
import qualified Polysemy.Log as Log
import Prelude hiding (group)

import Ribosome.Api.Autocmd (bufferAutocmd, eventignore)
import Ribosome.Api.Buffer (setBufferContent, wipeBuffer)
import Ribosome.Api.Syntax (executeWindowSyntax)
import Ribosome.Api.Tabpage (closeTabpage)
import Ribosome.Api.Window (closeWindow)
import Ribosome.Data.FloatOptions (FloatOptions, enter)
import Ribosome.Data.PluginName (PluginName (PluginName))
import Ribosome.Data.ScratchId (ScratchId (ScratchId, unScratchId))
import Ribosome.Data.ScratchOptions (ScratchOptions (ScratchOptions, filetype, name), focus, mappings, syntax)
import qualified Ribosome.Data.ScratchState as ScratchState
import Ribosome.Data.ScratchState (ScratchState (ScratchState))
import Ribosome.Host.Api.Data (Buffer, Tabpage, Window)
import Ribosome.Host.Api.Effect (
  bufferGetName,
  bufferSetName,
  bufferSetOption,
  nvimBufIsLoaded,
  nvimCreateBuf,
  nvimDelAutocmd,
  nvimOpenWin,
  vimCommand,
  vimGetCurrentBuffer,
  vimGetCurrentTabpage,
  vimGetCurrentWindow,
  vimSetCurrentWindow,
  windowGetBuffer,
  windowIsValid,
  windowSetHeight,
  windowSetOption,
  windowSetWidth,
  )
import Ribosome.Host.Class.Msgpack.Decode (fromMsgpack)
import Ribosome.Host.Class.Msgpack.Encode (toMsgpack)
import Ribosome.Host.Data.RpcError (RpcError)
import Ribosome.Host.Data.RpcType (AutocmdId (AutocmdId), group)
import Ribosome.Host.Effect.Rpc (Rpc)
import Ribosome.Mapping (activateBufferMapping)
import Ribosome.PluginName (pluginNamePascalCase)

createScratchTab :: Member Rpc r => Sem r Tabpage
createScratchTab = do
  vimCommand "tabnew"
  vimGetCurrentTabpage

createRegularWindow ::
  Member Rpc r =>
  Bool ->
  Bool ->
  Maybe Int ->
  Sem r (Buffer, Window)
createRegularWindow vertical bottom size = do
  vimCommand prefixedCmd
  buf <- vimGetCurrentBuffer
  win <- vimGetCurrentWindow
  pure (buf, win)
  where
    prefixedCmd = locationPrefix <> " " <> sizePrefix <> cmd
    cmd = if vertical then "vnew" else "new"
    sizePrefix = maybe "" show size
    locationPrefix = if bottom then "belowright" else "aboveleft"

floatConfig ::
  FloatOptions ->
  Map Text Object
floatConfig =
  fromRight Map.empty . fromMsgpack . toMsgpack

createFloatWith ::
  Member Rpc r =>
  Bool ->
  Bool ->
  FloatOptions ->
  Sem r (Buffer, Window)
createFloatWith listed scratch options = do
  buffer <- nvimCreateBuf listed scratch
  window <- nvimOpenWin buffer (enter options) (floatConfig options)
  pure (buffer, window)

createFloat ::
  Member Rpc r =>
  FloatOptions ->
  Sem r (Buffer, Window)
createFloat =
  createFloatWith True True

createScratchWindow ::
  Member Rpc r =>
  Bool ->
  Bool ->
  Bool ->
  Maybe FloatOptions ->
  Maybe Int ->
  Sem r (Buffer, Window)
createScratchWindow vertical wrap bottom float size = do
  (buffer, win) <- createWindow
  windowSetOption win "wrap" (toMsgpack wrap)
  windowSetOption win "number" (toMsgpack False)
  windowSetOption win "cursorline" (toMsgpack True)
  windowSetOption win "colorcolumn" (toMsgpack ("" :: Text))
  windowSetOption win "foldmethod" (toMsgpack ("manual" :: Text))
  windowSetOption win "conceallevel" (toMsgpack (2 :: Int))
  windowSetOption win "concealcursor" (toMsgpack ("nvic" :: Text))
  pure (buffer, win)
  where
    createWindow =
      maybe regular createFloat float
    regular =
      createRegularWindow vertical bottom size

createScratchUiInTab :: Member Rpc r => Sem r (Buffer, Window, Maybe Tabpage)
createScratchUiInTab = do
  tab <- createScratchTab
  win <- vimGetCurrentWindow
  buffer <- windowGetBuffer win
  pure (buffer, win, Just tab)

createScratchUi ::
  Member Rpc r =>
  ScratchOptions ->
  Sem r (Buffer, Window, Maybe Tabpage)
createScratchUi (ScratchOptions False vertical wrap _ _ bottom _ float size _ _ _ _ _) =
  uncurry (,,Nothing) <$> createScratchWindow vertical wrap bottom float size
createScratchUi _ =
  createScratchUiInTab

configureScratchBuffer ::
  Member Rpc r =>
  Buffer ->
  Maybe Text ->
  ScratchId ->
  Sem r ()
configureScratchBuffer buffer ft (ScratchId name) = do
  bufferSetOption buffer "bufhidden" ("wipe" :: Text)
  bufferSetOption buffer "buftype" ("nofile" :: Text)
  bufferSetOption buffer "swapfile" False
  traverse_ (bufferSetOption buffer "filetype") ft
  bufferSetName buffer name

setupScratchBuffer ::
  Members [Rpc, Log] r =>
  Window ->
  Buffer ->
  Maybe Text ->
  ScratchId ->
  Sem r Buffer
setupScratchBuffer window buffer ft name = do
  valid <- nvimBufIsLoaded buffer
  Log.debug [exon|#{if valid then "" else "in"}valid scratch buffer|]
  validBuffer <- if valid then pure buffer else windowGetBuffer window
  configureScratchBuffer validBuffer ft name
  pure validBuffer

setupDeleteAutocmd ::
  Members [Rpc, Reader PluginName] r =>
  ScratchId ->
  Buffer ->
  Sem r AutocmdId
setupDeleteAutocmd (ScratchId name) buffer = do
  PluginName pname <- pluginNamePascalCase
  bufferAutocmd buffer "BufDelete" def { group = Just "RibosomeScratch" } (deleteCall pname)
  where
    deleteCall pname =
      [exon|silent! call #{pname}DeleteScratch('#{name}')|]

setupScratchIn ::
  Members [Rpc, AtomicState (Map ScratchId ScratchState), Reader PluginName, Log] r =>
  Buffer ->
  Window ->
  Window ->
  Maybe Tabpage ->
  ScratchOptions ->
  Sem r ScratchState
setupScratchIn buffer previous window tab options@(ScratchOptions {..}) = do
  validBuffer <- setupScratchBuffer window buffer filetype name
  traverse_ (executeWindowSyntax window) syntax
  traverse_ (activateBufferMapping validBuffer) mappings
  unless focus (vimSetCurrentWindow previous)
  auId <- setupDeleteAutocmd name validBuffer
  let scratch = ScratchState name options validBuffer window previous tab auId
  atomicModify' (Map.insert name scratch)
  pure scratch

createScratch ::
  Members [Rpc, AtomicState (Map ScratchId ScratchState), Reader PluginName, Log, Resource] r =>
  ScratchOptions ->
  Sem r ScratchState
createScratch options = do
  Log.debug [exon|creating new scratch: #{show options}|]
  previous <- vimGetCurrentWindow
  (buffer, window, tab) <- eventignore (createScratchUi options)
  eventignore $ setupScratchIn buffer previous window tab options

bufferStillLoaded ::
  Members [Rpc !! RpcError, Rpc] r =>
  ScratchId ->
  Buffer ->
  Sem r Bool
bufferStillLoaded (ScratchId name) buffer =
  (&&) <$> loaded <*> loadedName
  where
    loaded =
      nvimBufIsLoaded buffer
    loadedName =
      resumeAs @RpcError False ((name ==) <$> bufferGetName buffer)

updateScratch ::
  Members [Rpc !! RpcError, Rpc, AtomicState (Map ScratchId ScratchState), Reader PluginName, Log, Resource] r =>
  ScratchState ->
  ScratchOptions ->
  Sem r ScratchState
updateScratch oldScratch@(ScratchState name _ oldBuffer oldWindow _ _ _) options = do
  Log.debug [exon|updating existing scratch `#{coerce name}`|]
  ifM (windowIsValid oldWindow) attemptReuseWindow reset
  where
    attemptReuseWindow =
      ifM (bufferStillLoaded name oldBuffer) (pure oldScratch) closeAndReset
    closeAndReset =
      closeWindow oldWindow *> reset
    reset =
      createScratch options

lookupScratch ::
  Member (AtomicState (Map ScratchId ScratchState)) r =>
  ScratchId ->
  Sem r (Maybe ScratchState)
lookupScratch name =
  atomicGets (Map.lookup name)

ensureScratch ::
  Members [Rpc !! RpcError, Rpc, AtomicState (Map ScratchId ScratchState), Reader PluginName, Log, Resource] r =>
  ScratchOptions ->
  Sem r ScratchState
ensureScratch options = do
  f <- maybe createScratch updateScratch <$> lookupScratch (options ^. #name)
  f options

withModifiable ::
  Member Rpc r =>
  Buffer ->
  ScratchOptions ->
  Sem r a ->
  Sem r a
withModifiable buffer options thunk =
  if isWrite then thunk else wrap
  where
    isWrite =
      options ^. #modify
    wrap =
      update True *> thunk <* update False
    update =
      bufferSetOption buffer "modifiable"

setScratchContent ::
  Foldable t =>
  Members [Rpc !! RpcError, Rpc] r =>
  ScratchState ->
  t Text ->
  Sem r ()
setScratchContent (ScratchState _ options buffer win _ _ _) lines' = do
  withModifiable buffer options $ setBufferContent buffer (toList lines')
  when (options ^. #resize) (resume_ @RpcError @Rpc (setSize win size))
  where
    size =
      max 1 calculateSize
    calculateSize =
      if vertical then fromMaybe 50 maxSize else min (length lines') (fromMaybe 30 maxSize)
    maxSize =
      options ^. #maxSize
    vertical =
      options ^. #vertical
    setSize =
      if vertical then windowSetWidth else windowSetHeight

showInScratch ::
  Foldable t =>
  Members [Rpc !! RpcError, Rpc, AtomicState (Map ScratchId ScratchState), Reader PluginName, Log, Resource] r =>
  t Text ->
  ScratchOptions ->
  Sem r ScratchState
showInScratch lines' options = do
  scratch <- ensureScratch options
  scratch <$ setScratchContent scratch lines'

showInScratchDef ::
  Foldable t =>
  Members [Rpc !! RpcError, Rpc, AtomicState (Map ScratchId ScratchState), Reader PluginName, Log, Resource] r =>
  t Text ->
  Sem r ScratchState
showInScratchDef lines' =
  showInScratch lines' def

killScratch ::
  Members [Rpc !! RpcError, AtomicState (Map ScratchId ScratchState), Log] r =>
  ScratchState ->
  Sem r ()
killScratch (ScratchState name _ buffer window _ tab (AutocmdId auId)) = do
  Log.debug [exon|Killing scratch buffer `#{unScratchId name}`|]
  atomicModify' (Map.delete @_ @ScratchState name)
  resume_ (nvimDelAutocmd auId)
  traverse_ (resume_ . closeTabpage) tab
  resume_ (closeWindow window)
  resume_ (wipeBuffer buffer)

scratchPreviousWindow ::
  Member (AtomicState (Map ScratchId ScratchState)) r =>
  ScratchId ->
  Sem r (Maybe Window)
scratchPreviousWindow =
  fmap (fmap ScratchState.previous) . lookupScratch

scratchWindow ::
  Member (AtomicState (Map ScratchId ScratchState)) r =>
  ScratchId ->
  Sem r (Maybe Window)
scratchWindow =
  fmap (fmap ScratchState.window) . lookupScratch

scratchBuffer ::
  Member (AtomicState (Map ScratchId ScratchState)) r =>
  ScratchId ->
  Sem r (Maybe Buffer)
scratchBuffer =
  fmap (fmap ScratchState.buffer) . lookupScratch