Haskell"弦乐运动"功能

时间:2015-12-30 10:21:55

标签: haskell

我正在尝试使用Haskell,hscurses和Data.Text构建一个简单的文本编辑器。我对Haskell很新。

以下是我的代码中的代码段:

data Cursor = Cursor {
  position :: Int,
  line     :: Int,
  column   :: Int
} deriving (Eq, Show)

isNewline :: Char -> Bool
isNewline c = c == '\n'

onNewline :: T.Text -> Int -> Bool
onNewline buf pos
  | pos >= T.length buf = False
  | otherwise           = isNewline $ T.index buf pos

findIndex :: (Char -> Bool) -> T.Text -> Int -> Maybe Int
findIndex pred buf pos
  | buf == T.empty = Just 0
  | otherwise      = rightWhile pos
  where rightWhile pos
          | pos > bufMax buf       = Nothing
          | pred $ T.index buf pos = Just pos
          | otherwise              = rightWhile (pos + 1)

findIndexLeft :: (Char -> Bool) -> T.Text -> Int -> Maybe Int
findIndexLeft pred buf pos = leftWhile pos
  where leftWhile pos
          | pos < 0                = Nothing
          | pred $ T.index buf pos = Just pos
          | otherwise              = leftWhile (pos - 1)

startOfLine :: T.Text -> Int -> Int
startOfLine buf pos = case findIndexLeft isNewline buf (pos - 1) of
  Nothing -> 0
  Just p  -> p + 1

endOfLine :: T.Text -> Int -> Int
endOfLine buf pos = case findIndex isNewline buf pos of
  Nothing -> 1 + bufMax buf
  Just p  -> p

lineOffset :: T.Text -> Int -> Int
lineOffset buf pos = pos - startOfLine buf pos

lineLength :: T.Text -> Int -> Int
lineLength buf pos = endOfLine buf pos - startOfLine buf pos

bufMax :: T.Text -> Int
bufMax buf = max 0 $ T.length buf - 1

bufLines :: T.Text -> Int
bufLines = T.foldl (\acc c -> if isNewline c then (acc+1) else acc) 0

moveCursorRight :: T.Text -> Cursor -> Cursor
moveCursorRight buf c@(Cursor pos line col)
  | buf == T.empty = c
  | otherwise      = Cursor newPos newLine newCol
  where end       = 1 + bufMax buf
        onEnd     = pos == end
        newPos    = clip (pos + 1) 0 end
        newLine   = if onNewline buf pos && not onEnd
                    then line + 1
                    else line
        newCol    = lineOffset buf newPos

moveCursorLeft :: T.Text -> Cursor -> Cursor
moveCursorLeft buf (Cursor pos line col) =
  Cursor newPos newLine newCol
  where onStart   = pos == 0
        newPos    = clipLow (pos - 1) 0
        newLine   = if onNewline buf newPos && not onStart
                    then line - 1
                    else line
        newCol    = lineOffset buf newPos

-- More movement functions follow...

此代码的问题在于,对于数千行长的缓冲区,它会变得非常慢。这可能是因为索引函数的使用,例如O(n),而不是像C中那样的恒定时间。

经验丰富的Haskeller如何接近这个?什么是一种合理有效的方式来实施&#34;运动&#34;在Haskell中的字符串?该运动也应该是可组合的,也就是说,我希望能够实现&#34; Page down&#34;根据&#34;向下移动一行&#34;等等。

修改:更新

如果有人需要这个,这就是我最终的目标。

type Line = T.Text

data BufferContext = BufferContext {
  before   :: [Line],
  at       :: Line,
  after    :: [Line]
} deriving (Eq, Show)

moveCursorRight :: Cursor -> Cursor
moveCursorRight c@(Cursor pos line col bc@(BufferContext before at after))
  | col >= T.length at = moveCursorDown c
  | otherwise          = Cursor (pos+1) line (col+1) bc

moveCursorLeft :: Cursor -> Cursor
moveCursorLeft c@(Cursor pos line col bc@(BufferContext before at after))
  | col <= 0  = upCursor { column = if null before then 0 else T.length $ head before }
  | otherwise = Cursor (pos-1) line (col-1) bc
  where upCursor = moveCursorUp c

moveCursorDown :: Cursor -> Cursor
moveCursorDown c@(Cursor _ _ _ (BufferContext _ _ [])) = c
moveCursorDown c@(Cursor _ cLine _ (BufferContext before at (l:ls))) =
  c { line    = cLine+1,
      column  = 0,
      context = BufferContext (at:before) l ls
    }

moveCursorUp c@(Cursor _ _ _ (BufferContext [] _ _)) = c
moveCursorUp c@(Cursor _ cLine _ (BufferContext (l:ls) at after)) =
  c { line = cLine-1,
      column = 0,
      context = BufferContext ls l (at:after)
    }

这个实现在100万行上非常实用,这对我来说已经足够了。但是,这种方法仍然存在一个问题。如果我想跳到一个随机线,我必须逐个移动,这可能很慢。但是,这仍然是对原始方法的巨大改进。

我也尝试将上下文实现为

data BufferContext = BufferContext {
  before   :: T.Text,
  at       :: Char,
  after    :: T.Text
} deriving (Eq, Show)

但这并没有太大的帮助,因为&#34; at&#34;必须与&#34;&#34;之前#34;根据文档,T.cons是O(n)...此外,当实际显示完成时,以行为中心的方法更好。

感谢所有帮助过的人!

1 个答案:

答案 0 :(得分:3)

正如加莱在评论中所说,你想要使用拉链。这个想法是你的“光标”实际上是这样的数据结构:

type Line = T.Text

data TextZipper = TextZipper {
   textBefore :: [Line],
   currentLine :: Line,
   textAfter :: [Line]
}

诀窍是“textBefore”以反向顺序包含光标上方的行。因此,为了向下移动一行,您将“currentLine”放在“textBefore”的头部,并从“textAfter”中获取新的“currentLine”,如下所示:

moveDown :: TextZipper -> Maybe TextZipper
moveDown tzip = case textAfter tzip of
   [] -> Nothing    -- Already at the bottom of the file.
   t:ts -> TextZipper {
      textBefore = currentLine tzip : textBefore tzip,
      currentLine = t,
      textAfter = ts
   }

moveUp将非常相似。您还需要一个textZipperToList函数来提取用于保存的zipper的内容,以及一个textZipperFromList函数。

我记得在某处读过Emacs使用类似的概念,除了它是按字符而不是按行进行的。缓冲区表示为两个文本块,一个是光标前的块,另一个是光标后的块。移动光标是通过将字符从一个块复制到另一个块来完成的。这里的概念相同。鉴于此,您可能需要考虑使用单个Text值替换每个行列表。