【发布时间】:2015-12-30 10:21:54
【问题描述】:
我正在尝试使用 Haskell、hscurses 和 Data.Text 构建一个简单的文本编辑器。我对 Haskell 很陌生。
这是我的代码中的一个 sn-p:
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 会如何处理这个问题?在 Haskell 中实现字符串“移动”的合理有效方法是什么?移动也应该是可组合的,即我希望能够实现“向下移动一行”等方面的“Page down”。
编辑:更新
如果有人需要这个,这就是我最终得到的。
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)
但这并没有太大帮助,因为“at”必须与“before”一致,根据文档,T.cons 是 O(n)...此外,以行为中心的方法在以下情况下更好实际显示完成。
感谢所有帮助过的人!
【问题讨论】:
-
您可能希望将缓冲区表示为行列表,使用Zipper 表示您当前的位置。对于 Agda 中的指导示例(您可以在 Haskell 中复制相当多的工作),您可以查看 this exercise
-
另见:stackoverflow.com/questions/4046246/…。有一种方法可以将
findIndex加速到 O(n),但之后您仍然使用索引位置,因此从长远来看它并不能真正帮助您。 -
如果你需要跳来跳去,你应该使用
Data.Sequence从一个list拉链切换到一个sequence拉链。data Zipper a = Zipper (Seq a) a (Seq a)或类似的。与列表拉链不同,您可以保持一切井井有条,因为结束与开始一样快。并且您可以在O(log n)时间移动n步骤。
标签: haskell