packages feed

zwirn-0.2.2.0: src/zwirn-lang/Zwirn/Language/Block.hs

{-# LANGUAGE OverloadedStrings #-}

module Zwirn.Language.Block
  ( Block (..),
    BlockError,
    Line (..),
    getBlock,
    structureLines,
    getBlockContent,
    getLineIndent,
    getSingleLine,
    getBlockStart,
    getBlockEnd,
  )
where

{-
    Block.hs - layout sensitive representation of blocks of code
    Copyright (C) 2023, Martin Gius

    This library is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this library.  If not, see <http://www.gnu.org/licenses/>.
-}

import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T

-- | A Block of code is a non-empty list of lines of code
newtype Block = Block
  { bContent :: NE.NonEmpty Line
  }
  deriving (Show, Eq)

-- | A Line of code has a start line and an end line, if start /= end, it represents a multiline
data Line = Line
  { lStart :: Int,
    lEnd :: Int,
    lContent :: T.Text
  }
  deriving (Show, Eq)

type BlockError = String

-- | Get the start line of a block, assumes that the lines are ordered.
getBlockStart :: Block -> Int
getBlockStart (Block ls) = lStart $ NE.head ls

-- | Get the end line of a block, assumes that the lines are ordered.
getBlockEnd :: Block -> Int
getBlockEnd (Block ls) = lEnd $ NE.last ls

-- | Get the contents of a block by merging the contents of its lines
getBlockContent :: Block -> [T.Text]
getBlockContent (Block ls) = NE.toList $ NE.map lContent ls

-- | Given a line number and a list of blocks, get the block that starts before and ends after the line.
getBlock :: Int -> [Block] -> Either BlockError Block
getBlock _ [] = Left "no block of code at current line"
getBlock num (block : bs) =
  if start <= num && num <= end
    then Right block
    else getBlock num bs
  where
    start = getBlockStart block
    end = getBlockEnd block

-- | Given a line number and a non-empty list of lines, get the line that starts before and ends after the line number.
getLine' :: Int -> NE.NonEmpty Line -> Either BlockError Line
getLine' num (l@(Line st en _) NE.:| ls) =
  if st <= num && num <= en
    then Right l
    else case NE.nonEmpty ls of
      Just xs -> getLine' num xs
      Nothing -> Left "no line at current position"

getSingleLine :: Int -> [Block] -> Either BlockError Line
getSingleLine i bs = do
  (Block ls) <- getBlock i bs
  getLine' i ls

-- | Parsing Blocks of code is layout sensitive - this function merges lines of code into a single multiline
-- | whenever the indent of the first line is smaller than the indent of the following lines
structureLines :: NE.NonEmpty Line -> NE.NonEmpty Line
structureLines (l NE.:| ls) = mergeLines vs NE.:| rs
  where
    vs = l NE.:| takeWhile (\x -> getLineIndent x > getLineIndent l) ls
    rs = case NE.nonEmpty $ drop (length vs - 1) ls of
      Just xs -> NE.toList $ structureLines xs
      Nothing -> []

-- | checks how many spaces or tabs are at the start of the line content
getLineIndent :: Line -> Int
getLineIndent (Line _ _ lc) = T.length $ T.takeWhile (\c -> c `elem` (" \t" :: String)) lc

-- | combines lines to a single multiline by merging their content and adjusting the end line
mergeLines :: NE.NonEmpty Line -> Line
mergeLines ((Line lst len c) NE.:| ls) = Line lst end (T.concat $ c : map lContent ls)
  where
    end = if null ls then len else lEnd $ last ls