我正在尝试使用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)...此外,当实际显示完成时,以行为中心的方法更好。
感谢所有帮助过的人!
答案 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值替换每个行列表。