Add 'shf/' from commit 'b6c5a95b664aeb861d7b33ffc9eefe447ba99dd7'

git-subtree-dir: shf
git-subtree-mainline: 401408448f
git-subtree-split: b6c5a95b66
This commit is contained in:
2026-04-24 00:27:49 +01:00
83 changed files with 43952 additions and 0 deletions

45
shf/fcore/app-type.sml Normal file
View File

@@ -0,0 +1,45 @@
structure AppType =
struct
datatype mode =
NORMAL_MODE of string
| NORMAL_SEARCH_MODE of
{ searchString: string
, tempSearchList: PersistentVector.t
, searchCursorIdx: int
, searchScrollColumn: int
, caseSensitive: bool
}
type app_type =
{ mode: mode
, buffer: LineGap.t
, bufferModifyTime: Time.time
, searchList: PersistentVector.t
, windowWidth: int
, windowHeight: int
(* line to start drawing from *)
, startLine: int
(* absolute index of movable cursor *)
, cursorIdx: int
(* column to start drawing text at for horizontal scrolling. *)
, visualScrollColumn: int
, dfa: int vector vector
(* msgs to send after an update.
* The list of messages is reset on each invocation of AppUpdate.update. *)
, msgs: MailboxType.t list
}
fun init (buffer, windowWidth, windowHeight, time) : app_type =
{ mode = NORMAL_MODE ""
, buffer = buffer
, bufferModifyTime = time
, searchList = PersistentVector.empty
, windowWidth = windowWidth
, windowHeight = windowHeight
, startLine = 0
, cursorIdx = 0
, visualScrollColumn = 0
, dfa = Vector.fromList []
, msgs = []
}
end

10
shf/fcore/app-update.sml Normal file
View File

@@ -0,0 +1,10 @@
structure AppUpdate =
struct
open AppType
fun update (app: app_type, msg, time) =
case #mode app of
NORMAL_MODE modeData => NormalMode.update (app, modeData, msg, time)
| NORMAL_SEARCH_MODE modeData =>
NormalSearchMode.update (app, modeData, msg, time)
end

35
shf/fcore/app-with.sml Normal file
View File

@@ -0,0 +1,35 @@
structure AppWith =
struct
open AppType
(* this function exists only for testing *)
fun idx (app, newIdx) =
let
val
{ startLine
, buffer
, bufferModifyTime
, searchList
, dfa
, mode
, windowWidth
, windowHeight
, msgs
, visualScrollColumn
, cursorIdx = _
} = app
in
{ startLine = startLine
, buffer = buffer
, bufferModifyTime = bufferModifyTime
, searchList = searchList
, dfa = dfa
, mode = mode
, windowWidth = windowWidth
, windowHeight = windowHeight
, msgs = msgs
, visualScrollColumn = visualScrollColumn
, cursorIdx = newIdx
}
end
end

120
shf/fcore/bin-search.sml Normal file
View File

@@ -0,0 +1,120 @@
structure BinSearch =
struct
local
fun reverseLinearSearch (findNum, idx, vec) =
if idx < 0 then
~1
else
let
val curVal = Vector.sub (vec, idx)
in
if curVal < findNum then idx
else reverseLinearSearch (findNum, idx - 1, vec)
end
fun helpBinSearch (findNum, vec, low, high) =
let
val mid = low + ((high - low) div 2)
in
if high >= low then
let
val midVal = Vector.sub (vec, mid)
in
if midVal = findNum then
mid
else if midVal < findNum then
helpBinSearch (findNum, vec, mid + 1, high)
else
helpBinSearch (findNum, vec, low, mid - 1)
end
else
reverseLinearSearch (findNum, mid, vec)
end
in
fun equalOrLess (findNum, vec) =
helpBinSearch (findNum, vec, 0, Vector.length vec - 1)
end
local
fun forwardLinearSearch (findNum, idx, vec) =
if idx = Vector.length vec then
~1
else
let
val curVal = Vector.sub (vec, idx)
in
if curVal > findNum then idx
else forwardLinearSearch (findNum, idx + 1, vec)
end
fun helpBinSearch (findNum, vec, low, high) =
let
val mid = low + ((high - low) div 2)
in
if high >= low then
let
val midVal = Vector.sub (vec, mid)
in
if midVal = findNum then
mid
else if midVal < findNum then
helpBinSearch (findNum, vec, mid + 1, high)
else
helpBinSearch (findNum, vec, low, mid - 1)
end
else
forwardLinearSearch (findNum, Int.max (mid, 0), vec)
end
in
fun equalOrMore (findNum, vec) =
helpBinSearch (findNum, vec, 0, Vector.length vec - 1)
end
local
fun helpExists (findNum, vec, low, high) =
let
val mid = low + ((high - low) div 2)
in
if high >= low then
let
val midVal = Vector.sub (vec, mid)
in
if midVal = findNum then
true
else if midVal < findNum then
helpExists (findNum, vec, mid + 1, high)
else
helpExists (findNum, vec, low, mid - 1)
end
else
false
end
in
fun exists (findNum, vec) =
helpExists (findNum, vec, 0, Vector.length vec - 1)
end
local
fun helpEqualOrMinus1 (findNum, vec, low, high) =
let
val mid = low + ((high - low) div 2)
in
if high >= low then
let
val midVal = Vector.sub (vec, mid)
in
if midVal = findNum then
mid
else if midVal < findNum then
helpEqualOrMinus1 (findNum, vec, mid + 1, high)
else
helpEqualOrMinus1 (findNum, vec, low, mid - 1)
end
else
~1
end
in
fun equalOrMinus1 (findNum, vec) =
helpEqualOrMinus1 (findNum, vec, 0, Vector.length vec - 1)
end
end

View File

@@ -0,0 +1,303 @@
signature MAKE_DFA_LOOP =
sig
val fStart: int * int * string * string list * Word8.word * int -> int
val startState: Word8.word
end
functor MakeNextDfaLoop(M: MAKE_DFA_LOOP) =
struct
fun next (lineGap: LineGap.t, cursorIdx, count) =
let
val {rightStrings, idx = bufferIdx, ...} = lineGap
(* convert absolute cursorIdx to idx relative to hd string *)
val strIdx = cursorIdx - bufferIdx
in
case rightStrings of
shd :: stl =>
if strIdx < String.size shd then
(* strIdx is in this string *)
M.fStart (strIdx, cursorIdx, shd, stl, M.startState, count)
else
(* strIdx is in tl *)
(case stl of
stlhd :: stltl =>
let
val strIdx = strIdx - String.size shd
in
M.fStart
(strIdx, cursorIdx, stlhd, stltl, M.startState, count)
end
| _ => cursorIdx)
| [] => cursorIdx
end
end
functor MakeNextDfaLoopPlus1(M: MAKE_DFA_LOOP) =
struct
fun next (lineGap: LineGap.t, cursorIdx, count) =
let
val {rightStrings, idx = bufferIdx, ...} = lineGap
(* convert absolute cursorIdx to idx relative to hd string *)
val strIdx = cursorIdx - bufferIdx + 1
val absIdx = cursorIdx + 1
in
case rightStrings of
shd :: stl =>
if strIdx < String.size shd then
(* strIdx is in this string *)
M.fStart (strIdx, absIdx, shd, stl, M.startState, count)
else
(* strIdx is in tl *)
(case stl of
stlhd :: stltl =>
let val strIdx = strIdx - String.size shd
in M.fStart (strIdx, absIdx, stlhd, stltl, M.startState, count)
end
| _ => cursorIdx)
| [] => cursorIdx
end
end
functor MakePrevDfaLoop(M: MAKE_DFA_LOOP) =
struct
fun startLeftStrings (leftStrings, absIdx, count) =
case leftStrings of
lhd :: ltl =>
M.fStart (String.size lhd - 1, absIdx, lhd, ltl, M.startState, count)
| [] => 0
fun prev (lineGap: LineGap.t, cursorIdx, count) =
let
val {rightStrings, leftStrings, idx = bufferIdx, ...} = lineGap
(* convert absolute cursorIdx to idx relative to hd string *)
val strIdx = cursorIdx - bufferIdx
in
case rightStrings of
shd :: stl =>
if strIdx < String.size shd then
(* strIdx is in this string *)
M.fStart (strIdx, cursorIdx, shd, leftStrings, M.startState, count)
else
(* strIdx is in tl *)
(case stl of
stlhd :: stltl =>
let
val strIdx = strIdx - String.size shd
val leftStrings = shd :: leftStrings
in
M.fStart
( strIdx
, cursorIdx
, stlhd
, leftStrings
, M.startState
, count
)
end
| [] => startLeftStrings (leftStrings, cursorIdx, count))
| [] => startLeftStrings (leftStrings, cursorIdx, count)
end
end
functor MakePrevDfaLoopMinus1(M: MAKE_DFA_LOOP) =
struct
fun startLeftStrings (leftStrings, absIdx, count) =
case leftStrings of
lhd :: ltl =>
M.fStart (String.size lhd - 1, absIdx, lhd, ltl, M.startState, count)
| [] => 0
fun prev (lineGap: LineGap.t, cursorIdx, count) =
let
val {idx = bufferIdx, leftStrings, ...} = lineGap
val strIdx = cursorIdx - bufferIdx - 1
val absIdx = cursorIdx - 1
in
if strIdx < 0 then
startLeftStrings (leftStrings, absIdx, count)
else
case #rightStrings lineGap of
rhd :: _ =>
M.fStart (strIdx, absIdx, rhd, leftStrings, M.startState, count)
| [] => startLeftStrings (leftStrings, absIdx, count)
end
end
signature MAKE_CHAR_FOLDER =
sig
val startState: Word8.word
val tables: Word8.word vector vector
val isFinal: Word8.word -> bool
val finish: int -> int
end
functor MakeCharFolderNext(Fn: MAKE_CHAR_FOLDER) =
struct
fun nextState (currentState, currentChar) =
let
val currentState = Word8.toInt currentState
val currentTable = Vector.sub (Fn.tables, currentState)
val charIdx = Char.ord currentChar
in
Vector.sub (currentTable, charIdx)
end
fun foldNext (idx, absIdx, str, tl, currentState, counter) =
if idx = String.size str then
case tl of
str :: tl => foldNext (0, absIdx, str, tl, currentState, counter)
| [] => absIdx
else
let
val chr = String.sub (str, idx)
val newState = nextState (currentState, chr)
in
if Fn.isFinal newState then
if counter - 1 = 0 then
Fn.finish absIdx
else
(* new loop, so reset start state and proceed *)
let val newState = nextState (Fn.startState, chr)
in foldNext (idx + 1, absIdx + 1, str, tl, newState, counter - 1)
end
else
foldNext (idx + 1, absIdx + 1, str, tl, newState, counter)
end
end
functor MakeCharFolderPrev(Fn: MAKE_CHAR_FOLDER) =
struct
fun nextState (currentState, currentChar) =
let
val currentState = Word8.toInt currentState
val currentTable = Vector.sub (Fn.tables, currentState)
val charIdx = Char.ord currentChar
in
Vector.sub (currentTable, charIdx)
end
fun foldPrev (idx, absIdx, str, tl, currentState, counter) =
if idx < 0 then
case tl of
str :: tl =>
foldPrev (String.size str - 1, absIdx, str, tl, currentState, counter)
| [] => 0
else
let
val chr = String.sub (str, idx)
val newState = nextState (currentState, chr)
in
if Fn.isFinal newState then
if counter - 1 = 0 then
Fn.finish absIdx
else
let val newState = nextState (Fn.startState, chr)
in foldPrev (idx - 1, absIdx - 1, str, tl, newState, counter - 1)
end
else
foldPrev (idx - 1, absIdx - 1, str, tl, newState, counter)
end
end
signature MAKE_IF_CHAR_FOLDER =
sig
type env
val fStart:
int * string * int vector * int * string list * int vector list * env
-> int
end
functor MakeIfCharFolderPrev(Fn: MAKE_IF_CHAR_FOLDER) =
struct
fun foldPrev (lineGap: LineGap.t, cursorIdx, env: Fn.env) =
let
val
{rightStrings, idx = bufferIdx, rightLines, leftStrings, leftLines, ...} =
lineGap
in
case (rightStrings, rightLines) of
(strHd :: strTl, lnHd :: lnTl) =>
let
(* convert absolute cursorIdx to idx relative to hd string *)
val strIdx = cursorIdx - bufferIdx
in
if strIdx < String.size strHd then
(* strIdx is either in this string or in leftStrings *)
if strIdx < 0 then
case (leftStrings, leftLines) of
(lshd :: lstl, llhd :: lltl) =>
Fn.fStart
( String.size lshd - 1
, lshd
, llhd
, cursorIdx
, lstl
, lltl
, env
)
| (_, _) => 0
else
Fn.fStart
(strIdx, strHd, lnHd, cursorIdx, leftStrings, leftLines, env)
else
(* strIdx must be in the strTl *)
(case (strTl, lnTl) of
(nestStrHd :: _, nestLnHd :: _) =>
let
val strIdx = strIdx - String.size strHd
in
Fn.fStart
( strIdx
, nestStrHd
, nestLnHd
, cursorIdx
, strHd :: leftStrings
, lnHd :: leftLines
, env
)
end
| (_, _) => cursorIdx)
end
| (_, _) => (* nowhere to go, so return cursorIdx *) cursorIdx
end
end
functor MakeIfCharFolderNext(Fn: MAKE_IF_CHAR_FOLDER) =
struct
fun foldNext (lineGap: LineGap.t, cursorIdx, env: Fn.env) =
let
val {rightStrings, idx = bufferIdx, rightLines, ...} = lineGap
in
case (rightStrings, rightLines) of
(strHd :: strTl, lnHd :: lnTl) =>
let
(* convert absolute cursorIdx to idx relative to hd string *)
val strIdx = cursorIdx - bufferIdx
in
if strIdx < String.size strHd then
(* strIdx is in this string *)
Fn.fStart (strIdx, strHd, lnHd, cursorIdx, strTl, lnTl, env)
else
(* strIdx must be in the strTl *)
(case (strTl, lnTl) of
(nestStrHd :: nestStrTl, nestLnHd :: nestLnTl) =>
let
val strIdx = strIdx - String.size strHd
in
Fn.fStart
( strIdx
, nestStrHd
, nestLnHd
, cursorIdx
, nestStrTl
, nestLnTl
, env
)
end
| (_, _) => cursorIdx)
end
| (_, _) => (* nowhere to go, so return cursorIdx *) cursorIdx
end
end

View File

@@ -0,0 +1,239 @@
structure ViCapsWordDfa =
struct
val startState: Word8.word = 0w0
val startNonBlankState: Word8.word = 0w1
val startSpaceState: Word8.word = 0w2
val nonBlankAfterSpaceState: Word8.word = 0w3
val spaceAfterNonBlankState = 0w4
val startNewline: Word8.word = 0w5
val newlineToNewline: Word8.word = 0w6
val chrToNewline: Word8.word = 0w07
fun makeStart i =
let
val chr = Char.chr i
in
if chr = #"\n" then startNewline
else if Char.isSpace chr then startSpaceState
else startNonBlankState
end
fun makeStartNonBlankState i =
let
val chr = Char.chr i
in
if chr = #"\n" then chrToNewline
else if Char.isSpace chr then spaceAfterNonBlankState
else startNonBlankState
end
fun makeStartSpace i =
let
val chr = Char.chr i
in
if chr = #"\n" then chrToNewline
else if Char.isSpace chr then startSpaceState
else nonBlankAfterSpaceState
end
fun makeNonBlankAfterSpace i =
let
val chr = Char.chr i
in
if chr = #"\n" then chrToNewline
else if Char.isSpace chr then spaceAfterNonBlankState
else nonBlankAfterSpaceState
end
fun makeStartNewline i =
let
val chr = Char.chr i
in
if chr = #"\n" then newlineToNewline
else if Char.isSpace chr then startSpaceState
else nonBlankAfterSpaceState
end
val startTable = Vector.tabulate (255, makeStart)
val startNonBlankTable = Vector.tabulate (255, makeStartNonBlankState)
val startSpaceTable = Vector.tabulate (255, makeStartSpace)
val nonBlankAfterSpaceTable = Vector.tabulate (255, makeNonBlankAfterSpace)
val spaceAfterNonBlankTable = nonBlankAfterSpaceTable
val newlineTable = Vector.tabulate (255, makeStartNewline)
val tables =
#[ startTable
, startNonBlankTable
, startSpaceTable
, nonBlankAfterSpaceTable
, spaceAfterNonBlankTable
, newlineTable
, newlineTable
, newlineTable
]
structure StartOfNextWORD =
MakeNextDfaLoop
(struct
val startState = startState
structure Folder =
MakeCharFolderNext
(struct
val startState = startState
val tables = tables
fun finish x = x
fun isFinal currentState =
currentState = nonBlankAfterSpaceState
orelse currentState = newlineToNewline
end)
val fStart = Folder.foldNext
end)
structure StartOfNextWORDForDelete =
MakeNextDfaLoop
(struct
val startState = startState
structure Folder =
MakeCharFolderNext
(struct
val startState = startState
val tables = tables
fun finish x = x
fun isFinal currentState =
currentState = nonBlankAfterSpaceState
orelse currentState = chrToNewline
end)
val fStart = Folder.foldNext
end)
structure EndOfPrevWORD =
MakePrevDfaLoop
(struct
val startState = startState
structure Folder =
MakeCharFolderPrev
(struct
val startState = startState
val tables = tables
fun finish x = x
fun isFinal currentState =
currentState = nonBlankAfterSpaceState
end)
val fStart = Folder.foldPrev
end)
structure StartOfCurrentWORDFolder =
MakeCharFolderPrev
(struct
val startState = startState
val tables = tables
fun isFinal currentState =
currentState = spaceAfterNonBlankState
orelse currentState = chrToNewline
orelse currentState = newlineToNewline
fun finish idx = idx + 1
end)
structure StartOfCurrentWORD =
MakePrevDfaLoopMinus1
(struct
val startState = startState
val fStart = StartOfCurrentWORDFolder.foldPrev
end)
structure StartOfNextWORDStrict =
MakePrevDfaLoop
(struct
val startState = startState
val fStart = StartOfCurrentWORDFolder.foldPrev
end)
fun endOfCurrentWORDFolderIsFinal currentState =
currentState = spaceAfterNonBlankState orelse currentState = chrToNewline
structure EndOfCurrentWORDFolder =
MakeCharFolderNext
(struct
val startState = startState
val tables = tables
val isFinal = endOfCurrentWORDFolderIsFinal
fun finish idx =
Int.max (0, idx - 1)
end)
structure EndOfCurrentWORD =
MakeNextDfaLoopPlus1
(struct
val startState = startState
val fStart = EndOfCurrentWORDFolder.foldNext
end)
structure EndOfCurrentWORDForDelete =
MakeNextDfaLoopPlus1
(struct
val startState = startState
structure Folder =
MakeCharFolderNext
(struct
val startState = startState
val tables = tables
val isFinal = endOfCurrentWORDFolderIsFinal
fun finish idx = idx
end)
val fStart = Folder.foldNext
end)
structure EndOfCurrentWORDStrict =
MakeNextDfaLoop
(struct
val startState = startState
val fStart = EndOfCurrentWORDFolder.foldNext
end)
(* W *)
val startOfNextWORD = StartOfNextWORD.next
val startOfNextWORDForDelete = StartOfNextWORDForDelete.next
(* gE *)
val endOfPrevWORD = EndOfPrevWORD.prev
(* B *)
val startOfCurrentWORD = StartOfCurrentWORD.prev
(* E *)
val endOfCurrentWORD = EndOfCurrentWORD.next
val endOfCurrentWORDForDelete = EndOfCurrentWORDForDelete.next
(* functions to strictly get the start and end of the current word.
* Problem: We want to support Vi motions like viW (selects a single word),
* as well as ciW (change one WORD) and diW (delete one WORD).
*
* The 'startOfCurrentWORD' and 'endOfCurrentWORD' functions do this
* (representing the vi 'B' and 'E' commands respectively),
* except that 'B' goes to the previous WORD if the cursor is on the first
* character of the current WORD, and 'E' goes to the next WORD if the cursor
* is on the last character of the current WORD.
*
* What is meant by "strict" is that these below functions always stay
* within the current WORD, not making the two exceptions mentioned above.
*)
val startOfCurrentWORDStrict = StartOfNextWORDStrict.prev
val endOfCurrentWORDStrict = EndOfCurrentWORDStrict.next
end

View File

@@ -0,0 +1,64 @@
structure ViDlrDfa =
struct
val startState: Word8.word = 0w0
val newlineState: Word8.word = 0w1
val notNewlineState = 0w2
fun makeStart i =
if Char.chr i = #"\n" then newlineState else notNewlineState
val startTable = Vector.tabulate (255, makeStart)
val newlineTable = startTable
val notNewlineTable = startTable
val tables = #[startTable, newlineTable, notNewlineTable]
fun isFinal currentState = currentState = newlineState
structure ViDlr =
MakeNextDfaLoop
(struct
val startState = startState
structure Folder =
MakeCharFolderNext
(struct
val startState = startState
val tables = tables
fun finish x = x - 1
val isFinal = isFinal
end)
fun fStart (idx, absIdx, str, tl, currentState, counter) =
if String.sub (str, idx) = #"\n" then
if counter = 1 then
absIdx
else
Folder.foldNext
(idx + 1, absIdx + 1, str, tl, currentState, counter - 1)
else
Folder.foldNext (idx, absIdx, str, tl, currentState, counter)
end)
structure ViDlrForDelete =
MakeNextDfaLoop
(struct
val startState = startState
structure Folder =
MakeCharFolderNext
(struct
val startState = startState
val tables = tables
fun finish x = x + 1
val isFinal = isFinal
end)
val fStart = Folder.foldNext
end)
val next = ViDlr.next
val nextForDelete = ViDlrForDelete.next
end

View File

@@ -0,0 +1,76 @@
structure ViHDfa =
struct
val startState: Word8.word = 0w0
val oneNewlineState: Word8.word = 0w1
val twoNewlineState: Word8.word = 0w2
val chrState: Word8.word = 0w3
val chrBeforeNewlieState: Word8.word = 0w4
fun makeStart i =
if Char.chr i = #"\n" then oneNewlineState else chrState
fun makeOneNewline i =
if Char.chr i = #"\n" then twoNewlineState else chrBeforeNewlieState
val startTable = Vector.tabulate (255, makeStart)
val oneNewlineTable = Vector.tabulate (255, makeOneNewline)
val twoNewlineTable = oneNewlineTable
val chrTable = startTable
val chrBeforeNewlieTable = startTable
val tables =
#[ startTable
, oneNewlineTable
, twoNewlineTable
, chrTable
, chrBeforeNewlieTable
]
fun next (currentState, chr) =
let val table = Vector.sub (tables, Word8.toInt currentState)
in Vector.sub (table, Char.ord chr)
end
structure ViH =
MakePrevDfaLoop
(struct
val startState = startState
fun loop (idx, absIdx, str, tl, currentState, counter) =
if idx < 0 then
case tl of
str :: tl =>
loop
(String.size str - 1, absIdx, str, tl, currentState, counter)
| [] => 0
else
let
val chr = String.sub (str, idx)
val newState = next (currentState, chr)
in
if newState = chrBeforeNewlieState orelse newState = chrState then
if counter - 1 = ~1 then
absIdx
else
loop (idx - 1, absIdx - 1, str, tl, startState, counter - 1)
else if newState = twoNewlineState then
if counter - 1 = ~1 then
absIdx + 1
else
loop
( idx - 1
, absIdx - 1
, str
, tl
, oneNewlineState
, counter - 1
)
else
loop (idx - 1, absIdx - 1, str, tl, newState, counter)
end
val fStart = loop
end)
val prev = ViH.prev
end

View File

@@ -0,0 +1,53 @@
structure ViLDfa =
struct
val startState: Word8.word = 0w0
val newlineState: Word8.word = 0w1
val chrState: Word8.word = 0w2
val newlineAfterCHrState: Word8.word = 0w3
fun makeStart i =
if Char.chr i = #"\n" then newlineState else chrState
fun makeChr i =
if Char.chr i = #"\n" then newlineAfterCHrState else chrState
val startTable = Vector.tabulate (255, makeStart)
val newlineTable = startTable
val chrTable = Vector.tabulate (255, makeChr)
val newlineAfterCHrTable = startTable
val tables = #[startTable, newlineTable, chrTable, newlineAfterCHrTable]
fun next (currentState, chr) =
let val table = Vector.sub (tables, Word8.toInt currentState)
in Vector.sub (table, Char.ord chr)
end
structure ViL =
MakeNextDfaLoop
(struct
val startState = startState
fun loop (idx, absIdx, str, tl, currentState, counter) =
if idx = String.size str then
case tl of
str :: tl => loop (0, absIdx, str, tl, currentState, counter)
| [] => absIdx
else
let
val chr = String.sub (str, idx)
val newState = next (currentState, chr)
in
if newState = newlineAfterCHrState then
loop (idx + 1, absIdx + 1, str, tl, newState, counter)
else if counter - 1 = ~1 then
absIdx
else
loop (idx + 1, absIdx + 1, str, tl, newState, counter - 1)
end
val fStart = loop
end)
val next = ViL.next
end

View File

@@ -0,0 +1,285 @@
structure ViWordDfa =
struct
val startState: Word8.word = 0w0
val startAlpha: Word8.word = 0w1
val startSpace: Word8.word = 0w2
val startPunct: Word8.word = 0w3
val alphaToSpace: Word8.word = 0w4
val punctToSpace: Word8.word = 0w5
val spaceToAlpha: Word8.word = 0w6
val spaceToPunct: Word8.word = 0w7
val startNewline: Word8.word = 0w8
val newlineToNewline: Word8.word = 0w9
val chrToNewline: Word8.word = 0w10
val newlineToAlpha: Word8.word = 0w11
val newlineToPunct: Word8.word = 0w12
val alphaToPunct: Word8.word = 0w13
val punctToAlpha: Word8.word = 0w14
fun makeStart i =
let
val chr = Char.chr i
in
if Char.isAlphaNum chr orelse chr = #"_" then startAlpha
else if chr = #"\n" then startNewline
else if Char.isSpace chr then startSpace
else startPunct
end
fun makeStartAlpha i =
let
val chr = Char.chr i
in
if Char.isAlphaNum chr orelse chr = #"_" then startAlpha
else if chr = #"\n" then chrToNewline
else if Char.isSpace chr then alphaToSpace
else alphaToPunct
end
fun makeStartSpace i =
let
val chr = Char.chr i
in
if Char.isAlphaNum chr orelse chr = #"_" then spaceToAlpha
else if chr = #"\n" then chrToNewline
else if Char.isSpace chr then startSpace
else spaceToPunct
end
fun makeStartPunct i =
let
val chr = Char.chr i
in
if Char.isAlphaNum chr orelse chr = #"_" then punctToAlpha
else if chr = #"\n" then chrToNewline
else if Char.isSpace chr then punctToSpace
else startPunct
end
fun makeStartNewline i =
let
val chr = Char.chr i
in
if Char.isAlphaNum chr orelse chr = #"_" then newlineToAlpha
else if chr = #"\n" then newlineToNewline
else if Char.isSpace chr then startSpace
else newlineToPunct
end
val startTable = Vector.tabulate (255, makeStart)
val startAlphaTable = Vector.tabulate (255, makeStartAlpha)
val startSpaceTable = Vector.tabulate (255, makeStartSpace)
val startPunctTable = Vector.tabulate (255, makeStartPunct)
val alphaToSpaceTable = startSpaceTable
val punctToSpaceTable = startSpaceTable
val spaceToAlphaTable = startAlphaTable
val spaceToPunctTable = startPunctTable
val newlineTable = Vector.tabulate (255, makeStartNewline)
val tables =
#[ startTable
, startAlphaTable
, startSpaceTable
, startPunctTable
, alphaToSpaceTable
, punctToSpaceTable
, spaceToAlphaTable
, spaceToPunctTable
, newlineTable
, newlineTable
, newlineTable
, startAlphaTable
, startPunctTable
]
structure StartOfNextWord =
MakeNextDfaLoop
(struct
val startState = startState
structure Folder =
MakeCharFolderNext
(struct
val startState = startState
val tables = tables
fun isFinal currentState =
currentState = alphaToPunct orelse currentState = punctToAlpha
orelse currentState = spaceToAlpha
orelse currentState = spaceToPunct
orelse currentState = newlineToNewline
orelse currentState = newlineToAlpha
orelse currentState = newlineToPunct
fun finish x = x
end)
val fStart = Folder.foldNext
end)
(* This is the same as StartOfNextWord, except for the `isFinal` function.
* The difference is that the `isFinal` function here considers
* the state where any character goes to a newline,
* to be a final state.
* This is because, in Vim, the 'w' motion will move past a newline
* when that newline is preceded by a non-newline character.
* However, the 'dw' motion deletes until that newline
* (not including the newline itself).
* It is easier, less fragile, and perhaps clearer,
* to implement the difference using a transition table like this
* than convoluted if-statements. *)
structure StartOfNextWordForDelete =
MakeNextDfaLoop
(struct
val startState = startState
structure Folder =
MakeCharFolderNext
(struct
val startState = startState
val tables = tables
fun isFinal currentState =
currentState = alphaToPunct orelse currentState = punctToAlpha
orelse currentState = spaceToAlpha
orelse currentState = spaceToPunct
orelse currentState = chrToNewline
fun finish x = x
end)
val fStart = Folder.foldNext
end)
structure EndOfPrevWord =
MakePrevDfaLoop
(struct
val startState = startState
structure Folder =
MakeCharFolderPrev
(struct
val startState = startState
val tables = tables
fun isFinal currentState =
currentState = alphaToPunct orelse currentState = punctToAlpha
orelse currentState = spaceToAlpha
orelse currentState = spaceToPunct
orelse currentState = newlineToNewline
orelse currentState = newlineToAlpha
orelse currentState = newlineToPunct
fun finish x = x
end)
val fStart = Folder.foldPrev
end)
structure StartOfCurrentWordFolder =
MakeCharFolderPrev
(struct
val startState = startState
val tables = tables
fun isFinal currentState =
currentState = alphaToSpace orelse currentState = punctToSpace
orelse currentState = alphaToPunct orelse currentState = punctToAlpha
orelse currentState = chrToNewline
orelse currentState = newlineToNewline
fun finish idx = idx + 1
end)
structure StartOfCurrentWord =
MakePrevDfaLoopMinus1
(struct
val startState = startState
val fStart = StartOfCurrentWordFolder.foldPrev
end)
structure StartOfCurrentWordStrict =
MakePrevDfaLoop
(struct
val startState = startState
val fStart = StartOfCurrentWordFolder.foldPrev
end)
fun isFinalForEndOfCurrentWord currentState =
currentState = alphaToSpace orelse currentState = punctToSpace
orelse currentState = alphaToPunct orelse currentState = punctToAlpha
orelse currentState = chrToNewline
structure EndOfCurrentWordFolder =
MakeCharFolderNext
(struct
val startState = startState
val tables = tables
val isFinal = isFinalForEndOfCurrentWord
fun finish x = x - 1
end)
structure EndOfCurrentWord =
MakeNextDfaLoopPlus1
(struct
val startState = startState
val fStart = EndOfCurrentWordFolder.foldNext
end)
structure EndOfCurrentWordStrict =
MakeNextDfaLoop
(struct
val startState = startState
val fStart = EndOfCurrentWordFolder.foldNext
end)
structure EndOfCurrentWordForDelete =
MakeNextDfaLoopPlus1
(struct
val startState = startState
structure Folder =
MakeCharFolderNext
(struct
val startState = startState
val tables = tables
val isFinal = isFinalForEndOfCurrentWord
fun finish x = x
end)
val fStart = Folder.foldNext
end)
(* w *)
val startOfNextWord = StartOfNextWord.next
val startOfNextWordForDelete = StartOfNextWordForDelete.next
(* ge *)
val endOfPrevWord = EndOfPrevWord.prev
(* b *)
val startOfCurrentWord = StartOfCurrentWord.prev
(* e *)
val endOfCurrentWord = EndOfCurrentWord.next
val endOfCurrentWordForDelete = EndOfCurrentWordForDelete.next
(* the meaning of "Strict" and the utility of these two functions
* is described in vi-WORD-dfa.sml *)
val startOfCurrentWordStrict = StartOfCurrentWordStrict.prev
val endOfCurrentWordStrict = EndOfCurrentWordStrict.next
end

1028
shf/fcore/cursor.sml Normal file

File diff suppressed because it is too large Load Diff

103
shf/fcore/move.sml Normal file
View File

@@ -0,0 +1,103 @@
signature MOVE =
sig
val fMove: LineGap.t * int -> int
end
signature MAKE_MOVE =
sig
val move: AppType.app_type * int -> AppType.app_type
end
functor MakeMove(Fn: MOVE): MAKE_MOVE =
struct
fun finish (app: AppType.app_type, buffer, cursorIdx) =
let
val {searchList, bufferModifyTime, ...} = app
in
NormalFinish.buildTextAndClear
(app, buffer, cursorIdx, searchList, [], bufferModifyTime)
end
fun helpMove (app: AppType.app_type, buffer, cursorIdx, count) =
if count = 0 then
finish (app, buffer, cursorIdx)
else
(* move LineGap to cursorIdx, which is necessary for finding newCursorIdx *)
let
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val textLength = #textLength buffer
val newCursorIdx = Fn.fMove (buffer, cursorIdx)
in
if newCursorIdx >= textLength - 1 then
let val newCursorIdx = Int.max (textLength - 1, 0)
in finish (app, buffer, newCursorIdx)
end
else
helpMove (app, buffer, newCursorIdx, count - 1)
end
fun move (app: AppType.app_type, count) =
let val {cursorIdx, buffer, ...} = app
in helpMove (app, buffer, cursorIdx, count)
end
end
structure MoveToStartOfLine = MakeMove (struct val fMove = Cursor.vi0 end)
signature DFA_MOVE =
sig
val fMove: LineGap.t * int * int -> int
end
signature MAKE_DFA_MOVE =
sig
val move: AppType.app_type * int -> AppType.app_type
end
functor MakeDfaMove(Fn: DFA_MOVE): MAKE_DFA_MOVE =
struct
fun move (app: AppType.app_type, count) : AppType.app_type =
let
val {buffer, cursorIdx, searchList, bufferModifyTime, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val cursorIdx = Fn.fMove (buffer, cursorIdx, count)
val textLength = #textLength buffer
in
if cursorIdx >= textLength - 1 then
let
val cursorIdx = Int.max (textLength - 1, 0)
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val cursorIdx =
if Cursor.isOnNewlineAfterChr (buffer, cursorIdx) then cursorIdx - 1
else cursorIdx
in
NormalFinish.buildTextAndClear
(app, buffer, cursorIdx, searchList, [], bufferModifyTime)
end
else
NormalFinish.buildTextAndClear
(app, buffer, cursorIdx, searchList, [], bufferModifyTime)
end
end
structure MoveViH = MakeDfaMove (struct val fMove = Cursor.viH end)
structure MoveViL = MakeDfaMove (struct val fMove = Cursor.viL end)
structure MoveToNextWord = MakeDfaMove (struct val fMove = Cursor.nextWord end)
structure MoveToNextWORD = MakeDfaMove (struct val fMove = Cursor.nextWORD end)
structure MoveToEndOfWord =
MakeDfaMove (struct val fMove = Cursor.endOfWord end)
structure MoveToEndOfWORD =
MakeDfaMove (struct val fMove = Cursor.endOfWORD end)
structure MoveToPrevWord = MakeDfaMove (struct val fMove = Cursor.prevWord end)
structure MoveToPrevWORD = MakeDfaMove (struct val fMove = Cursor.prevWORD end)
structure MoveToEndOfPrevWord =
MakeDfaMove (struct val fMove = Cursor.endOfPrevWord end)
structure MoveToEndOfPrevWORD =
MakeDfaMove (struct val fMove = Cursor.endOfPrevWORD end)
structure MoveToEndOfLine = MakeDfaMove (struct val fMove = Cursor.viDlr end)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
structure NormalDelete =
MakeNormalDelete
(struct
fun initMsgs _ = []
end)

View File

@@ -0,0 +1,155 @@
structure NormalFinish =
struct
open AppType
open MailboxType
open DrawMsg
open InputMsg
fun clearMode app =
NormalModeWith.mode (app, NORMAL_MODE "", [])
fun buildTextAndClear
(app: app_type, buffer, cursorIdx, searchList, msgs, bufferModifyTime) =
let
val
{ windowWidth
, windowHeight
, visualScrollColumn = prevScrollColumn
, startLine = prevLineNumber
, ...
} = app
(* calculate new scroll column and start line, if there are any changes *)
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val visualScrollColumn =
TextScroll.getScrollColumn
(buffer, cursorIdx, windowWidth, prevScrollColumn)
val cursorLine = LineGap.idxToLineNumber (cursorIdx, buffer)
val startLine =
TextScroll.getStartLine
(prevLineNumber, cursorLine, windowHeight, #lineLength buffer)
(* move buffer to new startLine as required by TextBuilder.build *)
val buffer = LineGap.goToLine (startLine, buffer)
val drawMsg = NormalModeTextBuilder.build
( startLine
, cursorIdx
, buffer
, windowWidth
, windowHeight
, searchList
, visualScrollColumn
)
val drawMsg = Vector.concat drawMsg
val drawMsg = DRAW_TEXT drawMsg
val msgs = DRAW drawMsg :: msgs
val mode = NORMAL_MODE ""
in
NormalModeWith.bufferAndCursorIdx
( app
, buffer
, cursorIdx
, mode
, startLine
, searchList
, msgs
, bufferModifyTime
, visualScrollColumn
)
end
fun resizeText (app: app_type, newWidth, newHeight) =
let
val
{ buffer
, windowWidth
, windowHeight
, startLine
, cursorIdx
, searchList
, bufferModifyTime
, visualScrollColumn = prevScrollColumn
, ...
} = app
val newBuffer = LineGap.goToIdx (cursorIdx, buffer)
val visualScrollColumn =
TextScroll.getScrollColumn
(newBuffer, cursorIdx, newWidth, prevScrollColumn)
val newBuffer = LineGap.goToLine (startLine, newBuffer)
val lineIdx = TextBuilderUtils.getLineAbsIdxFromBuffer (startLine, buffer)
val drawMsg = NormalModeTextBuilder.build
( startLine
, cursorIdx
, buffer
, newWidth
, newHeight
, searchList
, visualScrollColumn
)
val drawMsg = Vector.concat drawMsg
val drawMsg = DRAW_TEXT drawMsg
val msgs = [DRAW drawMsg]
in
NormalModeWith.bufferAndSize
( app
, newBuffer
, newWidth
, newHeight
, searchList
, msgs
, bufferModifyTime
, visualScrollColumn
)
end
fun centreToCursor (app: app_type) =
let
val
{ buffer
, windowWidth
, windowHeight
, startLine = prevLineNumber
, cursorIdx
, searchList
, bufferModifyTime
, visualScrollColumn
, ...
} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val cursorLine = LineGap.idxToLineNumber (cursorIdx, buffer)
val startLine = TextScroll.getLineCentre (cursorLine, windowHeight)
val buffer = LineGap.goToLine (startLine, buffer)
val drawMsg = NormalModeTextBuilder.build
( startLine
, cursorIdx
, buffer
, windowWidth
, windowHeight
, searchList
, visualScrollColumn
)
val drawMsg = Vector.concat drawMsg
val drawMsg = DRAW_TEXT drawMsg
val drawMsg = [DRAW drawMsg]
in
NormalModeWith.bufferAndCursorIdx
( app
, buffer
, cursorIdx
, NORMAL_MODE ""
, startLine
, searchList
, drawMsg
, bufferModifyTime
, #visualScrollColumn app
)
end
end

View File

@@ -0,0 +1,203 @@
structure NormalModeWith =
struct
open AppType
fun bufferMsgsAndMode (app: app_type, newBuffer, newMsgs, newMode) =
let
val
{ mode = _
, buffer = _
, msgs = _
, bufferModifyTime
, windowWidth
, windowHeight
, searchList
, visualScrollColumn
, startLine
, cursorIdx
, dfa
} = app
in
{ mode = newMode
, buffer = newBuffer
, msgs = newMsgs
, bufferModifyTime = bufferModifyTime
, windowWidth = windowWidth
, windowHeight = windowHeight
, searchList = searchList
, visualScrollColumn = visualScrollColumn
, startLine = startLine
, cursorIdx = cursorIdx
, dfa = dfa
}
end
fun bufferAndSize
( app: app_type
, newBuffer
, newWidth
, newHeight
, newSearchList
, newMsgs
, newBufferModifyTime
, newVisualScrollColumn
) =
let
val
{ mode
, buffer = _
, bufferModifyTime = _
, windowWidth = _
, windowHeight = _
, searchList = _
, visualScrollColumn = _
, msgs = _
, startLine
, cursorIdx
, dfa
} = app
in
{ mode = mode
, buffer = newBuffer
, bufferModifyTime = newBufferModifyTime
, windowWidth = newWidth
, windowHeight = newHeight
, searchList = newSearchList
, visualScrollColumn = newVisualScrollColumn
, msgs = newMsgs
, startLine = startLine
, cursorIdx = cursorIdx
, dfa = dfa
}
end
fun bufferAndCursorIdx
( app: app_type
, newBuffer
, newCursorIdx
, newMode
, newStartLine
, newSearchList
, newMsgs
, newBufferModifyTime
, newVisualScrollColumn
) =
let
val
{ mode = _
, buffer = _
, bufferModifyTime = _
, cursorIdx = _
, startLine = _
, searchList = _
, visualScrollColumn = _
, msgs = _
, windowWidth
, windowHeight
, dfa
} = app
in
{ mode = newMode
, buffer = newBuffer
, bufferModifyTime = newBufferModifyTime
, cursorIdx = newCursorIdx
, startLine = newStartLine
, searchList = newSearchList
, visualScrollColumn = newVisualScrollColumn
, msgs = newMsgs
, windowWidth = windowWidth
, windowHeight = windowHeight
, dfa = dfa
}
end
fun mode (app: app_type, newMode, newMsgs) =
let
val
{ mode = _
, msgs = _
, buffer
, bufferModifyTime
, searchList
, cursorIdx
, windowWidth
, windowHeight
, startLine
, visualScrollColumn
, dfa
} = app
in
{ mode = newMode
, msgs = newMsgs
, buffer = buffer
, bufferModifyTime = bufferModifyTime
, searchList = searchList
, cursorIdx = cursorIdx
, windowWidth = windowWidth
, windowHeight = windowHeight
, startLine = startLine
, visualScrollColumn = visualScrollColumn
, dfa = dfa
}
end
fun modeAndBuffer (app: app_type, newBuffer, newMode, newMsgs) =
let
val
{ mode = _
, msgs = _
, buffer = _
, bufferModifyTime
, searchList
, cursorIdx
, windowWidth
, windowHeight
, startLine
, visualScrollColumn
, dfa
} = app
in
{ mode = newMode
, msgs = newMsgs
, buffer = newBuffer
, bufferModifyTime = bufferModifyTime
, searchList = searchList
, cursorIdx = cursorIdx
, windowWidth = windowWidth
, windowHeight = windowHeight
, startLine = startLine
, visualScrollColumn = visualScrollColumn
, dfa = dfa
}
end
fun searchList (app: app_type, newSearchList, newBuffer, newBufferModifyTime) =
let
val
{ searchList = _
, buffer = _
, bufferModifyTime
, msgs
, mode
, cursorIdx
, windowWidth
, windowHeight
, startLine
, visualScrollColumn
, dfa
} = app
in
{ searchList = newSearchList
, buffer = newBuffer
, bufferModifyTime = newBufferModifyTime
, msgs = msgs
, mode = mode
, cursorIdx = cursorIdx
, windowWidth = windowWidth
, windowHeight = windowHeight
, startLine = startLine
, visualScrollColumn = visualScrollColumn
, dfa = dfa
}
end
end

View File

@@ -0,0 +1,693 @@
structure NormalMode =
struct
(* parsing functions, deciding what to do while we are in normal mode *)
open AppType
open InputMsg
fun switchToNormalSearchMode (app: app_type, caseSensitive) =
NormalSearchFinish.onSearchChanged
(app, "", PersistentVector.empty, 0, 0, caseSensitive, #buffer app)
fun getNumLength (pos, str) =
if pos = String.size str then
pos
else
let val chr = String.sub (str, pos)
in if Char.isDigit chr then getNumLength (pos + 1, str) else pos
end
fun appendChr (app: app_type, chr, str) =
let
val str = str ^ Char.toString chr
val mode = NORMAL_MODE str
in
NormalModeWith.mode (app, mode, [])
end
local
fun loop (app: app_type, cursorIdx, buffer, searchList, count, time) =
if count = 0 then
NormalDelete.finishAfterDeletingBuffer
(app, #cursorIdx app, buffer, searchList, time, [])
else
let
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val lineStart = Cursor.vi0 (buffer, cursorIdx)
val (buffer, searchList) = SearchList.insert
(lineStart, " ", buffer, searchList, #dfa app)
val buffer = LineGap.goToIdx (lineStart, buffer)
val lineEnd = Cursor.viDlr (buffer, lineStart, 1)
val buffer = LineGap.goToIdx (lineEnd, buffer)
val nextLine = Cursor.viL (buffer, lineEnd, 1)
val count = if lineEnd = nextLine then 0 else count - 1
in
loop (app, nextLine, buffer, searchList, count, time)
end
in
fun indnetLine (app: app_type, count, time) =
let val {buffer, searchList, cursorIdx, ...} = app
in loop (app, cursorIdx, buffer, searchList, count, time)
end
end
local
fun loop (cursorIdx, buffer, searchList, dfa, count) =
if count = 0 then
(buffer, searchList)
else
let
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val lineStart = Cursor.vi0 (buffer, cursorIdx)
val firstNonSpaceChr = Cursor.firstNonSpaceChr (buffer, lineStart)
(* delete from buffer *)
val difference = firstNonSpaceChr - lineStart
val deleteLength = Int.min (difference, 2)
val (buffer, searchList) =
if difference = 0 then
(* can't dedent as there is no leading space *)
(buffer, searchList)
else
SearchList.deleteBufferAndSearchList
(lineStart, deleteLength, buffer, searchList, dfa)
(* get next line to dedent *)
val buffer = LineGap.goToIdx (lineStart, buffer)
val lineEnd = Cursor.viDlr (buffer, lineStart, 1)
val buffer = LineGap.goToIdx (lineEnd, buffer)
val nextLine = Cursor.viL (buffer, lineEnd, 1)
val count = if lineEnd = nextLine then 0 else count - 1
in
loop (nextLine, buffer, searchList, dfa, count)
end
in
fun dedentLine (app: app_type, count, time) =
let
open MailboxType
val {cursorIdx, buffer, searchList, dfa, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val lineStart = Cursor.vi0 (buffer, cursorIdx)
val firstNonSpaceChr = Cursor.firstNonSpaceChr (buffer, lineStart)
(* calculate length to delete *)
val difference = firstNonSpaceChr - lineStart
val deleteLength = Int.min (difference, 2)
(* delete once *)
val (buffer, searchList) =
if deleteLength = 0 then
(buffer, searchList)
else
SearchList.deleteBufferAndSearchList
(lineStart, deleteLength, buffer, searchList, dfa)
(* Calculate nextLine and newCursorIdx.
* The cursorIdx might be past the current line after we dedent.
* If it is, we put the cursorIdx at the last char of the line. *)
val buffer = LineGap.goToIdx (lineStart, buffer)
val lineEnd = Cursor.viDlr (buffer, lineStart, 1)
val buffer = LineGap.goToIdx (lineEnd, buffer)
val nextLine = Cursor.viL (buffer, lineEnd, 1)
val newCursorIdx = Int.min (lineEnd, cursorIdx)
val (buffer, searchList) =
if lineEnd = nextLine then
(* at end of file, so we cannot dedent anymore *)
(buffer, searchList)
else
(* dedent remaining lines specified by count *)
loop (nextLine, buffer, searchList, dfa, count - 1)
val buffer = LineGap.goToStart buffer
in
NormalDelete.finishAfterDeletingBuffer
(app, newCursorIdx, buffer, searchList, time, [])
end
end
fun parseGo (count, app, chrCmd) =
case chrCmd of
#"e" => MoveToEndOfPrevWord.move (app, count)
| #"E" => MoveToEndOfPrevWORD.move (app, count)
| #"g" => NormalMove.moveToStart app
| _ => NormalFinish.clearMode app
fun parseChr (app: app_type, count, chr, str, time) =
case chr of
#"h" => MoveViH.move (app, count)
| #"j" => NormalMove.moveCursorDown (app, count)
| #"k" => NormalMove.moveCursorUp (app, count)
| #"l" => MoveViL.move (app, count)
| #"w" => MoveToNextWord.move (app, count)
| #"W" => MoveToNextWORD.move (app, count)
| #"b" => MoveToPrevWord.move (app, count)
| #"B" => MoveToPrevWORD.move (app, count)
| #"e" => MoveToEndOfWord.move (app, count)
| #"E" => MoveToEndOfWORD.move (app, count)
| #"n" => NormalMove.moveToNextMatch (app, count)
| #"N" => NormalMove.moveToPrevMatch (app, count)
| #"z" => NormalFinish.centreToCursor app
(* can only move to start or end of line once
* so hardcode count as 1 *)
| #"0" =>
(* 0 is a bit of a special case.
* If 0 is pressed without any preceding characters,
* then it should move cursor to the start of the line.
* However, if a number was pressed previously before 0 was,
* then this means user is entering a count.
* In that case, we append 0 to the string. *)
if String.size str > 0 then
let
val lastChr = String.sub (str, String.size str - 1)
in
if Char.isDigit lastChr then
let
val chr = Char.toString chr
val str = str ^ chr
val mode = NORMAL_MODE str
in
NormalModeWith.mode (app, mode, [])
end
else
MoveToStartOfLine.move (app, 1)
end
else
MoveToStartOfLine.move (app, 1)
| #"$" => MoveToEndOfLine.move (app, 1)
| #"^" => NormalMove.firstNonSpaceChr app
| #"G" =>
(* if str has a size larger than 0,
* interpret as "go to line" command;
* else, interpret as a command to move to end *)
if String.size str = 0 then NormalMove.moveToEnd app
else NormalMove.moveToLine (app, count)
| #"%" => NormalMove.moveToMatchingPair app
| #"D" => NormalDelete.deleteToEndOfLine (app, time)
| #"x" => NormalDelete.removeChr (app, count, time)
| #"J" => NormalDelete.removeLineBreaks (app, count, time)
| #"/" => switchToNormalSearchMode (app, false)
| #"?" => switchToNormalSearchMode (app, true)
| #">" => indnetLine (app, count, time)
| #"<" => dedentLine (app, count, time)
(* multi-char commands which can be appended *)
| #"t" => appendChr (app, chr, str)
| #"T" => appendChr (app, chr, str)
| #"y" => appendChr (app, chr, str)
| #"d" => appendChr (app, chr, str)
| #"f" => appendChr (app, chr, str)
| #"F" => appendChr (app, chr, str)
| #"g" => appendChr (app, chr, str)
| #"c" => appendChr (app, chr, str)
| _ =>
(* user may be entering a cmd with more than one chr
* such as "2dw" to delete two word
* so add current chr to mode, and save it in the app state *)
let
val str = if Char.isDigit chr then str ^ Char.toString chr else ""
val mode = NORMAL_MODE str
in
NormalModeWith.mode (app, mode, [])
end
structure ParseDelete =
struct
fun parseDeleteInside (app, chr, time) =
case chr of
#"w" => NormalDelete.deleteInsideWord (app, time)
| #"W" => NormalDelete.deleteInsideWORD (app, time)
| #"(" => NormalDelete.deleteInsidePair (app, #"(", #")", time)
| #")" => NormalDelete.deleteInsidePair (app, #"(", #")", time)
| #"[" => NormalDelete.deleteInsidePair (app, #"[", #"]", time)
| #"]" => NormalDelete.deleteInsidePair (app, #"[", #"]", time)
| #"{" => NormalDelete.deleteInsidePair (app, #"{", #"}", time)
| #"}" => NormalDelete.deleteInsidePair (app, #"{", #"}", time)
| #"<" => NormalDelete.deleteInsidePair (app, #"<", #">", time)
| #">" => NormalDelete.deleteInsidePair (app, #"<", #">", time)
| _ => NormalFinish.clearMode app
fun parseDeleteAround (app, chr, time) =
case chr of
#"w" => NormalDelete.deleteAroundWord (app, time)
| #"W" => NormalDelete.deleteAroundWORD (app, time)
| #"(" => NormalDelete.deleteAroundPair (app, #"(", #")", time)
| #")" => NormalDelete.deleteAroundPair (app, #"(", #")", time)
| #"[" => NormalDelete.deleteAroundPair (app, #"[", #"]", time)
| #"]" => NormalDelete.deleteAroundPair (app, #"[", #"]", time)
| #"{" => NormalDelete.deleteAroundPair (app, #"{", #"}", time)
| #"}" => NormalDelete.deleteAroundPair (app, #"{", #"}", time)
| #"<" => NormalDelete.deleteAroundPair (app, #"<", #">", time)
| #">" => NormalDelete.deleteAroundPair (app, #"<", #">", time)
| _ => NormalFinish.clearMode app
fun parseDeleteTerminal (str, count, app, chrCmd, time) =
case chrCmd of
(* terminal commands: require no input after *)
#"h" => NormalDelete.deleteCharsLeft (app, count, time)
| #"l" => NormalDelete.removeChr (app, count, time)
(* vi's 'j' and 'k' commands move up or down a column
* but 'dj' or 'dk' delete whole lines
* so their implementation differs from
* other cursor motions *)
| #"j" => NormalDelete.deleteLineDown (app, count, time)
| #"k" => NormalDelete.deleteLineUp (app, count, time)
| #"w" => NormalDelete.deleteWord (app, count, time)
| #"W" => NormalDelete.deleteWORD (app, count, time)
| #"b" => NormalDelete.deleteByDfa (app, count, Cursor.prevWord, time)
| #"B" => NormalDelete.deleteByDfa (app, count, Cursor.prevWORD, time)
| #"e" =>
NormalDelete.deleteByDfa (app, count, Cursor.endOfWordForDelete, time)
| #"E" =>
NormalDelete.deleteByDfa (app, count, Cursor.endOfWORDForDelete, time)
| #"0" => NormalDelete.delete (app, 1, Cursor.vi0, time)
| #"$" => NormalDelete.deleteToEndOfLine (app, time)
| #"^" => NormalDelete.deleteToFirstNonSpaceChr (app, time)
| #"d" => NormalDelete.deleteLine (app, count, time)
| #"n" => NormalDelete.deleteToNextMatch (app, count, time)
| #"N" => NormalDelete.deleteToPrevMatch (app, count, time)
| #"%" => NormalDelete.deletePair (app, time)
| #"G" => NormalDelete.deleteToEnd (app, time)
(* non-terminal commands which require appending chr *)
| #"t" => appendChr (app, chrCmd, str)
| #"T" => appendChr (app, chrCmd, str)
| #"f" => appendChr (app, chrCmd, str)
| #"F" => appendChr (app, chrCmd, str)
| #"g" => appendChr (app, chrCmd, str)
| #"i" => appendChr (app, chrCmd, str)
| #"a" => appendChr (app, chrCmd, str)
(* invalid command: reset mode *)
| _ => NormalFinish.clearMode app
fun parseDeleteGo (app, count, chrCmd, time) =
case chrCmd of
#"e" => NormalDelete.deleteToEndOfPrevWord (app, count, time)
| #"E" => NormalDelete.deleteToEndOfPrevWORD (app, count, time)
| #"g" => NormalDelete.deleteToStart (app, time)
| _ => NormalFinish.clearMode app
fun parseDelete (strPos, str, count, app, chrCmd, time) =
if strPos = String.size str - 1 then
parseDeleteTerminal (str, count, app, chrCmd, time)
else
(* have to continue parsing string *)
case String.sub (str, strPos + 1) of
#"t" => NormalDelete.deleteTillNextChr (app, count, chrCmd, time)
| #"T" => NormalDelete.deleteTillPrevChr (app, count, chrCmd, time)
| #"f" => NormalDelete.deleteToNextChr (app, count, chrCmd, time)
| #"F" => NormalDelete.deleteToPrevChr (app, count, chrCmd, time)
| #"g" => parseDeleteGo (app, count, chrCmd, time)
| #"i" => parseDeleteInside (app, chrCmd, time)
| #"a" => parseDeleteAround (app, chrCmd, time)
| _ => NormalFinish.clearMode app
end
structure ParseYankDelete =
struct
fun parseDeleteInside (app, chr, time) =
case chr of
#"w" => NormalYankDelete.deleteInsideWord (app, time)
| #"W" => NormalYankDelete.deleteInsideWORD (app, time)
| #"(" => NormalYankDelete.deleteInsidePair (app, #"(", #")", time)
| #")" => NormalYankDelete.deleteInsidePair (app, #"(", #")", time)
| #"[" => NormalYankDelete.deleteInsidePair (app, #"[", #"]", time)
| #"]" => NormalYankDelete.deleteInsidePair (app, #"[", #"]", time)
| #"{" => NormalYankDelete.deleteInsidePair (app, #"{", #"}", time)
| #"}" => NormalYankDelete.deleteInsidePair (app, #"{", #"}", time)
| #"<" => NormalYankDelete.deleteInsidePair (app, #"<", #">", time)
| #">" => NormalYankDelete.deleteInsidePair (app, #"<", #">", time)
| _ => NormalFinish.clearMode app
fun parseDeleteAround (app, chr, time) =
case chr of
#"w" => NormalYankDelete.deleteAroundWord (app, time)
| #"W" => NormalYankDelete.deleteAroundWORD (app, time)
| #"(" => NormalYankDelete.deleteAroundPair (app, #"(", #")", time)
| #")" => NormalYankDelete.deleteAroundPair (app, #"(", #")", time)
| #"[" => NormalYankDelete.deleteAroundPair (app, #"[", #"]", time)
| #"]" => NormalYankDelete.deleteAroundPair (app, #"[", #"]", time)
| #"{" => NormalYankDelete.deleteAroundPair (app, #"{", #"}", time)
| #"}" => NormalYankDelete.deleteAroundPair (app, #"{", #"}", time)
| #"<" => NormalYankDelete.deleteAroundPair (app, #"<", #">", time)
| #">" => NormalYankDelete.deleteAroundPair (app, #"<", #">", time)
| _ => NormalFinish.clearMode app
fun parseDeleteTerminal (str, count, app, chrCmd, time) =
case chrCmd of
(* terminal commands: require no input after *)
#"h" => NormalYankDelete.deleteCharsLeft (app, count, time)
| #"l" => NormalYankDelete.removeChr (app, count, time)
(* vi's 'j' and 'k' commands move up or down a column
* but 'dj' or 'dk' delete whole lines
* so their implementation differs from
* other cursor motions *)
| #"j" => NormalYankDelete.deleteLineDown (app, count, time)
| #"k" => NormalYankDelete.deleteLineUp (app, count, time)
| #"w" => NormalYankDelete.deleteWord (app, count, time)
| #"W" => NormalYankDelete.deleteWORD (app, count, time)
| #"b" => NormalYankDelete.deleteByDfa (app, count, Cursor.prevWord, time)
| #"B" => NormalYankDelete.deleteByDfa (app, count, Cursor.prevWORD, time)
| #"e" =>
NormalYankDelete.deleteByDfa
(app, count, Cursor.endOfWordForDelete, time)
| #"E" =>
NormalYankDelete.deleteByDfa
(app, count, Cursor.endOfWORDForDelete, time)
| #"0" => NormalYankDelete.delete (app, 1, Cursor.vi0, time)
| #"$" => NormalYankDelete.deleteToEndOfLine (app, time)
| #"^" => NormalYankDelete.deleteToFirstNonSpaceChr (app, time)
| #"d" => NormalYankDelete.deleteLine (app, count, time)
| #"n" => NormalYankDelete.deleteToNextMatch (app, count, time)
| #"N" => NormalYankDelete.deleteToPrevMatch (app, count, time)
| #"%" => NormalYankDelete.deletePair (app, time)
| #"G" => NormalYankDelete.deleteToEnd (app, time)
(* non-terminal commands which require appending chr *)
| #"t" => appendChr (app, chrCmd, str)
| #"T" => appendChr (app, chrCmd, str)
| #"f" => appendChr (app, chrCmd, str)
| #"F" => appendChr (app, chrCmd, str)
| #"g" => appendChr (app, chrCmd, str)
| #"i" => appendChr (app, chrCmd, str)
| #"a" => appendChr (app, chrCmd, str)
(* invalid command: reset mode *)
| _ => NormalFinish.clearMode app
fun parseDeleteGo (app, count, chrCmd, time) =
case chrCmd of
#"e" => NormalYankDelete.deleteToEndOfPrevWord (app, count, time)
| #"E" => NormalYankDelete.deleteToEndOfPrevWORD (app, count, time)
| #"g" => NormalYankDelete.deleteToStart (app, time)
| _ => NormalFinish.clearMode app
fun parseDelete (strPos, str, count, app, chrCmd, time) =
if strPos = String.size str - 1 then
parseDeleteTerminal (str, count, app, chrCmd, time)
else
(* have to continue parsing string *)
case String.sub (str, strPos + 1) of
#"t" => NormalYankDelete.deleteTillNextChr (app, count, chrCmd, time)
| #"T" => NormalYankDelete.deleteTillPrevChr (app, count, chrCmd, time)
| #"f" => NormalYankDelete.deleteToNextChr (app, count, chrCmd, time)
| #"F" => NormalYankDelete.deleteToPrevChr (app, count, chrCmd, time)
| #"g" => parseDeleteGo (app, count, chrCmd, time)
| #"i" => parseDeleteInside (app, chrCmd, time)
| #"a" => parseDeleteAround (app, chrCmd, time)
| _ => NormalFinish.clearMode app
end
structure ParseYank =
struct
fun yankWhenMovingBack (app: app_type, fMove, count) =
let
open DrawMsg
open MailboxType
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val low = fMove (buffer, cursorIdx, count)
val length = cursorIdx - low
val str = LineGap.substring (low, length, buffer)
val msg = YANK str
val mode = NORMAL_MODE ""
in
NormalModeWith.modeAndBuffer (app, buffer, mode, [DRAW msg])
end
fun yankWhenMovingForward (app: app_type, fMove, count) =
let
open DrawMsg
open MailboxType
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val high = fMove (buffer, cursorIdx, count)
val buffer = LineGap.goToIdx (high, buffer)
val length = high - cursorIdx
val str = LineGap.substring (cursorIdx, length, buffer)
val msg = YANK str
val mode = NORMAL_MODE ""
in
NormalModeWith.modeAndBuffer (app, buffer, mode, [DRAW msg])
end
fun parseYankTerminal (str, count, app, chrCmd, time) =
case chrCmd of
#"h" => NormalYank.yankLeft (app, count)
| #"k" => NormalYank.yankLineUp (app, count)
| #"j" => NormalYank.yankLineDown (app, count)
| #"l" => NormalYank.yankRight (app, count)
| #"y" => NormalYank.yankLine (app, count)
| #"0" => NormalYank.yankToStartOfLine app
| #"w" => NormalYank.yankWhenMovingForward (app, Cursor.nextWord, count)
| #"W" => NormalYank.yankWhenMovingForward (app, Cursor.nextWORD, count)
| #"b" => NormalYank.yankWhenMovingBack (app, Cursor.prevWord, count)
| #"B" => NormalYank.yankWhenMovingBack (app, Cursor.prevWORD, count)
| #"e" =>
NormalYank.yankWhenMovingForward
(app, Cursor.endOfWordForDelete, count)
| #"E" =>
NormalYank.yankWhenMovingForward
(app, Cursor.endOfWORDForDelete, count)
| #"$" => NormalYank.yankWhenMovingForward (app, Cursor.viDlr, 1)
| #"^" => NormalYank.yankToFirstNonSpaceChr app
| #"G" => NormalYank.yankToEndOfText app
| #"%" => NormalYank.yankToMatchingPair app
| #"n" => NormalYank.yankToNextMatch (app, count)
| #"N" => NormalYank.yankToPrevMatch (app, count)
| #"x" => NormalYankDelete.removeChr (app, count, time)
(* append non-terminal characters to string *)
| #"d" =>
let (* 'yd' motion, like 'ydw'; meant to be 'yank then delete' *)
in appendChr (app, chrCmd, str)
end
| #"t" => appendChr (app, chrCmd, str)
| #"T" => appendChr (app, chrCmd, str)
| #"f" => appendChr (app, chrCmd, str)
| #"F" => appendChr (app, chrCmd, str)
| #"g" => appendChr (app, chrCmd, str)
| #"i" => appendChr (app, chrCmd, str)
| #"a" => appendChr (app, chrCmd, str)
| _ => NormalFinish.clearMode app
fun parseYankGo (count, app, chrCmd) =
case chrCmd of
#"e" =>
NormalYank.yankWhenMovingBackPlusOne
(app, Cursor.endOfPrevWord, count)
| #"E" =>
NormalYank.yankWhenMovingBackPlusOne
(app, Cursor.endOfPrevWORD, count)
| #"g" => NormalYank.yankToStart app
| _ => NormalFinish.clearMode app
fun parseYankInside (app, chr) =
case chr of
#"w" => NormalYank.yankInsideWord app
| #"W" => NormalYank.yankInsideWORD app
| #"(" => NormalYank.yankInsideChrOpen (app, chr)
| #"[" => NormalYank.yankInsideChrOpen (app, chr)
| #"{" => NormalYank.yankInsideChrOpen (app, chr)
| #"<" => NormalYank.yankInsideChrOpen (app, chr)
| #")" => NormalYank.yankInsideChrClose (app, chr)
| #"]" => NormalYank.yankInsideChrClose (app, chr)
| #"}" => NormalYank.yankInsideChrClose (app, chr)
| #">" => NormalYank.yankInsideChrClose (app, chr)
| _ => NormalFinish.clearMode app
fun parseYankAround (app, chr) =
case chr of
#"(" => NormalYank.yankAroundChrOpen (app, chr)
| #"[" => NormalYank.yankAroundChrOpen (app, chr)
| #"{" => NormalYank.yankAroundChrOpen (app, chr)
| #"<" => NormalYank.yankAroundChrOpen (app, chr)
| #")" => NormalYank.yankAroundChrClose (app, chr)
| #"]" => NormalYank.yankAroundChrClose (app, chr)
| #"}" => NormalYank.yankAroundChrClose (app, chr)
| #">" => NormalYank.yankAroundChrClose (app, chr)
| _ => NormalFinish.clearMode app
fun parseYank (strPos, str, count, app, chrCmd, time) =
if strPos = String.size str - 1 then
parseYankTerminal (str, count, app, chrCmd, time)
else
case String.sub (str, strPos + 1) of
#"t" => NormalYank.yankTillNextChr (app, count, chrCmd)
| #"T" => NormalYank.yankTillPrevChr (app, count, chrCmd)
| #"f" => NormalYank.yankToNextChr (app, count, chrCmd)
| #"F" => NormalYank.yankToPrevChr (app, count, chrCmd)
| #"g" => parseYankGo (count, app, chrCmd)
| #"i" => parseYankInside (app, chrCmd)
| #"a" => parseYankAround (app, chrCmd)
| #"d" =>
ParseYankDelete.parseDelete
(strPos + 1, str, count, app, chrCmd, time)
| _ => NormalFinish.clearMode app
end
(* useful reference as list of non-terminal commands *)
fun parseAfterCount (strPos, str, count, app, chrCmd, time) =
(* we are trying to parse multi-char but non-terminal strings here.
* For example, we don't want to parse 3w which is a terminal commmand
* to go 3 words forwards
* but we do want to parse 3d which is a non-terminal command
* which can be made terminal by adding "w" or "e" at the end.
* *)
case String.sub (str, strPos) of
#"t" => NormalMove.tillNextChr (app, count, chrCmd)
| #"T" => NormalMove.tillPrevChr (app, count, chrCmd)
| #"y" => ParseYank.parseYank (strPos, str, count, app, chrCmd, time)
| #"d" => ParseDelete.parseDelete (strPos, str, count, app, chrCmd, time)
| #"f" => NormalMove.toNextChr (app, count, chrCmd)
| #"F" => NormalMove.toPrevChr (app, count, chrCmd)
| #"g" => (* go *) parseGo (count, app, chrCmd)
| #"c" => (* change *) NormalFinish.clearMode app
| _ =>
(* isn't a non-terminal cmd
* this case should never happen*)
NormalFinish.clearMode app
fun parseNormalModeCommand (app, str, chrCmd, time) =
if String.size str = 0 then
parseChr (app, 1, chrCmd, str, time)
else if String.size str = 1 then
case Int.fromString str of
SOME count => parseChr (app, count, chrCmd, str, time)
| NONE => parseAfterCount (0, str, 1, app, chrCmd, time)
else
let
val numLength = getNumLength (0, str)
val count = String.substring (str, 0, numLength)
val count =
case Int.fromString count of
SOME x => x
| NONE => 1
in
if numLength = String.size str then
(* reached end of str; str only contained numbers *)
parseChr (app, count, chrCmd, str, time)
else
(* continue parsing. *)
parseAfterCount (numLength, str, count, app, chrCmd, time)
end
structure LeftArrow =
struct
fun parseLeftArrowCommand (strPos, str, count, app, time) =
case String.sub (str, strPos) of
#"y" =>
if strPos + 1 = String.size str then
(* terminal command, so simple yank *)
raise Fail "left-arrow-yank unimplemnted"
else
(case String.sub (str, strPos + 1) of
#"d" => NormalYankDelete.deleteCharsLeft (app, count, time)
| _ => NormalFinish.clearMode app)
| #"d" => NormalDelete.deleteCharsLeft (app, count, time)
| _ => NormalFinish.clearMode app
fun parse (app, str, time) =
if String.size str = 0 then
MoveViH.move (app, 1)
else if String.size str = 1 then
case Int.fromString str of
SOME count => MoveViH.move (app, count)
| NONE => parseLeftArrowCommand (0, str, 1, app, time)
else
let
val numLength = getNumLength (0, str)
val count = String.substring (str, 0, numLength)
val count =
case Int.fromString count of
SOME x => x
| NONE => 1
in
if numLength = String.size str then
(* reached end of string; string only contained numbers *)
MoveViH.move (app, count)
else
parseLeftArrowCommand (numLength, str, count, app, time)
end
end
structure RightArrow =
struct
fun parseRightArrowCommand (strPos, str, count, app, time) =
case String.sub (str, strPos) of
#"y" =>
if strPos + 1 = String.size str then
raise Fail "right-arrow-yank unimplemnted"
else
(case String.sub (str, strPos + 1) of
#"d" => NormalYankDelete.removeChr (app, count, time)
| _ => NormalFinish.clearMode app)
| #"d" => NormalDelete.removeChr (app, count, time)
| _ => NormalFinish.clearMode app
fun parse (app, str, time) =
if String.size str = 0 then
MoveViL.move (app, 1)
else if String.size str = 1 then
case Int.fromString str of
SOME count => MoveViL.move (app, count)
| NONE => parseRightArrowCommand (0, str, 1, app, time)
else
let
val numLength = getNumLength (0, str)
val count = String.substring (str, 0, numLength)
val count =
case Int.fromString count of
SOME x => x
| NONE => 1
in
if numLength = String.size str then
(* reached end of string; string only contained numbers *)
MoveViH.move (app, count)
else
parseRightArrowCommand (numLength, str, count, app, time)
end
end
fun update (app, str, msg, time) =
case msg of
CHAR_EVENT chrCmd => parseNormalModeCommand (app, str, chrCmd, time)
| KEY_ESC => NormalFinish.clearMode app
| RESIZE_EVENT (width, height) =>
NormalFinish.resizeText (app, width, height)
| ARROW_RIGHT => RightArrow.parse (app, str, time)
| ARROW_LEFT => LeftArrow.parse (app, str, time)
| ARROW_UP => NormalFinish.clearMode app
| ARROW_DOWN => NormalFinish.clearMode app
| KEY_ENTER => NormalFinish.clearMode app
| KEY_BACKSPACE => NormalFinish.clearMode app
end

View File

@@ -0,0 +1,586 @@
structure NormalMove =
struct
open AppType
fun moveToStart (app: app_type) : AppType.app_type =
let
val
{ buffer
, windowWidth
, windowHeight
, searchList
, bufferModifyTime
, visualScrollColumn
, ...
} = app
val cursorIdx = 0
val startLine = 0
val buffer = LineGap.goToStart buffer
val drawMsg = NormalModeTextBuilder.build
( startLine
, cursorIdx
, buffer
, windowWidth
, windowHeight
, searchList
, visualScrollColumn
)
val drawMsg = Vector.concat drawMsg
val drawMsg = DrawMsg.DRAW_TEXT drawMsg
val drawMsg = [MailboxType.DRAW drawMsg]
val mode = NORMAL_MODE ""
in
NormalModeWith.bufferAndCursorIdx
( app
, buffer
, cursorIdx
, mode
, startLine
, searchList
, drawMsg
, bufferModifyTime
, 0
)
end
fun moveToEnd (app: app_type) =
let
val
{ buffer
, windowWidth
, windowHeight
, searchList
, bufferModifyTime
, visualScrollColumn = prevScrollColumn
, startLine = prevLineNumber
, ...
} = app
val buffer = LineGap.goToEnd buffer
val {line = bufferLine, textLength, ...} = buffer
val bufferIdx = Int.max (0, textLength - 1)
val bufferLine = bufferLine - 1
val buffer = LineGap.goToIdx (bufferIdx, buffer)
val bufferIdx =
if Cursor.isOnNewlineAfterChr (buffer, bufferIdx) then
Int.max (0, bufferIdx - 1)
else
bufferIdx
val buffer = LineGap.goToIdx (bufferIdx, buffer)
val visualScrollColumn =
TextScroll.getScrollColumn
(buffer, bufferIdx, windowWidth, prevScrollColumn)
val bufferLine =
TextScroll.getStartLine
(prevLineNumber, bufferLine, windowHeight, #lineLength buffer)
val buffer = LineGap.goToLine (bufferLine, buffer)
val drawMsg = NormalModeTextBuilder.build
( bufferLine
, bufferIdx
, buffer
, windowWidth
, windowHeight
, searchList
, visualScrollColumn
)
val drawMsg = Vector.concat drawMsg
val drawMsg = DrawMsg.DRAW_TEXT drawMsg
val drawMsg = [MailboxType.DRAW drawMsg]
val mode = NORMAL_MODE ""
in
NormalModeWith.bufferAndCursorIdx
( app
, buffer
, bufferIdx
, mode
, bufferLine
, searchList
, drawMsg
, bufferModifyTime
, visualScrollColumn
)
end
fun finishMoveCursorUpDown
(app: app_type, newCursorLineNumber, buffer, column, lineIdx) =
let
val
{ windowWidth
, windowHeight
, visualScrollColumn = prevScrollColumn
, startLine = prevLineNumber
, searchList
, bufferModifyTime
, ...
} = app
val buffer = LineGap.goToIdx (lineIdx, buffer)
val endOfLineIdx = Cursor.viDlr (buffer, lineIdx, 1)
val endOfLineIdx =
if endOfLineIdx >= #textLength buffer - 1 then
Int.max (0, #textLength buffer - 1)
else
endOfLineIdx
val cursorIdx = Int.min (endOfLineIdx, lineIdx + column)
val buffer = LineGap.goToIdx (cursorIdx, buffer)
(* create draw message *)
val visualScrollColumn =
TextScroll.getScrollColumn
(buffer, cursorIdx, windowWidth, prevScrollColumn)
val startLine =
TextScroll.getStartLine
( prevLineNumber
, newCursorLineNumber
, windowHeight
, #lineLength buffer
)
val buffer = LineGap.goToLine (startLine, buffer)
val drawMsg = NormalModeTextBuilder.build
( startLine
, cursorIdx
, buffer
, windowWidth
, windowHeight
, searchList
, visualScrollColumn
)
val drawMsg = Vector.concat drawMsg
val drawMsg = DrawMsg.DRAW_TEXT drawMsg
val drawMsg = [MailboxType.DRAW drawMsg]
val mode = NORMAL_MODE ""
in
NormalModeWith.bufferAndCursorIdx
( app
, buffer
, cursorIdx
, mode
, startLine
, searchList
, drawMsg
, bufferModifyTime
, visualScrollColumn
)
end
fun moveCursorUp (app: app_type, count) =
let
val {cursorIdx, buffer, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val startOfLine = Cursor.vi0 (buffer, cursorIdx)
in
if Cursor.isCursorAtStartOfLine (buffer, cursorIdx) then
let
val cursorLineNumber = LineGap.idxToLineNumber (cursorIdx + 1, buffer)
val newCursorLineNumber = Int.max (0, cursorLineNumber - count)
val buffer = LineGap.goToLine (newCursorLineNumber, buffer)
val lineIdx = LineGap.lineNumberToIdx (newCursorLineNumber, buffer)
val lineIdx =
if Cursor.isPrevChrStartOfLine (buffer, lineIdx) then lineIdx
else lineIdx - 1
val buffer = LineGap.goToIdx (lineIdx, buffer)
val lineIdx = Cursor.vi0 (buffer, lineIdx)
val lineIdx = Int.max (0, lineIdx)
in
finishMoveCursorUpDown (app, newCursorLineNumber, buffer, 0, lineIdx)
end
else
let
val cursorLineNumber = LineGap.idxToLineNumber (cursorIdx, buffer)
val newCursorLineNumber = Int.max (cursorLineNumber - count, 0)
val buffer = LineGap.goToLine (newCursorLineNumber, buffer)
val lineIdx =
LineGap.lineNumberToIdx (newCursorLineNumber, buffer) + 1
val column = cursorIdx - startOfLine
val column = if newCursorLineNumber = 0 then column - 1 else column
in
finishMoveCursorUpDown
(app, newCursorLineNumber, buffer, column, lineIdx)
end
end
fun moveCursorDown (app: app_type, count) =
let
val
{ windowWidth
, windowHeight
, cursorIdx
, buffer
, startLine = prevLineNumber
, searchList
, bufferModifyTime
, visualScrollColumn = prevScrollColumn
, ...
} = app
(* calculate new idx to move to *)
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val startOfLine = Cursor.vi0 (buffer, cursorIdx)
val column = cursorIdx - startOfLine
in
if Cursor.isCursorAtStartOfLine (buffer, cursorIdx) then
let
val cursorLineNumber = LineGap.idxToLineNumber (cursorIdx + 1, buffer)
val newCursorLineNumber = cursorLineNumber + count
val buffer = LineGap.goToLine (newCursorLineNumber, buffer)
val lineIdx = LineGap.lineNumberToIdx (newCursorLineNumber, buffer)
val lineIdx =
if Cursor.isPrevChrStartOfLine (buffer, lineIdx) then lineIdx
else lineIdx - 1
val buffer = LineGap.goToIdx (lineIdx, buffer)
val lineIdx = Cursor.vi0 (buffer, lineIdx)
val lineIdx =
if lineIdx >= #textLength buffer - 1 then
Int.max (0, #textLength buffer - 1)
else
lineIdx
in
finishMoveCursorUpDown (app, newCursorLineNumber, buffer, 0, lineIdx)
end
else
let
val cursorLineNumber = LineGap.idxToLineNumber (cursorIdx, buffer)
val newCursorLineNumber = cursorLineNumber + count
val buffer = LineGap.goToLine (newCursorLineNumber, buffer)
val lineIdx = LineGap.lineNumberToIdx (newCursorLineNumber, buffer)
val buffer = LineGap.goToIdx (lineIdx, buffer)
in
if lineIdx >= #textLength buffer - 1 then
(* we reached last line *)
let
val lineIdx = Int.max (#textLength buffer - 1, 0)
val buffer = LineGap.goToIdx (lineIdx, buffer)
val lineIdx =
if Cursor.isOnNewlineAfterChr (buffer, lineIdx) then lineIdx - 1
else lineIdx
val startOfLine = Cursor.vi0 (buffer, lineIdx)
in
if cursorIdx >= startOfLine then
(* we are already on last line so don't move *)
NormalFinish.buildTextAndClear
(app, buffer, cursorIdx, searchList, [], bufferModifyTime)
else
finishMoveCursorUpDown
(app, newCursorLineNumber, buffer, column, startOfLine)
end
else
let
val lineIdx =
if lineIdx >= #textLength buffer - 2 then
Int.max (0, #textLength buffer - 2)
else
lineIdx
val buffer = LineGap.goToIdx (lineIdx, buffer)
val lineIdx =
if Cursor.isOnNewlineAfterChr (buffer, lineIdx) then lineIdx + 1
else lineIdx
in
finishMoveCursorUpDown
(app, newCursorLineNumber, buffer, column, lineIdx)
end
end
end
fun moveToLine (app: app_type, reqLine) =
let
val reqLine = reqLine - 1
in
if reqLine = 0 then
moveToStart app
else
let
val
{ windowWidth
, windowHeight
, buffer
, startLine = prevLineNumber
, searchList
, bufferModifyTime
, visualScrollColumn = prevScrollColumn
, ...
} = app
val buffer = LineGap.goToLine (reqLine, buffer)
(* get idx of first chr after linebreak *)
val cursorIdx = LineGap.lineNumberToIdx (reqLine, buffer)
val buffer = LineGap.goToIdx (cursorIdx, buffer)
(* we got the line start idx, but we want to move to the index
* after it, where the first character of the line is.
* Unless the next character is a line break,
* in which case we want to stay at the current idx. *)
val cursorIdx =
if Cursor.isNextChrEndOfLine (buffer, cursorIdx) then cursorIdx
else cursorIdx + 1
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val visualScrollColumn =
TextScroll.getScrollColumn
(buffer, cursorIdx, windowWidth, prevScrollColumn)
val cursorLine = LineGap.idxToLineNumber (cursorIdx, buffer)
val startLine =
TextScroll.getStartLine
(prevLineNumber, cursorLine, windowHeight, #lineLength buffer)
val buffer = LineGap.goToLine (startLine, buffer)
val drawMsg = NormalModeTextBuilder.build
( startLine
, cursorIdx
, buffer
, windowWidth
, windowHeight
, searchList
, visualScrollColumn
)
val drawMsg = Vector.concat drawMsg
val drawMsg = DrawMsg.DRAW_TEXT drawMsg
val drawMsg = [MailboxType.DRAW drawMsg]
val mode = NORMAL_MODE ""
in
NormalModeWith.bufferAndCursorIdx
( app
, buffer
, cursorIdx
, mode
, startLine
, searchList
, drawMsg
, bufferModifyTime
, visualScrollColumn
)
end
end
fun moveToMatchingPair (app: app_type) =
let
val
{ buffer
, cursorIdx
, windowWidth
, windowHeight
, startLine = prevLineNumber
, searchList
, bufferModifyTime
, visualScrollColumn = prevScrollColumn
, ...
} = app
(* move LineGap and buffer to start of line *)
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val cursorIdx = Cursor.nextPairChr (buffer, cursorIdx)
in
if cursorIdx = ~1 then
NormalFinish.clearMode app
else
let
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val cursorIdx = Cursor.matchPair (buffer, cursorIdx)
in
if cursorIdx = ~1 then
NormalFinish.clearMode app
else
let
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val visualScrollColumn =
TextScroll.getScrollColumn
(buffer, cursorIdx, windowWidth, prevScrollColumn)
val cursorLine = LineGap.idxToLineNumber (cursorIdx, buffer)
val startLine =
TextScroll.getStartLine
(prevLineNumber, cursorLine, windowHeight, #lineLength buffer)
val buffer = LineGap.goToLine (startLine, buffer)
val drawMsg = NormalModeTextBuilder.build
( startLine
, cursorIdx
, buffer
, windowWidth
, windowHeight
, searchList
, visualScrollColumn
)
val drawMsg = Vector.concat drawMsg
val drawMsg = DrawMsg.DRAW_TEXT drawMsg
val drawMsg = [MailboxType.DRAW drawMsg]
in
NormalModeWith.bufferAndCursorIdx
( app
, buffer
, cursorIdx
, NORMAL_MODE ""
, startLine
, searchList
, drawMsg
, bufferModifyTime
, visualScrollColumn
)
end
end
end
fun firstNonSpaceChr (app: app_type) =
let
val
{ buffer
, cursorIdx
, windowWidth
, windowHeight
, startLine
, searchList
, bufferModifyTime
, ...
} = app
(* move LineGap and buffer to start of line *)
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val cursorIdx = Cursor.vi0 (buffer, cursorIdx)
(* move cursorIdx to first character on line *)
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val cursorIdx = Cursor.firstNonSpaceChr (buffer, cursorIdx)
in
NormalFinish.buildTextAndClear
(app, buffer, cursorIdx, searchList, [], bufferModifyTime)
end
fun helpMoveToChr (app: app_type, buffer, cursorIdx, count, fMove, chr) =
if count = 0 then
NormalFinish.buildTextAndClear
(app, buffer, cursorIdx, #searchList app, [], #bufferModifyTime app)
else
let
(* move LineGap to cursorIdx, which is necessary for finding newCursorIdx *)
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val newCursorIdx = fMove (buffer, cursorIdx, chr)
val newCount = if cursorIdx = newCursorIdx then 0 else count - 1
in
helpMoveToChr (app, buffer, newCursorIdx, newCount, fMove, chr)
end
fun moveToChr (app: app_type, count, fMove, chr) =
let val {cursorIdx, buffer, ...} = app
in helpMoveToChr (app, buffer, cursorIdx, count, fMove, chr)
end
fun moveToNextMatch (app: app_type, count) =
let
val
{ cursorIdx
, searchList
, buffer
, bufferModifyTime
, visualScrollColumn
, ...
} = app
val newCursorIdx =
PersistentVector.nextMatch (cursorIdx, searchList, count)
in
if newCursorIdx = ~1 then
NormalFinish.clearMode app
else
NormalFinish.buildTextAndClear
(app, buffer, newCursorIdx, searchList, [], bufferModifyTime)
end
fun moveToPrevMatch (app: app_type, count) =
let
val {cursorIdx, searchList, buffer, bufferModifyTime, ...} = app
val newCursorIdx =
PersistentVector.prevMatch (cursorIdx, searchList, count)
in
if newCursorIdx = ~1 then
NormalFinish.clearMode app
else
NormalFinish.buildTextAndClear
(app, buffer, newCursorIdx, searchList, [], bufferModifyTime)
end
fun toNextChr (app: app_type, count, chr) =
let
val {cursorIdx, buffer, searchList, bufferModifyTime, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val newCursorIdx =
Cursor.toNextChr (buffer, cursorIdx, {findChr = chr, count = count})
in
if newCursorIdx = ~1 then
NormalFinish.clearMode app
else
NormalFinish.buildTextAndClear
(app, buffer, newCursorIdx, searchList, [], bufferModifyTime)
end
fun tillNextChr (app: app_type, count, chr) =
let
val {cursorIdx, buffer, searchList, bufferModifyTime, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val newCursorIdx =
Cursor.toNextChr (buffer, cursorIdx, {findChr = chr, count = count})
in
if newCursorIdx = ~1 then
NormalFinish.clearMode app
else
NormalFinish.buildTextAndClear
(app, buffer, newCursorIdx - 1, searchList, [], bufferModifyTime)
end
fun toPrevChr (app: app_type, count, chr) =
let
val {cursorIdx, buffer, searchList, bufferModifyTime, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val newCursorIdx =
Cursor.toPrevChr (buffer, cursorIdx, {findChr = chr, count = count})
in
if newCursorIdx = ~1 then
NormalFinish.clearMode app
else
NormalFinish.buildTextAndClear
(app, buffer, newCursorIdx, searchList, [], bufferModifyTime)
end
fun tillPrevChr (app: app_type, count, chr) =
let
val {cursorIdx, buffer, searchList, bufferModifyTime, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val newCursorIdx =
Cursor.toPrevChr (buffer, cursorIdx, {findChr = chr, count = count})
in
if newCursorIdx = ~1 then
NormalFinish.clearMode app
else
NormalFinish.buildTextAndClear
(app, buffer, newCursorIdx + 1, searchList, [], bufferModifyTime)
end
end

View File

@@ -0,0 +1,156 @@
structure NormalSearchFinish =
struct
open AppType
open DrawMsg
fun onSearchChanged
( app: app_type
, searchString
, tempSearchList
, searchCursorIdx
, searchScrollColumn
, caseSensitive
, buffer
) =
let
val
{ buffer
, cursorIdx
, startLine = prevLineNumber
, windowWidth
, windowHeight
, visualScrollColumn
, ...
} = app
val searchScrollColumn =
TextScroll.getScrollColumnFromString
(searchCursorIdx, windowWidth, searchScrollColumn)
val mode = NORMAL_SEARCH_MODE
{ searchString = searchString
, tempSearchList = tempSearchList
, searchCursorIdx = searchCursorIdx
, searchScrollColumn = searchScrollColumn
, caseSensitive = caseSensitive
}
val floatWindowWidth = Real32.fromInt windowWidth
val floatWindowHeight = Real32.fromInt windowHeight
val searchStringPosY = windowHeight - TextConstants.ySpace - 5
val initialTextAcc = SearchBar.build
( searchString
, 5
, searchStringPosY
, windowWidth
, floatWindowWidth
, floatWindowHeight
, searchCursorIdx
, searchScrollColumn
, caseSensitive
)
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val cursorLine = LineGap.idxToLineNumber (cursorIdx, buffer)
val startLine =
TextScroll.getStartLine
(prevLineNumber, cursorLine, windowHeight, #lineLength buffer)
val buffer = LineGap.goToLine (startLine, buffer)
val remainingWindowHeight = windowHeight - (TextConstants.ySpace * 2)
val drawMsg = NormalModeTextBuilder.startBuild
( startLine
, cursorIdx
, buffer
, windowWidth
, remainingWindowHeight
, floatWindowWidth
, floatWindowHeight
, tempSearchList
, visualScrollColumn
, initialTextAcc
)
val drawMsg = Vector.concat drawMsg
val drawMsg = DrawMsg.DRAW_TEXT drawMsg
val msgs = [MailboxType.DRAW drawMsg]
in
NormalSearchModeWith.changeTempSearchString
(app, buffer, startLine, mode, msgs)
end
fun resize
( app: app_type
, newWindowWidth
, newWindowHeight
, searchString
, searchCursorIdx
, tempSearchList
, searchScrollColumn
, caseSensitive
) =
let
val
{buffer, cursorIdx, startLine = prevLineNumber, visualScrollColumn, ...} =
app
val floatWindowWidth = Real32.fromInt newWindowWidth
val floatWindowHeight = Real32.fromInt newWindowHeight
val searchScrollColumn =
TextScroll.getScrollColumnFromString
(searchCursorIdx, newWindowWidth, searchScrollColumn)
val mode = NORMAL_SEARCH_MODE
{ searchString = searchString
, tempSearchList = tempSearchList
, searchCursorIdx = searchCursorIdx
, searchScrollColumn = searchScrollColumn
, caseSensitive = caseSensitive
}
val searchStringPosY = newWindowHeight - TextConstants.ySpace - 5
val initialTextAcc = SearchBar.build
( searchString
, 5
, searchStringPosY
, newWindowWidth
, floatWindowWidth
, floatWindowHeight
, searchCursorIdx
, searchScrollColumn
, caseSensitive
)
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val cursorLine = LineGap.idxToLineNumber (cursorIdx, buffer)
val startLine =
TextScroll.getStartLine
(prevLineNumber, cursorLine, newWindowHeight, #lineLength buffer)
val buffer = LineGap.goToLine (startLine, buffer)
val remainingWindowHeight = newWindowHeight - (TextConstants.ySpace * 2)
val drawMsg = NormalModeTextBuilder.startBuild
( startLine
, cursorIdx
, buffer
, newWindowWidth
, remainingWindowHeight
, floatWindowWidth
, floatWindowHeight
, tempSearchList
, visualScrollColumn
, initialTextAcc
)
val drawMsg = Vector.concat drawMsg
val drawMsg = DrawMsg.DRAW_TEXT drawMsg
val msgs = [MailboxType.DRAW drawMsg]
in
NormalSearchModeWith.bufferAndSize
(app, mode, buffer, newWindowWidth, newWindowHeight, msgs)
end
end

View File

@@ -0,0 +1,140 @@
structure NormalSearchModeWith =
struct
open AppType
fun returnToNormalMode
( app: app_type
, newBuffer
, newSearchList
, newStartLine
, newMode
, newDfa
, newMsgs
) =
let
val
{ mode = _
, buffer = _
, searchList = _
, startLine = _
, msgs = _
, dfa = _
, bufferModifyTime
, windowWidth
, windowHeight
, cursorIdx
, visualScrollColumn
} = app
in
{ mode = newMode
, buffer = newBuffer
, searchList = newSearchList
, startLine = newStartLine
, dfa = newDfa
, bufferModifyTime = bufferModifyTime
, msgs = newMsgs
, windowWidth = windowWidth
, windowHeight = windowHeight
, cursorIdx = cursorIdx
, visualScrollColumn = visualScrollColumn
}
end
fun changeTempSearchString
(app: app_type, newBuffer, newStartLine, newMode, newMsgs) =
let
val
{ mode = _
, buffer = _
, searchList
, startLine = _
, msgs = _
, bufferModifyTime
, windowWidth
, windowHeight
, cursorIdx
, visualScrollColumn
, dfa
} = app
in
{ mode = newMode
, buffer = newBuffer
, startLine = newStartLine
, msgs = newMsgs
, searchList = searchList
, bufferModifyTime = bufferModifyTime
, windowWidth = windowWidth
, windowHeight = windowHeight
, cursorIdx = cursorIdx
, visualScrollColumn = visualScrollColumn
, dfa = dfa
}
end
fun searchList (app: app_type, newSearchList) =
let
val
{ mode
, buffer
, searchList = _
, startLine
, msgs
, bufferModifyTime
, windowWidth
, windowHeight
, cursorIdx
, visualScrollColumn
, dfa
} = app
in
{ mode = mode
, searchList = newSearchList
, buffer = buffer
, startLine = startLine
, msgs = msgs
, bufferModifyTime = bufferModifyTime
, windowWidth = windowWidth
, windowHeight = windowHeight
, cursorIdx = cursorIdx
, visualScrollColumn = visualScrollColumn
, dfa = dfa
}
end
fun bufferAndSize
( app: app_type
, newMode
, newBuffer
, newWindowWidth
, newWindowHeight
, newMsgs
) =
let
val
{ mode = _
, windowWidth = _
, windowHeight = _
, msgs = _
, buffer = _
, searchList
, startLine
, bufferModifyTime
, cursorIdx
, visualScrollColumn
, dfa
} = app
in
{ mode = newMode
, buffer = newBuffer
, windowWidth = newWindowWidth
, windowHeight = newWindowHeight
, msgs = newMsgs
, searchList = searchList
, startLine = startLine
, bufferModifyTime = bufferModifyTime
, cursorIdx = cursorIdx
, visualScrollColumn = visualScrollColumn
, dfa = dfa
}
end
end

View File

@@ -0,0 +1,274 @@
structure NormalSearchMode =
struct
open AppType
open InputMsg
open MailboxType
fun buildTempSearchList (searchString, buffer, cursorIdx, caseSensitive) =
let
val dfa =
if caseSensitive then CaseSensitiveDfa.fromString searchString
else CaseInsensitiveDfa.fromString searchString
in
SearchList.buildRange (buffer, cursorIdx + 1111, dfa)
end
fun addChr
( app: app_type
, searchString
, searchCursorIdx
, searchScrollColumn
, caseSensitive
, chr
) =
let
val {cursorIdx, buffer, ...} = app
val c = String.implode [chr]
val searchString =
if searchCursorIdx = String.size searchString then
searchString ^ c
else
let
val sub1 = Substring.extract (searchString, 0, SOME searchCursorIdx)
val sub2 = Substring.full c
val sub3 = Substring.extract (searchString, searchCursorIdx, NONE)
in
Substring.concat [sub1, sub2, sub3]
end
val searchCursorIdx = searchCursorIdx + 1
val buffer = LineGap.goToIdx (cursorIdx - 1111, buffer)
val (buffer, tempSearchList) =
buildTempSearchList (searchString, buffer, cursorIdx, caseSensitive)
in
NormalSearchFinish.onSearchChanged
( app
, searchString
, tempSearchList
, searchCursorIdx
, searchScrollColumn
, caseSensitive
, buffer
)
end
(* return to normal mode, keeping the same searchString and searchList
* from before entering this mode. *)
fun exitToNormalMode (app: app_type) =
let
val {buffer, cursorIdx, searchList, bufferModifyTime, ...} = app
in
NormalFinish.buildTextAndClear
(app, buffer, cursorIdx, searchList, [], bufferModifyTime)
end
(* save search string and searchList and return to normal mode *)
fun saveSearch (app: app_type, searchString, caseSensitive, time) =
let
val
{ buffer
, cursorIdx
, windowWidth
, windowHeight
, startLine
, visualScrollColumn
, ...
} = app
val dfa =
if caseSensitive then CaseSensitiveDfa.fromString searchString
else CaseInsensitiveDfa.fromString searchString
val buffer = LineGap.goToStart buffer
val (buffer, searchList) = SearchList.build (buffer, dfa)
(* move LineGap to first line displayed on screen *)
val buffer = LineGap.goToLine (startLine, buffer)
(* move buffer to new startLine as required by TextBuilder.build *)
val buffer = LineGap.goToLine (startLine, buffer)
val drawMsg = NormalModeTextBuilder.build
( startLine
, cursorIdx
, buffer
, windowWidth
, windowHeight
, searchList
, visualScrollColumn
)
val drawMsg = Vector.concat drawMsg
val drawMsg = DrawMsg.DRAW_TEXT drawMsg
val msgs = [DRAW drawMsg]
val mode = NORMAL_MODE ""
in
NormalSearchModeWith.returnToNormalMode
(app, buffer, searchList, startLine, mode, dfa, msgs)
end
fun backspace
( app: app_type
, searchString
, tempSearchList
, searchScrollColumn
, searchCursorIdx
, caseSensitive
) =
if searchCursorIdx = 0 then
app
else
let
val searchString =
if searchCursorIdx = String.size searchString then
String.substring (searchString, 0, String.size searchString - 1)
else
let
val sub1 = Substring.extract
(searchString, 0, SOME (searchCursorIdx - 1))
val sub2 = Substring.extract (searchString, searchCursorIdx, SOME
(String.size searchString - searchCursorIdx))
in
Substring.concat [sub1, sub2]
end
val searchCursorIdx = searchCursorIdx - 1
val {cursorIdx, buffer, ...} = app
val buffer = LineGap.goToIdx (cursorIdx - 1111, buffer)
val (buffer, tempSearchList) =
buildTempSearchList (searchString, buffer, cursorIdx, caseSensitive)
in
NormalSearchFinish.onSearchChanged
( app
, searchString
, tempSearchList
, searchCursorIdx
, searchScrollColumn
, caseSensitive
, buffer
)
end
fun moveLeft
( app
, searchString
, tempSearchList
, searchCursorIdx
, searchScrollColumn
, caseSensitive
) =
if searchCursorIdx = 0 then
app
else
let
val searchCursorIdx = Int.max (0, searchCursorIdx - 1)
in
NormalSearchFinish.onSearchChanged
( app
, searchString
, tempSearchList
, searchCursorIdx
, searchScrollColumn
, caseSensitive
, #buffer app
)
end
fun moveRight
( app
, searchString
, tempSearchList
, searchCursorIdx
, searchScrollColumn
, caseSensitive
) =
if searchCursorIdx = String.size searchString then
app
else
let
val searchCursorIdx =
Int.min (searchCursorIdx + 1, String.size searchString)
in
NormalSearchFinish.onSearchChanged
( app
, searchString
, tempSearchList
, searchCursorIdx
, searchScrollColumn
, caseSensitive
, #buffer app
)
end
fun update
( app
, { searchString
, tempSearchList
, searchCursorIdx
, searchScrollColumn
, caseSensitive
}
, msg
, time
) =
case msg of
CHAR_EVENT chr =>
addChr
( app
, searchString
, searchCursorIdx
, searchScrollColumn
, caseSensitive
, chr
)
| KEY_BACKSPACE =>
backspace
( app
, searchString
, tempSearchList
, searchScrollColumn
, searchCursorIdx
, caseSensitive
)
| KEY_ESC => exitToNormalMode app
| KEY_ENTER => saveSearch (app, searchString, caseSensitive, time)
| ARROW_LEFT =>
moveLeft
( app
, searchString
, tempSearchList
, searchCursorIdx
, searchScrollColumn
, caseSensitive
)
| ARROW_RIGHT =>
moveRight
( app
, searchString
, tempSearchList
, searchCursorIdx
, searchScrollColumn
, caseSensitive
)
| RESIZE_EVENT (width, height) =>
NormalSearchFinish.resize
( app
, width
, height
, searchString
, searchCursorIdx
, tempSearchList
, searchScrollColumn
, caseSensitive
)
(* In Vim's search mode, the up and down arrows can be used
* to scroll through the search history.
* I don't find this feature too useful as it is often easier to type
* the whole search string again, so I'm leaving it unimplemented
* until/unless I find that I wish this functionality was there
* while using the program. *)
| ARROW_UP => app
| ARROW_DOWN => app
end

View File

@@ -0,0 +1,14 @@
structure NormalYankDelete =
MakeNormalDelete
(struct
open DrawMsg
open MailboxType
fun initMsgs (low, length, buffer) =
let
val str = LineGap.substring (low, length + 1, buffer)
val msg = YANK str
in
[DRAW msg]
end
end)

View File

@@ -0,0 +1,534 @@
structure NormalYank =
struct
open AppType
open DrawMsg
open MailboxType
fun finish (app, buffer, yankedString) =
let
val msgs = [DRAW (YANK yankedString)]
val mode = NORMAL_MODE ""
in
NormalModeWith.modeAndBuffer (app, buffer, mode, msgs)
end
fun yankLeft (app: app_type, count) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val min = Cursor.vi0 (buffer, cursorIdx)
val low = Cursor.viH (buffer, cursorIdx, count)
val low = Int.max (min, low)
val length = cursorIdx - low
val str = LineGap.substring (low, length, buffer)
in
finish (app, buffer, str)
end
fun yankRight (app: app_type, count) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val endOfLineIdx = Cursor.viDlr (buffer, cursorIdx, 1) + 1
val high = Cursor.viL (buffer, cursorIdx, count)
val high = Int.min (high, endOfLineIdx)
val length = high - cursorIdx
val buffer = LineGap.goToIdx (high, buffer)
val str = LineGap.substring (cursorIdx, length, buffer)
in
finish (app, buffer, str)
end
fun yankLineUp (app: app_type, count) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val cursorLineNumber =
if Cursor.isNextChrEndOfLine (buffer, cursorIdx) then
LineGap.idxToLineNumber (cursorIdx + 1, buffer)
else
LineGap.idxToLineNumber (cursorIdx, buffer)
val newCursorLineNumber = Int.max (cursorLineNumber - count, 0)
in
if cursorLineNumber = 0 then
NormalFinish.clearMode app
else if newCursorLineNumber = 0 then
let
val endOfLine = Cursor.viDlr (buffer, cursorIdx, 1)
val buffer = LineGap.goToIdx (endOfLine, buffer)
val endOfLine =
if Cursor.isCursorAtStartOfLine (buffer, endOfLine) then
endOfLine + 1
else
endOfLine + 2
val buffer = LineGap.goToIdx (endOfLine, buffer)
val str = LineGap.substring (0, endOfLine, buffer)
in
finish (app, buffer, str)
end
else
let
val endOfLine = Cursor.viDlr (buffer, cursorIdx, 1)
val buffer = LineGap.goToIdx (endOfLine, buffer)
val endsOnNewline = Cursor.isCursorAtStartOfLine (buffer, endOfLine)
val endOfLine = if endsOnNewline then endOfLine else endOfLine + 1
val newCursorLineNumber =
if endsOnNewline andalso endOfLine = #textLength buffer - 1 then
newCursorLineNumber - 1
else
newCursorLineNumber
val buffer = LineGap.goToLine (newCursorLineNumber, buffer)
val lineIdx = LineGap.lineNumberToIdx (newCursorLineNumber, buffer)
val length = endOfLine - lineIdx
val buffer = LineGap.goToIdx (endOfLine, buffer)
val str = LineGap.substring (lineIdx + 1, length, buffer)
in
finish (app, buffer, str)
end
end
fun yankLineDown (app: app_type, count) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val startIdx = Cursor.vi0 (buffer, cursorIdx)
val buffer = LineGap.goToIdx (startIdx, buffer)
val startLine =
if Cursor.isCursorAtStartOfLine (buffer, startIdx) then
LineGap.idxToLineNumber (startIdx, buffer)
else
LineGap.idxToLineNumber (startIdx + 1, buffer)
val endLine = startLine + count + 1
val buffer = LineGap.goToLine (endLine, buffer)
val endLineIdx = LineGap.lineNumberToIdx (endLine, buffer)
val buffer = LineGap.goToIdx (endLineIdx - 1, buffer)
(* get "real" endLine by not considering newline after non-newline *)
val endLine =
if Cursor.isOnNewlineAfterChr (buffer, endLineIdx - 1) then
LineGap.idxToLineNumber (endLineIdx - 1, buffer)
else
LineGap.idxToLineNumber (endLineIdx, buffer)
in
if endLineIdx = #textLength buffer andalso endLine = startLine then
(* cursor is already on last line so don't yank *)
NormalFinish.clearMode app
else
let
val endLineIdx = endLineIdx + 1
val length = endLineIdx - startIdx
(* perform the actual yank *)
val buffer = LineGap.goToIdx (endLineIdx, buffer)
val str = LineGap.substring (startIdx, length, buffer)
in
finish (app, buffer, str)
end
end
fun yankLine (app: app_type, count) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val low = Cursor.vi0 (buffer, cursorIdx)
val buffer = LineGap.goToIdx (low, buffer)
val high = Cursor.viDlrForDelete (buffer, low, count)
val buffer = LineGap.goToIdx (high, buffer)
val length = high - low
val str = LineGap.substring (low, length, buffer)
in
finish (app, buffer, str)
end
fun yankToStartOfLine (app: app_type) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val low = Cursor.vi0 (buffer, cursorIdx)
val length = cursorIdx - low
val str = LineGap.substring (low, length, buffer)
in
finish (app, buffer, str)
end
fun yankWhenMovingBack (app: app_type, fMove, count) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val low = fMove (buffer, cursorIdx, count)
val length = cursorIdx - low
val str = LineGap.substring (low, length, buffer)
in
finish (app, buffer, str)
end
fun yankWhenMovingBackPlusOne (app: app_type, fMove, count) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val low = fMove (buffer, cursorIdx, count)
val length = (cursorIdx + 1) - low
val str = LineGap.substring (low, length, buffer)
in
finish (app, buffer, str)
end
fun yankWhenMovingForward (app: app_type, fMove, count) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val high = fMove (buffer, cursorIdx, count)
val beforeHigh = high - 1
val buffer = LineGap.goToIdx (beforeHigh, buffer)
val high =
if Cursor.isOnNewlineAfterChr (buffer, beforeHigh) then beforeHigh
else high
val length = high - cursorIdx
val str = LineGap.substring (cursorIdx, length, buffer)
in
finish (app, buffer, str)
end
fun yankToFirstNonSpaceChr (app: app_type) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val otherIdx = Cursor.vi0 (buffer, cursorIdx)
val buffer = LineGap.goToIdx (otherIdx, buffer)
val otherIdx = Cursor.firstNonSpaceChr (buffer, otherIdx)
in
if cursorIdx > otherIdx then
(* yanking backwards from cursorIdx *)
let
val length = cursorIdx - otherIdx + 1
val buffer = LineGap.goToIdx (otherIdx, buffer)
val str = LineGap.substring (otherIdx, length, buffer)
in
finish (app, buffer, str)
end
else if cursorIdx < otherIdx then
(* yanking forward from cursorIdx *)
let
val length = otherIdx - cursorIdx
val str = LineGap.substring (cursorIdx, length, buffer)
in
finish (app, buffer, str)
end
else
NormalFinish.clearMode app
end
fun yankToEndOfText (app: app_type) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToEnd buffer
val {rightStrings, idx, ...} = buffer
val finishIdx = Int.max (0, idx - 1)
val length = finishIdx - cursorIdx
val str = LineGap.substring (cursorIdx, length, buffer)
in
finish (app, buffer, str)
end
fun yankToMatchingPair (app: app_type) =
let
val {buffer, cursorIdx, ...} = app
val otherIdx = Cursor.matchPair (buffer, cursorIdx)
in
if cursorIdx = otherIdx then
NormalFinish.clearMode app
else
let
val low = Int.min (cursorIdx, otherIdx)
val high = Int.max (cursorIdx, otherIdx)
val length = high - low + 1
val buffer = LineGap.goToIdx (high, buffer)
val str = LineGap.substring (low, length, buffer)
in
finish (app, buffer, str)
end
end
fun yankToNextMatch (app: app_type, count) =
let
val {cursorIdx, searchList, buffer, ...} = app
val high = PersistentVector.nextMatch (cursorIdx, searchList, count)
in
if high = ~1 orelse high <= cursorIdx then
NormalFinish.clearMode app
else
let
val length = high - cursorIdx
val buffer = LineGap.goToIdx (high, buffer)
val str = LineGap.substring (cursorIdx, length, buffer)
in
finish (app, buffer, str)
end
end
fun yankToPrevMatch (app: app_type, count) =
let
val {cursorIdx, searchList, buffer, ...} = app
val low = PersistentVector.prevMatch (cursorIdx, searchList, count)
in
if low = ~1 orelse low >= cursorIdx then
NormalFinish.clearMode app
else
let
val length = cursorIdx - low
val str = LineGap.substring (low, length, buffer)
in
finish (app, buffer, str)
end
end
fun helpYankToChr
(app: app_type, buffer, cursorIdx, otherIdx, count, fMove, fInc, chr) =
if count = 0 then
let
val low = Int.min (cursorIdx, otherIdx)
val high = Int.max (cursorIdx, otherIdx)
val length = high - low
val buffer = LineGap.goToIdx (high, buffer)
val str = LineGap.substring (low, length, buffer)
in
finish (app, buffer, str)
end
else
let
val buffer = LineGap.goToIdx (otherIdx, buffer)
val newOtherIdx = fMove (buffer, otherIdx, chr)
val newCount = if newOtherIdx = otherIdx then 0 else count - 1
val newOtherIdx = fInc (newOtherIdx, 1)
in
helpYankToChr
(app, buffer, cursorIdx, newOtherIdx, newCount, fMove, fInc, chr)
end
fun yankToNextChr (app: app_type, count, chr) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val newCursorIdx =
Cursor.toNextChr (buffer, cursorIdx, {findChr = chr, count = count})
in
if newCursorIdx = ~1 then
NormalFinish.clearMode app
else
let
val length = newCursorIdx - cursorIdx + 1
val buffer = LineGap.goToIdx (newCursorIdx, buffer)
val str = LineGap.substring (cursorIdx, length, buffer)
in
finish (app, buffer, str)
end
end
fun yankTillNextChr (app: app_type, count, chr) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val newCursorIdx =
Cursor.toNextChr (buffer, cursorIdx, {findChr = chr, count = count})
in
if newCursorIdx = ~1 then
NormalFinish.clearMode app
else
let
val length = newCursorIdx - cursorIdx
val buffer = LineGap.goToIdx (newCursorIdx, buffer)
val str = LineGap.substring (cursorIdx, length, buffer)
in
finish (app, buffer, str)
end
end
fun yankToPrevChr (app: app_type, count, chr) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val newCursorIdx =
Cursor.toPrevChr (buffer, cursorIdx, {findChr = chr, count = count})
in
if newCursorIdx = ~1 then
NormalFinish.clearMode app
else
let
val length = cursorIdx - newCursorIdx
val str = LineGap.substring (newCursorIdx, length, buffer)
in
finish (app, buffer, str)
end
end
fun yankTillPrevChr (app: app_type, count, chr) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val newCursorIdx =
Cursor.toPrevChr (buffer, cursorIdx, {findChr = chr, count = count})
in
if newCursorIdx = ~1 then
NormalFinish.clearMode app
else
let
val newCursorIdx = newCursorIdx + 1
val length = cursorIdx - newCursorIdx
val str = LineGap.substring (newCursorIdx, length, buffer)
in
finish (app, buffer, str)
end
end
fun yankToStart (app: app_type) =
let
val {cursorIdx, buffer, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val high = Cursor.viDlrForDelete (buffer, cursorIdx, 1)
val buffer = LineGap.goToIdx (high, buffer)
val str = LineGap.substring (0, high, buffer)
in
finish (app, buffer, str)
end
fun yankInsideWord (app: app_type) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val low = Cursor.prevWordStrict (buffer, cursorIdx, 1)
val high = Cursor.endOfWordStrict (buffer, cursorIdx, 1)
val high = high + 1
val buffer = LineGap.goToIdx (high, buffer)
val length = high - low
val str = LineGap.substring (low, length, buffer)
in
if str = "\n" then NormalFinish.clearMode app
else finish (app, buffer, str)
end
fun yankInsideWORD (app: app_type) =
let
val {buffer, cursorIdx, ...} = app
val buffer = LineGap.goToIdx (cursorIdx, buffer)
val low = Cursor.prevWORDStrict (buffer, cursorIdx, 1)
val high = Cursor.endOfWORDStrict (buffer, cursorIdx, 1)
val high = high + 1
val buffer = LineGap.goToIdx (high, buffer)
val length = high - low
val str = LineGap.substring (low, length, buffer)
in
if str = "\n" then NormalFinish.clearMode app
else finish (app, buffer, str)
end
fun finishAfterYankInside (app: app_type, low, high, buffer) =
let
val length = high - low
val str = LineGap.substring (low, length, buffer)
in
finish (app, buffer, str)
end
fun yankInsideChrOpen (app: app_type, chr) =
let
val {cursorIdx, buffer, ...} = app
val start = cursorIdx + 1
val buffer = LineGap.goToIdx (start, buffer)
val low = Cursor.toPrevChr (buffer, start, {findChr = chr, count = 1})
val buffer = LineGap.goToIdx (low, buffer)
val high = Cursor.matchPair (buffer, low)
val buffer = LineGap.goToIdx (high, buffer)
val low = low + 1
in
if low = high then NormalFinish.clearMode app
else finishAfterYankInside (app, low, high, buffer)
end
fun yankInsideChrClose (app: app_type, chr) =
let
val {cursorIdx, buffer, ...} = app
val start = Int.max (cursorIdx - 1, 0)
val buffer = LineGap.goToIdx (start, buffer)
val high = Cursor.toNextChr (buffer, start, {findChr = chr, count = 1})
val buffer = LineGap.goToIdx (high, buffer)
val low = Cursor.matchPair (buffer, high) + 1
in
if low = high then NormalFinish.clearMode app
else finishAfterYankInside (app, low, high, buffer)
end
fun yankAroundChrOpen (app: app_type, chr) =
let
val {cursorIdx, buffer, ...} = app
val start = cursorIdx + 1
val buffer = LineGap.goToIdx (start, buffer)
val low = Cursor.toPrevChr (buffer, start, {findChr = chr, count = 1})
val buffer = LineGap.goToIdx (low, buffer)
val high = Cursor.matchPair (buffer, low) + 1
val buffer = LineGap.goToIdx (high, buffer)
val low = low
in
if low = high then NormalFinish.clearMode app
else finishAfterYankInside (app, low, high, buffer)
end
fun yankAroundChrClose (app: app_type, chr) =
let
val {cursorIdx, buffer, ...} = app
val start = Int.max (cursorIdx - 1, 0)
val buffer = LineGap.goToIdx (start, buffer)
val high = Cursor.toNextChr (buffer, start, {findChr = chr, count = 1})
val buffer = LineGap.goToIdx (high, buffer)
val low = Cursor.matchPair (buffer, high)
val high = high + 1
in
if low = high then NormalFinish.clearMode app
else finishAfterYankInside (app, low, high, buffer)
end
end

View File

@@ -0,0 +1,937 @@
structure PersistentVector =
struct
(* Clojure-style persistent vector, for building search list.
* There is an "int table" too, which stores the last index
* at the node with the same index.
* We can use the size table for binary search.
* *)
datatype t =
BRANCH of t vector * int vector
| LEAF of {start: int, finish: int} vector * int vector
val maxSize = 32
val halfSize = 16
fun isEmpty t =
case t of
LEAF (_, sizes) => Vector.length sizes = 0
| BRANCH (_, sizes) => Vector.length sizes = 0
val empty = LEAF (#[], #[])
datatype append_result = APPEND of t | UPDATE of t
fun isInRange (checkIdx, t) =
case t of
BRANCH (nodes, sizes) =>
let
val searchIdx = BinSearch.equalOrMore (checkIdx, sizes)
in
if searchIdx = ~1 then
false
else if searchIdx = 0 then
isInRange (checkIdx, Vector.sub (nodes, searchIdx))
else
let
val nextCheckIdx = checkIdx - Vector.sub (sizes, searchIdx - 1)
in
isInRange (nextCheckIdx, Vector.sub (nodes, searchIdx))
end
end
| LEAF (values, sizes) =>
let
val searchIdx = BinSearch.equalOrMore (checkIdx, sizes)
in
if searchIdx = ~1 then
false
else
let
val {start, finish} = Vector.sub (values, searchIdx)
in
checkIdx >= start andalso checkIdx <= finish
end
end
fun getFinishIdx t =
case t of
BRANCH (_, sizes) => Vector.sub (sizes, Vector.length sizes - 1)
| LEAF (_, sizes) => Vector.sub (sizes, Vector.length sizes - 1)
fun getStartIdx t =
case t of
BRANCH (nodes, _) => getStartIdx (Vector.sub (nodes, 0))
| LEAF (items, _) =>
if Vector.length items = 0 then
0
else
#start (Vector.sub (items, 0))
fun helpAppend (start, finish, tree) =
case tree of
BRANCH (nodes, sizes) =>
let
val lastNode = Vector.sub (nodes, Vector.length nodes - 1)
val prevSize =
if Vector.length sizes > 1 then
Vector.sub (sizes, Vector.length sizes - 2)
else
0
in
case helpAppend (start - prevSize, finish - prevSize, lastNode) of
UPDATE newLast =>
let
val lastPos = Vector.length nodes - 1
val newNode = Vector.update (nodes, lastPos, newLast)
val newSizes = Vector.update (sizes, lastPos, finish)
val newNode = BRANCH (newNode, newSizes)
in
UPDATE newNode
end
| APPEND newVec =>
if Vector.length nodes = maxSize then
let
(* adjust "finish" so that it does not consider
* offset for "lower" vector *)
val finish = finish - Vector.sub (sizes, Vector.length sizes - 1)
val newNode = BRANCH (#[newVec], #[finish])
in
APPEND newNode
end
else
let
val newNodes = Vector.concat [nodes, #[newVec]]
val newSizes = Vector.concat [sizes, #[finish]]
val newNodes = BRANCH (newNodes, newSizes)
in
UPDATE newNodes
end
end
| LEAF (values, sizes) =>
if Vector.length values + 1 > maxSize then
(* when we split a leaf into two vectors,
* we want to adjust the start and finish parameters
* so that they don't contain the offset relevant to the
* "lower" vector, which was split from *)
let
val prevFinish = Vector.sub (sizes, Vector.length sizes - 1)
val start = start - prevFinish
val finish = finish - prevFinish
val newNode = LEAF (#[{start = start, finish = finish}], #[finish])
in
APPEND newNode
end
else
let
val newNode = Vector.concat
[values, #[{start = start, finish = finish}]]
val newSizes = Vector.concat [sizes, #[finish]]
val newNode = LEAF (newNode, newSizes)
in
UPDATE newNode
end
fun append (start, finish, tree) =
case helpAppend (start, finish, tree) of
UPDATE t => t
| APPEND newNode =>
let
val maxSize = getFinishIdx tree
in
BRANCH (#[tree, newNode], #[maxSize, finish])
end
fun getStart tree =
case tree of
LEAF (values, _) => Vector.sub (values, 0)
| BRANCH (nodes, _) => getStart (Vector.sub (nodes, 0))
fun helpNextMatch (cursorIdx, tree, absOffset) =
case tree of
LEAF (values, sizes) =>
let
val idx = BinSearch.equalOrMore (cursorIdx, sizes)
in
if idx = ~1 then {start = ~1, finish = ~1}
else
let
val {start, finish} = Vector.sub (values, idx)
in
{start = start + absOffset, finish = finish + absOffset}
end
end
| BRANCH (nodes, sizes) =>
let
val idx = BinSearch.equalOrMore (cursorIdx, sizes)
in
if idx = ~1 then
{start = ~1, finish = ~1}
else if idx = 0 then
helpNextMatch (cursorIdx, Vector.sub (nodes, idx), absOffset)
else
let
val prevSize = Vector.sub (sizes, idx - 1)
val cursorIdx = cursorIdx - prevSize
val absOffset = absOffset + prevSize
in
helpNextMatch (cursorIdx, Vector.sub (nodes, idx), absOffset)
end
end
fun loopNextMatch (prevStart, prevFinish, tree, count) =
if count = 0 then
prevStart
else
let
val {start, finish} = helpNextMatch (prevFinish + 1, tree, 0)
in
if start = ~1 then
let val {start, finish} = getStart tree
in loopNextMatch (start, finish, tree, count - 1)
end
else
loopNextMatch (start, finish, tree, count - 1)
end
fun nextMatch (cursorIdx, tree, count) =
if isEmpty tree then
~1
else
let
val {start, finish} = helpNextMatch (cursorIdx, tree, 0)
in
if start = ~1 then
let val {start, finish} = getStart tree
in loopNextMatch (start, finish, tree, count - 1)
end
else if cursorIdx >= start andalso cursorIdx <= finish then
loopNextMatch (start, finish, tree, count)
else
loopNextMatch (start, finish, tree, count - 1)
end
fun getLast (tree, absOffset) =
case tree of
LEAF (values, _) =>
let
val {start, finish} = Vector.sub (values, Vector.length values - 1)
in
{start = start + absOffset, finish = finish + absOffset}
end
| BRANCH (nodes, sizes) =>
let
val prevSize =
if Vector.length sizes - 2 >= 0 then
Vector.sub (sizes, Vector.length sizes - 2)
else
0
val absOffset = absOffset + prevSize
in
getLast (Vector.sub (nodes, Vector.length nodes - 1), absOffset)
end
(* slightly tricky.
* The `sizes` vector contains the last/finish position of the item
* at the corresponding index in the `nodes` or `values` vector
* However, what we when searching for the previous match
* is different: we want the node that has a start prior
* to the cursorIdx.
* This information cannot be retrieved with 100% accuracy
* using the `sizes` vector.
* To get what we want, we recurse downwards using the `sizes` vector.
* If we found the node we want, we return it.
* Otherwise, we return a state meaning "no node at this position"
* and we use the call stack to descend down the node at the previous index.
* There might not be a previous index because the current index is 0.
* In this case, either the call stack will handle it,
* or the caller to `helpPrevMatch` will. *)
fun helpPrevMatch (cursorIdx, tree, absOffset) =
case tree of
LEAF (values, sizes) =>
let
val idx = BinSearch.equalOrMore (cursorIdx, sizes)
in
if idx < 0 then
{start = ~1, finish = ~1}
else if idx = 0 then
let
val {start, finish} = Vector.sub (values, 0)
in
if start < cursorIdx then
{start = start + absOffset, finish = finish + absOffset}
else
{start = ~1, finish = ~1}
end
else
let
val {start, finish} = Vector.sub (values, idx)
in
if cursorIdx > start then
{start = start + absOffset, finish = finish + absOffset}
else
let
val {start, finish} = Vector.sub (values, idx - 1)
in
{start = start + absOffset, finish = finish + absOffset}
end
end
end
| BRANCH (nodes, sizes) =>
let
val idx = BinSearch.equalOrMore (cursorIdx, sizes)
in
if idx < 0 then
{start = ~1, finish = ~1}
else if idx = 0 then
helpPrevMatch (cursorIdx, Vector.sub (nodes, idx), absOffset)
else
let
val prevSize = Vector.sub (sizes, idx - 1)
val node = Vector.sub (nodes, idx)
val result =
helpPrevMatch (cursorIdx - prevSize, node, absOffset + prevSize)
in
if #start result = ~1 then
let
val prevSize =
if idx - 2 >= 0 then
Vector.sub (sizes, idx - 2)
else
0
val absOffset = absOffset + prevSize
in
getLast (Vector.sub (nodes, idx - 1), absOffset)
end
else result
end
end
fun loopPrevMatch (prevStart, prevFinish, tree, count) =
if count = 0 then
prevStart
else
let
val {start, finish} = helpPrevMatch (prevFinish - 1, tree, 0)
in
if start = ~1 then
let val {start, finish} = getLast (tree, 0)
in loopPrevMatch (start, finish, tree, count - 1)
end
else
loopPrevMatch (start, finish, tree, count - 1)
end
fun prevMatch (cursorIdx, tree, count) =
if isEmpty tree then
~1
else
let
val {start, finish} = helpPrevMatch (cursorIdx, tree, 0)
in
if start = ~1 then
let val {start, finish} = getLast (tree, 0)
in loopPrevMatch (start, finish, tree, count - 1)
end
else if cursorIdx >= start andalso cursorIdx <= finish then
loopPrevMatch (start, finish, tree, count)
else
loopPrevMatch (start, finish, tree, count - 1)
end
fun splitLeft (splitIdx, tree) =
case tree of
LEAF (items, sizes) =>
if Vector.length items = 0 then
(* if tree is empty, then just return tree *)
tree
else
let
val {start, ...} = Vector.sub (items, 0)
in
(* if all items are after splitIdx,
* then we want to return an empty tree,
* splitting everything *)
if splitIdx < start then
empty
else if splitIdx > Vector.sub (sizes, Vector.length sizes - 1) then
(* if all items are before splitIdx,
* then we want to return the same tree,
* splitting nothing *)
tree
else
(* we want to split from somewhere in middle, keeping left *)
let
val idx = BinSearch.equalOrMore (splitIdx, sizes)
val idx = SOME idx
val items = VectorSlice.slice (items, 0, idx)
val items = VectorSlice.vector items
val sizes = VectorSlice.slice (sizes, 0, idx)
val sizes = VectorSlice.vector sizes
in
LEAF (items, sizes)
end
end
| BRANCH (nodes, sizes) =>
if Vector.length nodes = 0 then
tree
else
if splitIdx < Vector.sub (sizes, 0) then
(* we want to split first node from rest *)
splitLeft (splitIdx, Vector.sub (nodes, 0))
else if splitIdx > Vector.sub (sizes, Vector.length sizes - 1) then
(* split point is after this subtree,
* so return this subtree unchanged *)
tree
else
(* we want to split from somewhere in middle *)
let
val idx = BinSearch.equalOrMore (splitIdx, sizes)
val prevSize =
if idx = 0 then
0
else
Vector.sub (sizes, idx - 1)
val child =
splitLeft (splitIdx - prevSize, Vector.sub (nodes, idx))
val sizes = VectorSlice.slice (sizes, 0, SOME idx)
val nodes = VectorSlice.slice (nodes, 0, SOME idx)
in
if isEmpty child then
let
val sizes = VectorSlice.vector sizes
val nodes = VectorSlice.vector nodes
in
BRANCH (nodes, sizes)
end
else
let
val childSize = VectorSlice.full #[getFinishIdx child + prevSize]
val sizes =VectorSlice.concat [sizes, childSize]
val childNode = VectorSlice.full #[child]
val nodes = VectorSlice.concat [nodes, childNode]
in
BRANCH (nodes, sizes)
end
end
(* When we split in this function,
* we always want to update the sizes vector
* so that the relative rope-like metadata is valid *)
fun splitRight (splitIdx, tree) =
case tree of
BRANCH (nodes, sizes) =>
if splitIdx > Vector.sub (sizes, Vector.length sizes - 1) then
(* splitIdx is greater than largest element,
* so we want to remove everything;
* or, in other words, we want to return an empty vec *)
empty
else
let
val idx = BinSearch.equalOrMore (splitIdx, sizes)
val prevSize =
if idx = 0 then
0
else
Vector.sub (sizes, idx - 1)
val oldChildSize = Vector.sub (sizes, idx)
val child = splitRight (splitIdx - prevSize, Vector.sub (nodes, idx))
val len = Vector.length nodes - (idx + 1)
val sizesSlice = VectorSlice.slice (sizes, idx + 1, SOME len)
val nodesSlice = VectorSlice.slice (nodes, idx + 1, SOME len)
in
if isEmpty child then
if VectorSlice.length sizesSlice = 0 then
(* if we descended down last node and last node became empty,
* then return empty vector *)
empty
else
let
val nodes = VectorSlice.vector nodesSlice
val sizes = VectorSlice.map (fn el => el - oldChildSize) sizesSlice
in
BRANCH (nodes, sizes)
end
else
let
val newChildSize = getFinishIdx child
val sizes = Vector.tabulate (VectorSlice.length sizesSlice + 1,
fn i =>
if i = 0 then
newChildSize
else
let
val el = VectorSlice.sub (sizesSlice, i - 1)
in
el - oldChildSize + newChildSize
end
)
val child = VectorSlice.full #[child]
val nodes = VectorSlice.concat [child, nodesSlice]
in
BRANCH (nodes, sizes)
end
end
| LEAF (items, sizes) =>
if Vector.length items = 0 then
tree
else
if splitIdx > Vector.sub (sizes, Vector.length sizes - 1) then
empty
else if splitIdx < #start (Vector.sub (items, 0)) then
tree
else
let
val idx = BinSearch.equalOrMore (splitIdx, sizes)
val {start, finish} = Vector.sub (items, idx)
val idx =
if splitIdx >= start then
idx + 1
else
idx
in
if idx >= Vector.length items then
empty
else
let
val prevSize =
if idx > 0 then
Vector.sub (sizes, idx - 1)
else
0
val len = Vector.length items - idx
val itemsSlice = VectorSlice.slice (items, idx, SOME len)
val items = VectorSlice.map
(fn {start, finish} =>
{start = start - prevSize, finish = finish - prevSize}
)
itemsSlice
val sizes = Vector.map #finish items
in
LEAF (items, sizes)
end
end
fun decrementBy (decBy, tree) =
case tree of
BRANCH (nodes, sizes) =>
let
val child = decrementBy (decBy, Vector.sub (nodes, 0))
val nodes = Vector.update (nodes, 0, child)
val sizes = Vector.map (fn sz => sz - decBy) sizes
in
BRANCH (nodes, sizes)
end
| LEAF (items, sizes) =>
let
val items = Vector.map
(fn {start, finish} =>
{start = start - decBy, finish = finish - decBy}
) items
val sizes = Vector.map #finish items
in
LEAF (items, sizes)
end
fun incrementBy (incBy, tree) =
case tree of
BRANCH (nodes, sizes) =>
let
val child = incrementBy (incBy, Vector.sub (nodes, 0))
val nodes = Vector.update (nodes, 0, child)
val sizes = Vector.map (fn sz => sz + incBy) sizes
in
BRANCH (nodes, sizes)
end
| LEAF (items, sizes) =>
let
val items = Vector.map
(fn {start, finish} =>
{start = start + incBy, finish = finish + incBy}
) items
val sizes = Vector.map #finish items
in
LEAF (items, sizes)
end
fun countDepthLoop (counter, tree) =
case tree of
BRANCH (nodes, _) => countDepthLoop (counter + 1, Vector.sub (nodes, 0))
| LEAF (_, _) => counter + 1
fun countDepth tree = countDepthLoop (0, tree)
datatype merge_same_depth_result =
MERGE_SAME_DEPTH_UPDATE of t
| MERGE_SAME_DEPTH_FULL
fun mergeSameDepth (left, right) =
case (left, right) of
(LEAF (leftItems, leftSizes), LEAF (rightItems, rightSizes)) =>
if Vector.length leftItems + Vector.length rightItems <= maxSize then
let
val offset = Vector.sub (leftSizes, Vector.length leftSizes - 1)
val newVecLen = Vector.length leftItems + Vector.length rightItems
val items = Vector.tabulate (newVecLen,
fn i =>
if i < Vector.length leftItems then
Vector.sub (leftItems, i)
else
let
val {start, finish} =
Vector.sub (rightItems, i - Vector.length leftItems)
in
{start = start + offset, finish = finish + offset}
end
)
val sizes = Vector.map #finish items
in
MERGE_SAME_DEPTH_UPDATE (LEAF (items, sizes))
end
else
MERGE_SAME_DEPTH_FULL
| (BRANCH (leftNodes, leftSizes), BRANCH (rightNodes, rightSizes)) =>
if Vector.length leftNodes + Vector.length rightNodes <= maxSize then
let
val offset = Vector.sub (leftSizes, Vector.length leftSizes - 1)
val nodes = Vector.concat [leftNodes, rightNodes]
val sizes = Vector.tabulate (Vector.length nodes,
fn i =>
if i < Vector.length leftSizes then
Vector.sub (leftSizes, i)
else
Vector.sub (rightSizes, i - Vector.length leftSizes) + offset
)
in
MERGE_SAME_DEPTH_UPDATE (BRANCH (nodes, sizes))
end
else
MERGE_SAME_DEPTH_FULL
| _ =>
raise Fail "PersistentVector.mergeSameDepth: \
\left and right should both be BRANCH or both be LEAF \
\but one is BRANCH and one is LEAF"
datatype merge_diff_depth_result =
MERGE_DIFF_DEPTH_UPDATE of t
| MERGE_DIFF_DEPTH_FULL
fun mergeWhenRightDepthIsGreater (left, right, targetDepth, curDepth) =
if curDepth = targetDepth then
case mergeSameDepth (left, right) of
MERGE_SAME_DEPTH_UPDATE tree => MERGE_DIFF_DEPTH_UPDATE tree
| MERGE_SAME_DEPTH_FULL => MERGE_DIFF_DEPTH_FULL
else
case right of
BRANCH (nodes, sizes) =>
(case mergeWhenRightDepthIsGreater
(left, Vector.sub (nodes, 0), targetDepth, curDepth + 1) of
MERGE_DIFF_DEPTH_UPDATE child =>
let
val oldChildSize = Vector.sub (sizes, 0)
val newChildSize = getFinishIdx child
val difference = newChildSize - oldChildSize
val nodes = Vector.update (nodes, 0, child)
val sizes = Vector.map (fn el => el + difference) sizes
in
MERGE_DIFF_DEPTH_UPDATE (BRANCH (nodes, sizes))
end
| MERGE_DIFF_DEPTH_FULL =>
let
val leftSize = getFinishIdx left
val sizes = Vector.tabulate (Vector.length nodes + 1,
fn i =>
if i = 0 then
leftSize
else
Vector.sub (sizes, i - 1) + leftSize
)
val nodes = Vector.concat [#[left], nodes]
in
MERGE_DIFF_DEPTH_UPDATE (BRANCH (nodes, sizes))
end)
| LEAF _ =>
raise Fail "PersistentVector.mergeWhenRightDepthIsGreater: \
\reached LEAF before (curDepth = targetDepth)"
fun mergeWhenLeftDepthIsGreater (left, right, targetDepth, curDepth) =
if targetDepth = curDepth then
case mergeSameDepth (left, right) of
MERGE_SAME_DEPTH_UPDATE tree => MERGE_DIFF_DEPTH_UPDATE tree
| MERGE_SAME_DEPTH_FULL => MERGE_DIFF_DEPTH_FULL
else
case left of
BRANCH (nodes, sizes) =>
(case
mergeWhenLeftDepthIsGreater (
Vector.sub (nodes, Vector.length nodes - 1),
right,
targetDepth,
curDepth + 1) of
MERGE_DIFF_DEPTH_UPDATE child =>
let
val lastIdx = Vector.length sizes - 1
val oldChildSize = Vector.sub (sizes, lastIdx)
val newChildSize = getFinishIdx child
val difference = newChildSize - oldChildSize
val nodes = Vector.update (nodes, lastIdx, child)
val sizes = Vector.map (fn el => el + difference) sizes
in
MERGE_DIFF_DEPTH_UPDATE (BRANCH (nodes, sizes))
end
| MERGE_DIFF_DEPTH_FULL =>
let
val maxLeftSize = Vector.sub (sizes, Vector.length sizes - 1)
val rightSize = getFinishIdx right + maxLeftSize
val sizes = Vector.concat [sizes, #[rightSize]]
val nodes = Vector.concat [nodes, #[right]]
in
MERGE_DIFF_DEPTH_UPDATE (BRANCH (nodes, sizes))
end)
| LEAF _ =>
raise Fail "PersistentVector.mergeWhenLeftDepthIsGreater: \
\reached LEAF before (curDepth = targetDepth)"
fun merge (left, right) =
let
val leftDepth = countDepth left
val rightDepth = countDepth right
in
if leftDepth = rightDepth then
case mergeSameDepth (left, right) of
MERGE_SAME_DEPTH_UPDATE t => t
| MERGE_SAME_DEPTH_FULL =>
let
val leftSize = getFinishIdx left
val sizes = #[leftSize, getFinishIdx right + leftSize]
val nodes = #[left, right]
in
BRANCH (nodes, sizes)
end
else if leftDepth < rightDepth then
let
val targetDepth = rightDepth - leftDepth
in
case mergeWhenRightDepthIsGreater
(left, right, targetDepth, 0) of
MERGE_DIFF_DEPTH_UPDATE t => t
| MERGE_DIFF_DEPTH_FULL => empty
end
else
let
val targetDepth = leftDepth - rightDepth
in
case mergeWhenLeftDepthIsGreater
(left, right, targetDepth, 0) of
MERGE_DIFF_DEPTH_UPDATE t => t
| MERGE_DIFF_DEPTH_FULL => empty
end
end
fun delete (start, length, tree) =
if isEmpty tree then
empty
else
let
val finish = start + length
val matchAfterFinish = nextMatch (finish, tree, 1)
val matchAfterFinish =
if matchAfterFinish < finish then
~1
else
matchAfterFinish
in
let
val left = splitLeft (start, tree)
val right = splitRight (finish, tree)
in
if isEmpty left andalso isEmpty right then
empty
else if isEmpty left then
(* just decrement right and return it *)
let
val rightStart = getStartIdx right
val shouldBeStartIdx = matchAfterFinish - length
val difference = rightStart - shouldBeStartIdx
in
if difference = 0 then
right
else
decrementBy (difference, right)
end
else if isEmpty right then
(* return left half without doing anything *)
left
else
(* decrement right, and then merge both together *)
let
val leftSize = getFinishIdx left
val rightStartRelative = getStartIdx right
val rightStartAbsolute = leftSize + rightStartRelative
val shouldBeStartIdx = matchAfterFinish - length
val difference = rightStartAbsolute - shouldBeStartIdx
in
if difference = 0 then
merge (left, right)
else
let
val right = decrementBy (difference, right)
in
merge (left, right)
end
end
end
end
(* Usually, when inserting, we want the absolute metadata
* to be adjusted appropriately.
* An insertion should cause the absolute metadata to increment.
* However, we sometimes want to insert a match without adjusting
* the absolute metadata in this way.
* We want to do this when deleting some part of the buffer
* would cause a new match to be found, for example. *)
fun insertMatchKeepingAbsoluteInddices (start, finish, tree) =
let
val matchAfterFinish = nextMatch (finish, tree, 1)
in
if matchAfterFinish <= finish then
(* no match after the 'finish', so we can just append to 'tree' *)
append (start, finish, tree)
else
let
val left = splitLeft (start, tree)
val right = splitRight (finish, tree)
val left = append (start, finish, left)
val rightStartRelative = getStartIdx right
val rightStartAbsolute = rightStartRelative + finish
val difference = rightStartAbsolute - matchAfterFinish
val right = decrementBy (difference, right)
in
merge (left, right)
end
end
fun extendExistingMatch (start, newFinish, tree) =
let
val matchAfterFinish = nextMatch (newFinish, tree, 1)
val left = splitLeft (start, tree)
val left = append (start, newFinish, left)
in
if matchAfterFinish <= newFinish then
(* no match after newFinish, so we can return 'left'
* which has the newFinish appended *)
left
else
let
val right = splitRight (newFinish, tree)
val leftFinish = getFinishIdx left
val rightStartRelative = getStartIdx right
val rightStartAbsolute = rightStartRelative + leftFinish
val difference = rightStartAbsolute - matchAfterFinish
val right = decrementBy (difference, right)
in
merge (left, right)
end
end
(* functions only for testing *)
fun childrenHaveSameDepth (pos, nodes, expectedDepth) =
if pos = Vector.length nodes then
true
else
let
val node = Vector.sub (nodes, pos)
in
if allLeavesAtSameDepth node then
let
val nodeDepth = countDepth node
in
if nodeDepth = expectedDepth then
childrenHaveSameDepth (pos + 1, nodes, expectedDepth)
else
false
end
else
false
end
and allLeavesAtSameDepth tree =
case tree of
BRANCH (nodes, _) =>
let
val expectedDepth = countDepth (Vector.sub (nodes, 0))
in
childrenHaveSameDepth (0, nodes, expectedDepth)
end
| LEAF _ => true
fun fromListLoop (lst, acc) =
case lst of
{start, finish} :: tl =>
let
val acc = append (start, finish, acc)
in
fromListLoop (tl, acc)
end
| [] => acc
fun fromList coords = fromListLoop (coords, empty)
fun toListLoop (tree, acc) =
case tree of
BRANCH (nodes, _) =>
let
fun branchLoop (pos, acc) =
if pos = Vector.length nodes then
acc
else
let
val acc = toListLoop (Vector.sub (nodes, pos), acc)
in
branchLoop (pos + 1, acc)
end
in
branchLoop (0, acc)
end
| LEAF (items, _) =>
let
fun itemLoop (pos, acc, offset) =
if pos = Vector.length items then
acc
else
let
val {start, finish} = Vector.sub (items, pos)
val item = {start = start + offset, finish = finish + offset}
in
itemLoop (pos + 1, item :: acc, offset)
end
val offset =
case acc of
{finish, ...} :: _ => finish
| [] => 0
in
itemLoop (0, acc, offset)
end
fun toList tree =
let
val result = toListLoop (tree, [])
in
List.rev result
end
end

41
shf/fcore/pipe-cursor.sml Normal file
View File

@@ -0,0 +1,41 @@
structure PipeCursor =
struct
fun xToNdc (xOffset, xpos, scale, halfWidth) =
((xpos * scale + xOffset) - halfWidth) / halfWidth
fun yToNdc (yOffset, ypos, scale, halfHeight) =
~(((ypos * scale + yOffset) - halfHeight) / halfHeight)
fun lerp (xOffset: Real32.real, yOffset, z, scale, windowWidth, windowHeight, r, g, b) =
let
val halfWidth = windowWidth / 2.0
val halfHeight = windowHeight / 2.0
in
#[
xToNdc (xOffset, 0.000000000000000, scale, halfWidth),
yToNdc (yOffset, 13.000000000000000, scale, halfHeight),
z,
r, g, b,
xToNdc (xOffset, 1.000000000000000, scale, halfWidth),
yToNdc (yOffset, 13.000000000000000, scale, halfHeight),
z,
r, g, b,
xToNdc (xOffset, 0.000000000000000, scale, halfWidth),
yToNdc (yOffset, 0.000000000000000, scale, halfHeight),
z,
r, g, b,
xToNdc (xOffset, 0.000000000000000, scale, halfWidth),
yToNdc (yOffset, 0.000000000000000, scale, halfHeight),
z,
r, g, b,
xToNdc (xOffset, 1.000000000000000, scale, halfWidth),
yToNdc (yOffset, 13.000000000000000, scale, halfHeight),
z,
r, g, b,
xToNdc (xOffset, 1.000000000000000, scale, halfWidth),
yToNdc (yOffset, 0.000000000000000, scale, halfHeight),
z,
r, g, b
]
end
end

35
shf/fcore/rect.sml Normal file
View File

@@ -0,0 +1,35 @@
structure Rect =
struct
fun xToNdc (xOffset, xpos, scale, halfWidth) =
((xpos * scale + xOffset) - halfWidth) / halfWidth
fun yToNdc (yOffset, ypos, scale, halfHeight) =
~(((ypos * scale + yOffset) - halfHeight) / halfHeight)
fun lerp (xOffset: Real32.real, yOffset, z, scale, windowWidth, windowHeight, r, g, b) =
let
val halfWidth = windowWidth / 2.0
val halfHeight = windowHeight / 2.0
in
#[
xToNdc (xOffset, 0.000000000000000, scale, halfWidth),
yToNdc (yOffset, 13.000000000000000, scale, halfHeight),
z, r, g, b,
xToNdc (xOffset, 7.000000000000000, scale, halfWidth),
yToNdc (yOffset, 13.000000000000000, scale, halfHeight),
z, r, g, b,
xToNdc (xOffset, 0.000000000000000, scale, halfWidth),
yToNdc (yOffset, 0.000000000000000, scale, halfHeight),
z, r, g, b,
xToNdc (xOffset, 0.000000000000000, scale, halfWidth),
yToNdc (yOffset, 0.000000000000000, scale, halfHeight),
z, r, g, b,
xToNdc (xOffset, 7.000000000000000, scale, halfWidth),
yToNdc (yOffset, 13.000000000000000, scale, halfHeight),
z, r, g, b,
xToNdc (xOffset, 7.000000000000000, scale, halfWidth),
yToNdc (yOffset, 0.000000000000000, scale, halfHeight),
z, r, g, b
]
end
end

View File

@@ -0,0 +1,928 @@
signature DFA_GEN_PARAMS =
sig
val endMarker: char
val charIsEqual: char * char -> bool
end
signature DFA_GEN =
sig
type dfa = int vector vector
type dfa_state = int
val fromString: string -> dfa
val nextState: dfa * dfa_state * char -> dfa_state
val isFinal: dfa * dfa_state -> bool
val isDead: dfa_state -> bool
val matchString: dfa * string -> (int * int) list
end
functor MakeDfaGen(Fn: DFA_GEN_PARAMS): DFA_GEN =
struct
datatype parse_tree =
CHAR_LITERAL of {char: char, position: int}
| WILDCARD of int
| IS_ANY_CHARACTER of {chars: char vector, position: int}
| NOT_ANY_CHARACTER of {chars: char vector, position: int}
| CONCAT of
{l: parse_tree, r: parse_tree, leftMaxState: int, rightMaxState: int}
| ALTERNATION of
{l: parse_tree, r: parse_tree, leftMaxState: int, rightMaxState: int}
| ZERO_OR_ONE of parse_tree
| ZERO_OR_MORE of parse_tree
| ONE_OR_MORE of parse_tree
| GROUP of parse_tree
fun isNullable tree =
case tree of
CHAR_LITERAL _ => false
| WILDCARD _ => false
| IS_ANY_CHARACTER _ => false
| NOT_ANY_CHARACTER _ => false
| CONCAT {l, r, ...} => isNullable l andalso isNullable r
| ALTERNATION {l, r, ...} => isNullable l orelse isNullable r
| ZERO_OR_ONE _ => true
| ZERO_OR_MORE _ => true
| ONE_OR_MORE regex => isNullable regex
| GROUP regex => isNullable regex
fun firstpos (tree, acc) =
case tree of
CHAR_LITERAL {position, ...} => position :: acc
| IS_ANY_CHARACTER {position, ...} => position :: acc
| NOT_ANY_CHARACTER {position, ...} => position :: acc
| WILDCARD i => i :: acc
| CONCAT {l, r, ...} =>
if isNullable l then
let val acc = firstpos (l, acc)
in firstpos (r, acc)
end
else
firstpos (l, acc)
| ALTERNATION {l, r, ...} =>
let val acc = firstpos (l, acc)
in firstpos (r, acc)
end
| ZERO_OR_ONE regex => firstpos (regex, acc)
| ZERO_OR_MORE regex => firstpos (regex, acc)
| ONE_OR_MORE regex => firstpos (regex, acc)
| GROUP regex => firstpos (regex, acc)
fun lastpos (tree, acc) =
case tree of
CHAR_LITERAL {position, ...} => position :: acc
| IS_ANY_CHARACTER {position, ...} => position :: acc
| NOT_ANY_CHARACTER {position, ...} => position :: acc
| WILDCARD i => i :: acc
| CONCAT {l, r, ...} =>
if isNullable r then
let val acc = lastpos (l, acc)
in lastpos (r, acc)
end
else
lastpos (r, acc)
| ALTERNATION {l, r, ...} =>
let val acc = lastpos (l, acc)
in lastpos (r, acc)
end
| ZERO_OR_ONE regex => lastpos (regex, acc)
| ZERO_OR_MORE regex => lastpos (regex, acc)
| ONE_OR_MORE regex => lastpos (regex, acc)
| GROUP regex => lastpos (regex, acc)
structure Set =
struct
datatype 'a set = BRANCH of 'a set * int * 'a * 'a set | LEAF
fun isEmpty set =
case set of
BRANCH _ => false
| LEAF => true
fun insertOrReplace (newKey, newVal, tree) =
case tree of
BRANCH (l, curKey, curVal, r) =>
if newKey > curKey then
let val r = insertOrReplace (newKey, newVal, r)
in BRANCH (l, curKey, curVal, r)
end
else if newKey < curKey then
let val l = insertOrReplace (newKey, newVal, l)
in BRANCH (l, curKey, curVal, r)
end
else
BRANCH (l, newKey, newVal, r)
| LEAF => BRANCH (LEAF, newKey, newVal, LEAF)
fun addFromList (lst, tree) =
case lst of
[] => tree
| k :: tl =>
let val tree = insertOrReplace (k, (), tree)
in addFromList (tl, tree)
end
fun getOrDefault (findKey, tree, default) =
case tree of
BRANCH (l, curKey, curVal, r) =>
if findKey > curKey then getOrDefault (findKey, r, default)
else if findKey < curKey then getOrDefault (findKey, l, default)
else curVal
| LEAF => default
fun helpToList (tree, acc) =
case tree of
BRANCH (l, curKey, curVal, r) =>
let
val acc = helpToList (r, acc)
val acc = (curKey, curVal) :: acc
in
helpToList (l, acc)
end
| LEAF => acc
fun toList tree = helpToList (tree, [])
fun helpKeysToList (tree, acc) =
case tree of
BRANCH (l, curKey, _, r) =>
let
val acc = helpKeysToList (r, acc)
val acc = curKey :: acc
in
helpKeysToList (l, acc)
end
| LEAF => acc
fun keysToList tree = helpKeysToList (tree, [])
fun helpValuesToList (tree, acc) =
case tree of
BRANCH (l, _, v, r) =>
let
val acc = helpValuesToList (r, acc)
val acc = v :: acc
in
helpValuesToList (l, acc)
end
| LEAF => acc
fun valuesToList tree = helpValuesToList (tree, [])
fun map (f, tree) =
case tree of
BRANCH (l, key, value, r) =>
let
val r = map (f, r)
val l = map (f, l)
val value = f value
in
BRANCH (l, key, value, r)
end
| LEAF => LEAF
fun foldl (f, tree, acc) =
case tree of
BRANCH (l, k, v, r) =>
let
val acc = foldl (f, l, acc)
val acc = f (v, acc)
in
foldl (f, r, acc)
end
| LEAF => acc
fun foldr (f, tree, acc) =
case tree of
BRANCH (l, k, v, r) =>
let
val acc = foldr (f, r, acc)
val acc = f (v, acc)
in
foldr (f, l, acc)
end
| LEAF => acc
end
structure ParseDfa =
struct
(* parsing through precedence climbing algorithm. *)
val postfixLevel = 1
val concatLevel = 2
val altLevel = 3
local
fun loop (pos, str, openParens, closeParens) =
if pos = String.size str then
NONE
else
case String.sub (str, pos) of
#"(" => loop (pos + 1, str, openParens + 1, closeParens)
| #")" =>
if closeParens + 1 = openParens then SOME pos
else loop (pos + 1, str, openParens, closeParens + 1)
| _ => loop (pos + 1, str, openParens, closeParens)
in
fun getRightParenIdx (pos, str) = loop (pos, str, 1, 0)
end
(* assumes previous char is a backslash *)
fun isValidEscapeSequence chr =
case chr of
(* regex metacharacters *)
#"(" => (true, chr)
| #")" => (true, chr)
| #"[" => (true, chr)
| #"]" => (true, chr)
| #"+" => (true, chr)
| #"*" => (true, chr)
| #"|" => (true, chr)
| #"?" => (true, chr)
| #"." => (true, chr)
| #"-" => (true, chr)
(* standard escape sequences *)
| #"a" => (true, #"\a")
| #"b" => (true, #"\b")
| #"t" => (true, #"\t")
| #"n" => (true, #"\n")
| #"v" => (true, #"\v")
| #"f" => (true, #"\f")
| #"r" => (true, #"\r")
| #"\\" => (true, chr)
| _ => (false, chr)
fun getCharsBetween (lowChr, highChr, acc) =
if lowChr = highChr then
highChr :: acc
else
let
val acc = lowChr :: acc
val lowChr = Char.succ lowChr
in
getCharsBetween (lowChr, highChr, acc)
end
fun getCharsInBrackets (pos, str, acc) =
if pos = String.size str then
NONE
else
case String.sub (str, pos) of
#"\\" =>
(* escape sequences *)
if pos + 1 = String.size str then
NONE
else
let
val chr = String.sub (str, pos + 1)
val (isValid, chr) = isValidEscapeSequence chr
in
if isValid then
(* Edge case:
* We have to check if there is a char range like a-z,
* and if there is,
* we have to check if the second char in the range
* is another escaped-character *)
if
pos + 2 < String.size str
andalso String.sub (str, pos + 2) = #"-"
andalso pos + 3 < String.size str
then
(* we do have a character range,
* which may possibly be escaped *)
case String.sub (str, pos + 3) of
#"(" => NONE
| #")" => NONE
| #"[" => NONE
| #"]" => NONE
| #"+" => NONE
| #"*" => NONE
| #"|" => NONE
| #"?" => NONE
| #"." => NONE
| #"-" => NONE
| #"\\" =>
if pos + 4 < String.size str then
let
val chr2 = String.sub (str, pos + 4)
val (isValid, chr2) = isValidEscapeSequence chr2
val acc =
if chr < chr2 then
getCharsBetween (chr, chr2, acc)
else
getCharsBetween (chr2, chr, acc)
in
getCharsInBrackets (pos + 5, str, acc)
end
else
NONE
| chr2 =>
let
val acc =
if chr < chr2 then getCharsBetween (chr, chr2, acc)
else getCharsBetween (chr2, chr, acc)
in
getCharsInBrackets (pos + 4, str, acc)
end
else
(* no character range we have to check *)
getCharsInBrackets (pos + 2, str, chr :: acc)
else
NONE
end
| #"]" =>
let val chars = Vector.fromList acc
in SOME (pos + 1, chars)
end
| chr =>
if
pos + 1 < String.size str andalso String.sub (str, pos + 1) = #"-"
andalso pos + 2 < String.size str
then
(* handle character ranges like a-z.
* There are edge cases regarding
* the second character in the range.
* We have to check that any unescaped metacharacters
* return an invalid parse state.
* We also have to unescape any escape sequences.
* *)
case String.sub (str, pos + 2) of
#"\\" =>
(* second char contains an escape sequence *)
if pos + 3 < String.size str then
let
val chr2 = String.sub (str, pos + 3)
val (isValid, chr2) = isValidEscapeSequence chr2
val acc =
if chr < chr2 then getCharsBetween (chr, chr2, acc)
else getCharsBetween (chr2, chr, acc)
in
if isValid then getCharsInBrackets (pos + 4, str, acc)
else NONE
end
else
NONE
| #"(" => NONE
| #")" => NONE
| #"[" => NONE
| #"]" => NONE
| #"+" => NONE
| #"*" => NONE
| #"|" => NONE
| #"?" => NONE
| #"." => NONE
| #"-" => NONE
| chr2 =>
(* valid char range *)
let
val acc =
if chr < chr2 then getCharsBetween (chr, chr2, acc)
else getCharsBetween (chr2, chr, acc)
in
getCharsInBrackets (pos + 3, str, acc)
end
else
getCharsInBrackets (pos + 1, str, chr :: acc)
fun parseCharacterClass (pos, str, stateNum) =
case getCharsInBrackets (pos, str, []) of
SOME (pos, chars) =>
let
val node = IS_ANY_CHARACTER {chars = chars, position = stateNum + 1}
in
SOME (pos, node, stateNum + 1)
end
| NONE => NONE
fun parseNegateCharacterClass (pos, str, stateNum) =
case getCharsInBrackets (pos, str, []) of
SOME (pos, chars) =>
let
val node =
NOT_ANY_CHARACTER {chars = chars, position = stateNum + 1}
in
SOME (pos, node, stateNum + 1)
end
| NONE => NONE
fun computeAtom (pos, str, stateNum) =
if pos = String.size str then
NONE
else
case String.sub (str, pos) of
#"(" =>
(case getRightParenIdx (pos + 1, str) of
SOME groupEndIdx =>
let
val substr = String.substring
(str, pos + 1, groupEndIdx - pos - 1)
in
case parse (substr, stateNum) of
SOME (rhs, stateNum) =>
SOME (groupEndIdx + 1, rhs, stateNum)
| NONE => NONE
end
| NONE => NONE)
| #"\\" =>
(* escape sequences *)
if pos + 1 = String.size str then
NONE
else
let
val chr = String.sub (str, pos + 1)
val (isValid, chr) = isValidEscapeSequence chr
in
if Fn.charIsEqual (chr, Fn.endMarker) then
NONE
else if isValid then
let
val chr = CHAR_LITERAL {char = chr, position = stateNum + 1}
in
SOME (pos + 2, chr, stateNum + 1)
end
else
NONE
end
| #"." =>
let val w = WILDCARD (stateNum + 1)
in SOME (pos + 1, w, stateNum + 1)
end
| #"[" =>
if pos + 1 = String.size str then
NONE
else if String.sub (str, pos + 1) = #"^" then
parseNegateCharacterClass (pos + 2, str, stateNum)
else
parseCharacterClass (pos + 1, str, stateNum)
| #")" => NONE
| #"]" => NONE
| #"+" => NONE
| #"*" => NONE
| #"|" => NONE
| #"?" => NONE
| #"-" => NONE
| chr =>
if Fn.charIsEqual (chr, Fn.endMarker) then
NONE
else
let val chr = CHAR_LITERAL {char = chr, position = stateNum + 1}
in SOME (pos + 1, chr, stateNum + 1)
end
and climb (pos, str, lhs, level, stateNum) : (int * parse_tree * int) option =
if pos = String.size str then
SOME (pos, lhs, stateNum)
else
case String.sub (str, pos) of
#"|" =>
if level < altLevel then
SOME (pos, lhs, stateNum)
else if pos + 1 < String.size str then
let
val chr = String.sub (str, pos + 1)
val chr = CHAR_LITERAL {char = chr, position = stateNum + 1}
in
case climb (pos + 2, str, chr, altLevel, stateNum + 1) of
SOME (pos, rhs, rightStateNum) =>
let
val result = ALTERNATION
{ l = lhs
, r = rhs
, leftMaxState = stateNum
, rightMaxState = rightStateNum
}
in
SOME (pos, result, rightStateNum)
end
| NONE => NONE
end
else
NONE
| #"?" =>
if level < postfixLevel then
SOME (pos, lhs, stateNum)
else
let val lhs = ZERO_OR_ONE lhs
in climb (pos + 1, str, lhs, postfixLevel, stateNum)
end
| #"*" =>
if level < postfixLevel then
SOME (pos, lhs, stateNum)
else
let val lhs = ZERO_OR_MORE lhs
in climb (pos + 1, str, lhs, postfixLevel, stateNum)
end
| #"+" =>
if level < postfixLevel then
SOME (pos, lhs, stateNum)
else
let val lhs = ONE_OR_MORE lhs
in climb (pos + 1, str, lhs, postfixLevel, stateNum)
end
| chr =>
if level < concatLevel then
SOME (pos, lhs, stateNum)
else
case computeAtom (pos, str, stateNum) of
SOME (nextPos, curAtom, atomStateNum) =>
(case climb (nextPos, str, curAtom, concatLevel, atomStateNum) of
SOME (pos, rhs, rightStateNum) =>
let
val result = CONCAT
{ l = lhs
, r = rhs
, leftMaxState = stateNum
, rightMaxState = rightStateNum
}
in
SOME (pos, result, rightStateNum)
end
| NONE => NONE)
| NONE => NONE
and loop (pos, str, ast, stateNum) =
if pos = String.size str then
SOME (ast, stateNum)
else
case climb (pos, str, ast, altLevel, stateNum) of
SOME (pos, ast, stateNum) => loop (pos, str, ast, stateNum)
| NONE => NONE
and parse (str, stateNum) =
if String.size str > 0 then
case computeAtom (0, str, stateNum) of
SOME (nextPos, lhs, stateNum) => loop (nextPos, str, lhs, stateNum)
| NONE => NONE
else
NONE
end
structure ToDfa =
struct
type dstate_element = {marked: bool, transitions: int list}
type dstate_vec = dstate_element vector
fun chrExistsInVec (idx, vec, curChr) =
if idx = Vector.length vec then
false
else
let
val idxChr = Vector.sub (vec, idx)
in
Fn.charIsEqual (idxChr, curChr)
orelse chrExistsInVec (idx + 1, vec, curChr)
end
fun addKeysToFollowSet (lst, addSet, followSet) =
case lst of
hd :: tl =>
let
val currentFollows = Set.getOrDefault (hd, followSet, [])
val updatedFollows = Set.addFromList (currentFollows, addSet)
val updatedFollows: int list = Set.keysToList updatedFollows
val followSet = Set.insertOrReplace (hd, updatedFollows, followSet)
in
addKeysToFollowSet (tl, addSet, followSet)
end
| [] => followSet
fun addToFollowSet (tree, followSet) =
case tree of
WILDCARD _ => followSet
| CHAR_LITERAL {char, position} =>
(* we add the endMarker and its position to the followSet *)
if char = Fn.endMarker then
Set.insertOrReplace (position, [Char.ord Fn.endMarker], followSet)
else
followSet
| IS_ANY_CHARACTER _ => followSet
| NOT_ANY_CHARACTER _ => followSet
| CONCAT {l, r, ...} =>
let
val followSet = addToFollowSet (l, followSet)
val followSet = addToFollowSet (r, followSet)
val lpOfLeft = lastpos (l, [])
val fpOfRight = firstpos (r, [])
val fpOfRight = Set.addFromList (fpOfRight, Set.LEAF)
in
addKeysToFollowSet (lpOfLeft, fpOfRight, followSet)
end
| ALTERNATION {l, r, ...} =>
let val followSet = addToFollowSet (l, followSet)
in addToFollowSet (r, followSet)
end
| ZERO_OR_MORE child =>
let
val followSet = addToFollowSet (child, followSet)
val fp = firstpos (child, [])
val fp = Set.addFromList (fp, Set.LEAF)
val lp = lastpos (child, [])
in
addKeysToFollowSet (lp, fp, followSet)
end
| ONE_OR_MORE child =>
let
val followSet = addToFollowSet (child, followSet)
val lp = lastpos (child, [])
val fp = firstpos (child, [])
val fp = Set.addFromList (fp, Set.LEAF)
in
addKeysToFollowSet (lp, fp, followSet)
end
| ZERO_OR_ONE child => addToFollowSet (child, followSet)
| GROUP child => addToFollowSet (child, followSet)
fun appendIfNew (pos, dstates, newStates) =
if pos = Vector.length dstates then
let
val record = {transitions = newStates, marked = false}
val dstates = Vector.concat [dstates, Vector.fromList [record]]
in
(pos, dstates)
end
else
let
val {transitions: int list, marked = _} = Vector.sub (dstates, pos)
in
if transitions = newStates then (pos, dstates)
else appendIfNew (pos + 1, dstates, newStates)
end
fun getUnmarkedTransitionsIfExists (pos, dstates) =
if pos = Vector.length dstates then
NONE
else
let
val record: dstate_element = Vector.sub (dstates, pos)
in
if #marked record then
getUnmarkedTransitionsIfExists (pos + 1, dstates)
else
SOME (pos, #transitions record)
end
fun isCharMatch (regex, pos, curChr) =
case regex of
CHAR_LITERAL {char, ...} => Fn.charIsEqual (char, curChr)
| WILDCARD _ => true
| IS_ANY_CHARACTER {chars, ...} => chrExistsInVec (0, chars, curChr)
| NOT_ANY_CHARACTER {chars, ...} =>
let val charIsValid = chrExistsInVec (0, chars, curChr)
in not charIsValid
end
| ALTERNATION {l, r, leftMaxState, ...} =>
if pos > leftMaxState then isCharMatch (r, pos, curChr)
else isCharMatch (l, pos, curChr)
| CONCAT {l, r, leftMaxState, ...} =>
if pos > leftMaxState then isCharMatch (r, pos, curChr)
else isCharMatch (l, pos, curChr)
| ZERO_OR_ONE child => isCharMatch (child, pos, curChr)
| ZERO_OR_MORE child => isCharMatch (child, pos, curChr)
| ONE_OR_MORE child => isCharMatch (child, pos, curChr)
| GROUP child => isCharMatch (child, pos, curChr)
fun positionsThatCorrespondToChar
(char, curStates, regex, acc, followSet, hasAnyMatch) =
case curStates of
[] => List.concat (Set.valuesToList acc)
| pos :: tl =>
if isCharMatch (regex, pos, Char.chr char) then
let
(* get union of new and previous follows *)
val prevFollows = Set.getOrDefault (char, acc, [])
val newFollows = Set.getOrDefault (pos, followSet, [])
val tempSet = Set.addFromList (prevFollows, Set.LEAF)
val tempSet = Set.addFromList (newFollows, tempSet)
val allFollowList = Set.keysToList tempSet
(* store union of new and previous follows so far *)
val acc = Set.insertOrReplace (char, allFollowList, acc)
in
positionsThatCorrespondToChar
(char, tl, regex, acc, followSet, true)
end
else
positionsThatCorrespondToChar
(char, tl, regex, acc, followSet, hasAnyMatch)
structure Dtran =
struct
(* vector, with idx corresponding to state in dstate,
* an int key which corresponds to char's ascii code,
* and an int value corresponding to state we will transition to *)
type t = int Set.set vector
fun insert (dStateIdx, char, toStateIdx, dtran: t) =
if dStateIdx = Vector.length dtran then
let
val el = Set.insertOrReplace (char, toStateIdx, Set.LEAF)
val el = Vector.fromList [el]
in
Vector.concat [dtran, el]
end
else if dStateIdx < Vector.length dtran then
let
val el = Vector.sub (dtran, dStateIdx)
val el = Set.insertOrReplace (char, toStateIdx, el)
in
Vector.update (dtran, dStateIdx, el)
end
else
let
val appendLength = dStateIdx - Vector.length dtran
val appendVecs = Vector.tabulate (appendLength, fn _ => Set.LEAF)
val dtran = Vector.concat [dtran, appendVecs]
in
insert (dStateIdx, char, toStateIdx, dtran)
end
end
fun convertChar
( char
, regex
, dstates
, dtran: Dtran.t
, unmarkedState
, unmarkedIdx
, followSet
, prevDstateLength
) =
if char < 0 then
(dstates, dtran)
else
let
val u = positionsThatCorrespondToChar
(char, unmarkedState, regex, Set.LEAF, followSet, false)
in
case u of
[] =>
convertChar
( char - 1
, regex
, dstates
, dtran
, unmarkedState
, unmarkedIdx
, followSet
, prevDstateLength
)
| _ =>
let
(* dtran is idx -> char -> state_list map *)
val (uIdx, dstates) = appendIfNew (0, dstates, u)
val dtran = Dtran.insert (unmarkedIdx, char, uIdx, dtran)
in
convertChar
( char - 1
, regex
, dstates
, dtran
, unmarkedState
, unmarkedIdx
, followSet
, prevDstateLength
)
end
end
fun convertLoop (regex, dstates, dtran, followSet) =
case getUnmarkedTransitionsIfExists (0, dstates) of
SOME (unmarkedIdx, unamarkedTransition) =>
let
(* mark transition *)
val dstates =
let
val newMark = {marked = true, transitions = unamarkedTransition}
in
Vector.update (dstates, unmarkedIdx, newMark)
end
val (dstates, dtran) = convertChar
( 255
, regex
, dstates
, dtran
, unamarkedTransition
, unmarkedIdx
, followSet
, Vector.length dstates
)
in
convertLoop (regex, dstates, dtran, followSet)
end
| NONE =>
Vector.map
(fn set =>
Vector.tabulate (256, fn i => Set.getOrDefault (i, set, ~1)))
dtran
fun convert regex =
let
val followSet = addToFollowSet (regex, Set.LEAF)
(* get firstpos, sorted *)
val first = firstpos (regex, [])
val first = Set.addFromList (first, Set.LEAF)
val first = Set.keysToList first
val dstates = Vector.fromList [{transitions = first, marked = false}]
in
convertLoop (regex, dstates, Vector.fromList [Set.LEAF], followSet)
end
end
fun fromString str =
case ParseDfa.parse (str, 0) of
SOME (ast, numStates) =>
let
val endMarker =
CHAR_LITERAL {char = Fn.endMarker, position = numStates + 1}
val ast = CONCAT
{ l = ast
, leftMaxState = numStates
, r = endMarker
, rightMaxState = numStates + 1
}
in
ToDfa.convert ast
end
| NONE => Vector.fromList []
type dfa = int vector vector
type dfa_state = int
fun nextState (dfa: dfa, curState: dfa_state, chr) =
let val curTable = Vector.sub (dfa, curState)
in Vector.sub (curTable, Char.ord chr)
end
fun isFinal (dfa: dfa, curState: dfa_state) =
curState <> ~1
andalso
let
val curTable = Vector.sub (dfa, curState)
val endMarkerCode = Char.ord Fn.endMarker
in
Vector.sub (curTable, endMarkerCode) <> ~1
end
fun isDead (curState: dfa_state) = curState = ~1
fun helpMatchString (strPos, str, dfa, curState, startPos, prevFinalPos, acc) =
if strPos = String.size str then
let
val acc =
if prevFinalPos = ~1 then acc else (startPos, prevFinalPos) :: acc
in
List.rev acc
end
else
let
val chr = String.sub (str, strPos)
val newState = nextState (dfa, curState, chr)
val prevFinalPos =
if isFinal (dfa, newState) then strPos else prevFinalPos
in
if isDead newState then
if prevFinalPos = ~1 then
(* restart from startPos *)
helpMatchString (startPos + 1, str, dfa, 0, startPos + 1, ~1, acc)
else
let
val acc = (startPos, prevFinalPos) :: acc
in
helpMatchString
(prevFinalPos + 1, str, dfa, 0, prevFinalPos + 1, ~1, acc)
end
else
helpMatchString
(strPos + 1, str, dfa, newState, startPos, prevFinalPos, acc)
end
fun matchString (dfa, string) =
if Vector.length dfa = 0 then []
else helpMatchString (0, string, dfa, 0, 0, ~1, [])
end
structure CaseInsensitiveDfa =
MakeDfaGen
(struct
val endMarker = #"\^@"
fun charIsEqual (a: char, b: char) = Char.toLower a = Char.toLower b
end)
structure CaseSensitiveDfa =
MakeDfaGen
(struct
val endMarker = #"\^@"
fun charIsEqual (a: char, b: char) = a = b
end)

View File

@@ -0,0 +1,254 @@
structure SearchList =
struct
structure Dfa = CaseInsensitiveDfa
fun buildLoop (idx, buffer, dfa, acc, curState, startPos, prevFinalPos) =
let
val buffer = LineGap.goToIdx (idx, buffer)
in
if idx = #textLength buffer then
let
val acc =
if prevFinalPos < 0 then acc
else PersistentVector.append (startPos, prevFinalPos, acc)
in
(buffer, acc)
end
else
let
val chr = LineGap.sub (idx, buffer)
val newState = Dfa.nextState (dfa, curState, chr)
val prevFinalPos =
if Dfa.isFinal (dfa, newState) then idx else prevFinalPos
in
if Dfa.isDead newState then
if prevFinalPos = ~1 then
(* no match found: restart search from `startPos + 1` *)
buildLoop (startPos + 1, buffer, dfa, acc, 0, startPos + 1, ~1)
else
(* match found: append and continue *)
let
val acc = PersistentVector.append (startPos, prevFinalPos, acc)
(* we start 1 idx after the final position we found *)
val newStart = prevFinalPos + 1
in
buildLoop (newStart, buffer, dfa, acc, 0, newStart, ~1)
end
else
buildLoop
(idx + 1, buffer, dfa, acc, newState, startPos, prevFinalPos)
end
end
fun build (buffer, dfa) =
if Vector.length dfa > 0 then
let val buffer = LineGap.goToStart buffer
in buildLoop (0, buffer, dfa, PersistentVector.empty, 0, 0, ~1)
end
else
(buffer, PersistentVector.empty)
fun rangeLoop
( dfa
, bufferPos
, buffer
, finishIdx
, searchList
, curState
, startPos
, prevFinalPos
) =
if bufferPos = #textLength buffer orelse bufferPos > finishIdx then
let
val searchList =
if prevFinalPos = ~1 then searchList
else PersistentVector.append (startPos, prevFinalPos, searchList)
in
(buffer, searchList)
end
else
let
val buffer = LineGap.goToIdx (bufferPos, buffer)
val chr = LineGap.sub (bufferPos, buffer)
val newState = Dfa.nextState (dfa, curState, chr)
val prevFinalPos =
if Dfa.isFinal (dfa, newState) then bufferPos else prevFinalPos
in
if Dfa.isDead newState then
if prevFinalPos = ~1 then
(* no match found: restart search from `startPos + 1` *)
rangeLoop
( dfa
, startPos + 1
, buffer
, finishIdx
, searchList
, 0
, startPos + 1
, ~1
)
else
(* match found: append and continue *)
let
val searchList =
PersistentVector.append (startPos, prevFinalPos, searchList)
(* we start 1 idx after the final position we found *)
val newStart = prevFinalPos + 1
in
rangeLoop
(dfa, newStart, buffer, finishIdx, searchList, 0, newStart, ~1)
end
else
(* continue searching for match *)
rangeLoop
( dfa
, bufferPos + 1
, buffer
, finishIdx
, searchList
, newState
, startPos
, prevFinalPos
)
end
fun buildRange (buffer, finishIdx, dfa) =
if Vector.length dfa > 0 then
rangeLoop
( dfa
, #idx buffer
, buffer
, finishIdx
, PersistentVector.empty
, 0
, #idx buffer
, ~1
)
else
(buffer, PersistentVector.empty)
fun insertUntilMatch
(idx, buffer, searchList, dfa, curState, startPos, prevFinalPos) =
if idx = #textLength buffer then
if prevFinalPos < 0 then
(buffer, searchList)
else if PersistentVector.isInRange (prevFinalPos, searchList) then
(buffer, searchList)
else
let
val searchList =
PersistentVector.insertMatchKeepingAbsoluteInddices
(startPos, prevFinalPos, searchList)
in
(buffer, searchList)
end
else if Dfa.isDead curState then
if prevFinalPos = ~1 then
(* no match found: restart search from `startPos + 1` *)
insertUntilMatch
(startPos + 1, buffer, searchList, dfa, 0, startPos + 1, ~1)
else if PersistentVector.isInRange (prevFinalPos, searchList) then
(buffer, searchList)
else
(* new match. Insert and continue *)
let
val searchList =
PersistentVector.insertMatchKeepingAbsoluteInddices
(startPos, prevFinalPos, searchList)
val newStart = prevFinalPos + 1
in
insertUntilMatch (newStart, buffer, searchList, dfa, 0, newStart, ~1)
end
else
let
val buffer = LineGap.goToIdx (idx, buffer)
val chr = LineGap.sub (idx, buffer)
val newState = Dfa.nextState (dfa, curState, chr)
val prevFinalPos =
if Dfa.isFinal (dfa, newState) then idx else prevFinalPos
in
(* continue *)
insertUntilMatch
(idx + 1, buffer, searchList, dfa, newState, startPos, prevFinalPos)
end
fun tryExtendingPrevMatch
(idx, buffer, searchList, dfa, finalPos, curState, start) =
if idx = #textLength buffer then
let
val searchList =
PersistentVector.extendExistingMatch (start, finalPos, searchList)
in
(buffer, searchList)
end
else if Dfa.isDead curState then
let
val searchList =
PersistentVector.extendExistingMatch (start, finalPos, searchList)
in
insertUntilMatch
(finalPos + 1, buffer, searchList, dfa, 0, finalPos + 1, ~1)
end
else
let
val buffer = LineGap.goToIdx (idx, buffer)
val chr = LineGap.sub (idx, buffer)
val newState = Dfa.nextState (dfa, curState, chr)
val finalPos = if Dfa.isFinal (dfa, newState) then idx else finalPos
in
(* continue *)
tryExtendingPrevMatch
(idx + 1, buffer, searchList, dfa, finalPos, newState, start)
end
fun deleteBufferAndSearchList (start, length, buffer, searchList, dfa) =
let
val buffer = LineGap.delete (start, length, buffer)
val searchList = PersistentVector.delete (start, length, searchList)
val oldStart = PersistentVector.prevMatch (start, searchList, 1)
in
if Vector.length dfa = 0 then
(buffer, searchList)
else if oldStart >= start orelse oldStart = ~1 then
(* no previous match, so try searching for a match from start of buffer *)
insertUntilMatch (0, buffer, searchList, dfa, 0, 0, ~1)
else
tryExtendingPrevMatch
(oldStart, buffer, searchList, dfa, ~1, 0, oldStart)
end
(* inserts into buffer and searchList both *)
fun insert (insIdx, insString, buffer, searchList, dfa) =
let
val buffer = LineGap.insert (insIdx, insString, buffer)
(* incremennt existing elements in the searchList after the insIdx
* by the length of the string that was just inserted *)
val searchList =
let
val searchListLeft = PersistentVector.splitLeft (insIdx, searchList)
val insLength = String.size insString
val searchListRight =
PersistentVector.splitRight (insIdx + insLength, searchList)
val searchListRight = PersistentVector.empty
in
if PersistentVector.isEmpty searchListLeft then searchListRight
else if PersistentVector.isEmpty searchListRight then searchListLeft
else PersistentVector.merge (searchListLeft, searchListRight)
end
val oldStart = PersistentVector.prevMatch (insIdx, searchList, 1)
in
if Vector.length dfa = 0 then
(buffer, searchList)
else if oldStart >= insIdx orelse oldStart = ~1 then
(* no previous match, so try searching for a match from start of buffer *)
insertUntilMatch (0, buffer, searchList, dfa, 0, 0, ~1)
else
tryExtendingPrevMatch
(oldStart, buffer, searchList, dfa, ~1, 0, oldStart)
end
end

View File

@@ -0,0 +1,142 @@
structure NormalModeTextBuilder =
struct
structure Utils = TextBuilderUtils
(* Prerequisite to all functions in this structure:
* - Move buffer to startLine before calling any function. *)
fun startBuild
( startLine
, cursorPos
, buffer: LineGap.t
, windowWidth
, windowHeight
, floatWindowWidth
, floatWindowHeight
, searchList
, visualScrollColumn
, acc
) =
let
val
{ rightStrings
, rightLines
, line = curLine
, idx = curIdx
, textLength
, ...
} = buffer
val env = Utils.initEnv
( 0
, 0
, windowWidth
, windowHeight
, floatWindowWidth
, floatWindowHeight
, searchList
, visualScrollColumn
, startLine
)
val {startX, startY, ...} = env
in
if textLength = 1 then
(* empty string, so there is nothing we can draw
* except a cursor at the line start.
* An empty string is usually thought of to have a length of 0
* and this is true, but we always have a \n at the end of the buffer
* to respect Unix-style file endings, which we always uphold.
* So, for us, an empty string has a length of 1. *)
[Utils.makeCursor (startX, startY, env)]
else
case (rightStrings, rightLines) of
(shd :: stl, lhd :: ltl) =>
let
(* get relative index of line to start building from *)
val strPos =
Utils.getRelativeLineStartFromRightHead
(startLine, curLine, lhd)
(* get absolute idx of line *)
val absIdx = curIdx + strPos
in
if PersistentVector.isEmpty searchList then
TextBuilderWithCursor.build
( strPos
, shd
, stl
, lhd
, ltl
, startX
, startY
, 0
, startLine
, absIdx
, cursorPos
, env
, acc
)
else
TextBuilderWithHighlight.build
( strPos
, shd
, stl
, lhd
, ltl
, startX
, startY
, 0
, startLine
, absIdx
, cursorPos
, env
, acc
)
end
| (_, _) => acc
end
fun buildWithExisting
( startLine
, cursorPos
, buffer: LineGap.t
, windowWidth
, windowHeight
, searchList
, visualScrollColumn
, acc
) =
startBuild
( startLine
, cursorPos
, buffer
, windowWidth
, windowHeight
, Real32.fromInt windowWidth
, Real32.fromInt windowHeight
, searchList
, visualScrollColumn
, []
)
fun build
( startLine
, cursorPos
, buffer: LineGap.t
, windowWidth
, windowHeight
, searchList
, visualScrollColumn
) =
startBuild
( startLine
, cursorPos
, buffer
, windowWidth
, windowHeight
, Real32.fromInt windowWidth
, Real32.fromInt windowHeight
, searchList
, visualScrollColumn
, []
)
end

View File

@@ -0,0 +1,131 @@
structure SearchBar =
struct
structure TC = TextConstants
structure Utils = TextBuilderUtils
fun loop
(pos, str, posX, posY, endX, acc, floatWindowWidth, floatWindowHeight) =
if pos = String.size str then
acc
else if posX >= endX then
acc
else
let
val chr = String.sub (str, pos)
val r: Real32.real = 0.01
val g: Real32.real = 0.01
val b: Real32.real = 0.01
val fPosX = Real32.fromInt posX
val fPosY = Real32.fromInt posY
val z: Real32.real = 0.1
val chr = CozetteAscii.make
( chr
, fPosX
, fPosY
, z
, TC.scale
, floatWindowWidth
, floatWindowHeight
, r
, g
, b
)
val acc = chr :: acc
val nextPosX = posX + TC.xSpace
in
loop
( pos + 1
, str
, nextPosX
, posY
, endX
, acc
, floatWindowWidth
, floatWindowHeight
)
end
(* builds a single text line from a string.
* Used for getting Real32.real vector representing search input.
* Todo: add scrolling, so that text scrolls horizontally when greater than width. *)
fun build
( str
, startX
, startY
, endX
, floatWindowWidth
, floatWindowHeight
, searchCursorIdx
, searchScrollColumn
, caseSensitive
) =
let
val r: Real32.real = 0.1
val g: Real32.real = 0.1
val b: Real32.real = 0.1
val z: Real32.real = 0.1
val width = endX - startX
val (startX, endX) =
if TC.textLineWidth > width then
(startX, endX)
else
let
val startX = (width - TC.textLineWidth) div 2
val endX = startX + TC.textLineWidth
in
(startX, endX)
end
val fPosX = Real32.fromInt startX
val fPosY = Real32.fromInt startY
val searchSymbol = CozetteAscii.make
( if caseSensitive then #"?" else #"/"
, fPosX
, fPosY
, z
, TC.scale
, floatWindowWidth
, floatWindowHeight
, r
, g
, b
)
val cursor =
let
val xpos = (searchCursorIdx + 1) - searchScrollColumn
val xpos = TextConstants.xSpace * xpos + startX
val xpos = Int.min (endX, xpos)
val x = Real32.fromInt xpos
in
PipeCursor.lerp
( x
, fPosY
, 0.01
, TC.scale
, floatWindowWidth
, floatWindowHeight
, r
, g
, b
)
end
val posX = startX + TC.xSpace
in
loop
( searchScrollColumn
, str
, posX
, startY
, endX
, [cursor, searchSymbol]
, floatWindowWidth
, floatWindowHeight
)
end
end

View File

@@ -0,0 +1,250 @@
structure TextBuilderUtils =
struct
structure TC = TextConstants
type env_data =
{ charR: Real32.real
, charG: Real32.real
, charB: Real32.real
, cursorR: Real32.real
, cursorG: Real32.real
, cursorB: Real32.real
, searchHighlightR: Real32.real
, searchHighlightG: Real32.real
, searchHighlightB: Real32.real
(* different colours for char when cursor is on char *)
, cursorHighlightedCharR: Real32.real
, cursorHighlightedCharG: Real32.real
, cursorHighlightedCharB: Real32.real
, searchHighlightedCharR: Real32.real
, searchHighlightedCharG: Real32.real
, searchHighlightedCharB: Real32.real
, charZ: Real32.real
, cursorZ: Real32.real
, searchHighlightZ: Real32.real
, startX: int
, startY: int
, scrollColumnStart: int
, scrollColumnEnd: int
, lastLineNumber: int
(* fw/fh = float window width and float window height *)
, fw: Real32.real
, fh: Real32.real
, searchList: PersistentVector.t
}
fun initEnv
( startX
, startY
, endX
, endY
, floatWindowWidth
, floatWindowHeight
, searchList
, visualScrollColumn
, startLine
) : env_data =
let
val width = endX - startX
val lastLineNumber =
let
val height = endY - startY
val howManyLines = height div TC.ySpace
in
startLine + howManyLines
end
in
if TC.textLineWidth > width then
{ charR = 0.0
, charG = 0.0
, charB = 0.0
, searchHighlightR = 0.41
, searchHighlightG = 0.05
, searchHighlightB = 0.67
, cursorR = 0.0
, cursorG = 0.0
, cursorB = 0.0
, searchHighlightedCharR = 0.89
, searchHighlightedCharG = 0.89
, searchHighlightedCharB = 0.89
, cursorHighlightedCharR = 0.89
, cursorHighlightedCharG = 0.89
, cursorHighlightedCharB = 0.89
, charZ = 0.01
, cursorZ = 0.03
, searchHighlightZ = 0.05
, startX = startX
, startY = startX
, scrollColumnStart = visualScrollColumn
, scrollColumnEnd = width div TC.xSpace + visualScrollColumn
, lastLineNumber = lastLineNumber
, fw = floatWindowWidth
, fh = floatWindowHeight
, searchList = searchList
}
else
let
val startX = (width - TC.textLineWidth) div 2
in
{ charR = 0.0
, charG = 0.0
, charB = 0.0
, searchHighlightR = 0.41
, searchHighlightG = 0.05
, searchHighlightB = 0.67
, cursorR = 0.0
, cursorG = 0.0
, cursorB = 0.0
, searchHighlightedCharR = 0.89
, searchHighlightedCharG = 0.89
, searchHighlightedCharB = 0.89
, cursorHighlightedCharR = 0.89
, cursorHighlightedCharG = 0.89
, cursorHighlightedCharB = 0.89
, charZ = 0.01
, cursorZ = 0.03
, searchHighlightZ = 0.05
, startX = startX
, startY = startY
, scrollColumnStart = visualScrollColumn
, scrollColumnEnd = visualScrollColumn + TC.textLineCount
, lastLineNumber = lastLineNumber
, fw = floatWindowWidth
, fh = floatWindowHeight
, searchList = searchList
}
end
end
(* different functions to make vectors of different things we want to draw. *)
fun makeCursor (posX, posY, env: env_data) =
Rect.lerp
( Real32.fromInt (posX - 2)
, Real32.fromInt posY
, #cursorZ env
, TC.scale
, #fw env
, #fh env
, #cursorR env
, #cursorG env
, #cursorB env
)
fun makeSearchHighlight (posX, posY, env: env_data) =
Rect.lerp
( Real32.fromInt (posX - 2)
, Real32.fromInt posY
, #searchHighlightZ env
, TC.scale
, #fw env
, #fh env
, #searchHighlightR env
, #searchHighlightG env
, #searchHighlightB env
)
fun makeChr (chr, posX, posY, env: env_data) =
CozetteAscii.make
( chr
, Real32.fromInt posX
, Real32.fromInt posY
, #charZ env
, TC.scale
, #fw env
, #fh env
, #charR env
, #charG env
, #charB env
)
fun makeCursorHighlightedChr (chr, posX, posY, env: env_data) =
CozetteAscii.make
( chr
, Real32.fromInt posX
, Real32.fromInt posY
, #charZ env
, TC.scale
, #fw env
, #fh env
, #cursorHighlightedCharR env
, #cursorHighlightedCharG env
, #cursorHighlightedCharB env
)
fun makeSearchHighlightedChr (chr, posX, posY, env: env_data) =
CozetteAscii.make
( chr
, Real32.fromInt posX
, Real32.fromInt posY
, #charZ env
, TC.scale
, #fw env
, #fh env
, #searchHighlightedCharR env
, #searchHighlightedCharG env
, #searchHighlightedCharB env
)
(* gets line start idx, relative to right hd *)
fun getRelativeLineStartFromRightHead (startLine, curLine, rLnHd) =
if startLine > curLine then
let val lnPos = startLine - curLine - 1
in Vector.sub (rLnHd, lnPos) + 1
end
else
0
(* gets line start idx, absolute *)
fun getAbsoluteLineStartFromRightHead (curIdx, startLine, curLine, rLnHd) =
let
val startIdx =
if startLine > curLine then
let val lnPos = startLine - curLine - 1
in Vector.sub (rLnHd, lnPos) + 1
end
else
0
in
curIdx + startIdx
end
fun getLineAbsIdxFromBuffer (startLine, buffer: LineGap.t) =
let
val {rightLines, line = curLine, idx = curIdx, ...} = buffer
in
case rightLines of
rLnHd :: _ =>
getAbsoluteLineStartFromRightHead (curIdx, startLine, curLine, rLnHd)
| [] =>
raise Fail
"text-builder-utils.sml 268:\
\should never call function when at end of buffer"
end
end

View File

@@ -0,0 +1,253 @@
structure TextBuilderWithCursor =
struct
structure TC = TextConstants
structure Utils = TextBuilderUtils
fun goToFirstLineAfter
(stl, ltl, posY, lineNumber, absIdx, cursorIdx, env, acc) =
case (stl, ltl) of
(shd :: stl, lhd :: ltl) =>
if Vector.length lhd > 0 then
let
val lineOffset = Vector.sub (lhd, 0)
val strPos = lineOffset + 1
val absIdx = absIdx + strPos
val posY = posY + TC.ySpace
val lineNumber = lineNumber + 1
in
build
( strPos
, shd
, stl
, lhd
, ltl
, #startX env
, posY
, 0
, lineNumber
, absIdx
, cursorIdx
, env
, acc
)
end
else
(* keep looping until we find a linebreak *)
goToFirstLineAfter
( stl
, ltl
, posY
, lineNumber
, absIdx + String.size shd
, cursorIdx
, env
, acc
)
| (_, _) => acc
and skipToNextLine
(pos, str, stl, line, ltl, posY, lineNumber, absIdx, cursorIdx, env, acc) =
if Vector.length line = 0 then
let
(* get index of buffer after this string *)
val absIdx = absIdx - pos
val absIdx = absIdx + String.size str
in
goToFirstLineAfter
(stl, ltl, posY, lineNumber, absIdx, cursorIdx, env, acc)
end
else
(* bin search lines *)
let
val linePos = BinSearch.equalOrMore (pos + 1, line)
in
if linePos = ~1 then
(* next line is not in this node *)
let
val absIdx = absIdx - pos
val absIdx = absIdx + String.size str
in
goToFirstLineAfter
(stl, ltl, posY, lineNumber, absIdx, cursorIdx, env, acc)
end
else
let
val lineOffset = Vector.sub (line, linePos)
val newStrPos = lineOffset + 1
val absIdx = absIdx - pos + newStrPos
val posY = posY + TC.ySpace
val lineNumber = lineNumber + 1
in
build
( newStrPos
, str
, stl
, line
, ltl
, #startX env
, posY
, 0
, lineNumber
, absIdx
, cursorIdx
, env
, acc
)
end
end
and build
( pos
, str
, stl
, line
, ltl
, posX
, posY
, column
, lineNumber
, absIdx
, cursorIdx
, env: Utils.env_data
, acc
) =
if pos = String.size str then
case (stl, ltl) of
(str :: stl, line :: ltl) =>
build
( 0
, str
, stl
, line
, ltl
, posX
, posY
, column
, lineNumber
, absIdx
, cursorIdx
, env
, acc
)
| (_, _) => acc
else
case String.sub (str, pos) of
#"\n" =>
if lineNumber + 1 > #lastLineNumber env then
acc
else
let
val acc =
if absIdx = cursorIdx then
Utils.makeCursor (posX, posY, env) :: acc
else
acc
in
build
( pos + 1
, str
, stl
, line
, ltl
, #startX env
, posY + TC.ySpace
, 0
, lineNumber + 1
, absIdx + 1
, cursorIdx
, env
, acc
)
end
| #" " =>
let
val acc =
if absIdx = cursorIdx then
Utils.makeCursor (posX, posY, env) :: acc
else
acc
val posX =
if column < #scrollColumnStart env then
(* if we are prior to the start column,
* we want to set the x position to be at the start
* in preparation for when we are at the start column *)
#startX env
else
posX + TC.xSpace
in
build
( pos + 1
, str
, stl
, line
, ltl
, posX
, posY
, column + 1
, lineNumber
, absIdx + 1
, cursorIdx
, env
, acc
)
end
| chr =>
if column < #scrollColumnStart env then
build
( pos + 1
, str
, stl
, line
, ltl
, #startX env
, posY
, column + 1
, lineNumber
, absIdx + 1
, cursorIdx
, env
, acc
)
else if column > #scrollColumnEnd env then
skipToNextLine
( pos
, str
, stl
, line
, ltl
, posY
, lineNumber
, absIdx
, cursorIdx
, env
, acc
)
else
let
val acc =
if absIdx = cursorIdx then
let
val acc = Utils.makeCursor (posX, posY, env) :: acc
in
Utils.makeCursorHighlightedChr (chr, posX, posY, env) :: acc
end
else
Utils.makeChr (chr, posX, posY, env) :: acc
in
build
( pos + 1
, str
, stl
, line
, ltl
, posX + TC.xSpace
, posY
, column + 1
, lineNumber
, absIdx + 1
, cursorIdx
, env
, acc
)
end
end

View File

@@ -0,0 +1,258 @@
structure TextBuilderWithHighlight =
struct
structure TC = TextConstants
structure Utils = TextBuilderUtils
fun isSecondLastChr (pos, str, tl) =
case tl of
[] => pos = String.size str - 2
| _ => false
fun goToFirstLineAfter
(stl, ltl, posY, lineNumber, absIdx, cursorIdx, env, acc) =
case (stl, ltl) of
(shd :: stl, lhd :: ltl) =>
if Vector.length lhd > 0 then
let
val lineOffset = Vector.sub (lhd, 0)
val strPos = lineOffset + 1
val absIdx = absIdx + strPos
val posY = posY + TC.ySpace
val lineNumber = lineNumber + 1
in
build
( strPos
, shd
, stl
, lhd
, ltl
, #startX env
, posY
, 0
, lineNumber
, absIdx
, cursorIdx
, env
, acc
)
end
else
(* keep looping until we find a linebreak *)
goToFirstLineAfter
( stl
, ltl
, posY
, lineNumber
, absIdx + String.size shd
, cursorIdx
, env
, acc
)
| (_, _) => acc
and skipToNextLine
(pos, str, stl, line, ltl, posY, lineNumber, absIdx, cursorIdx, env, acc) =
if Vector.length line = 0 then
let
(* get index of buffer after this string *)
val absIdx = absIdx - pos
val absIdx = absIdx + String.size str
in
goToFirstLineAfter
(stl, ltl, posY, lineNumber, absIdx, cursorIdx, env, acc)
end
else
(* bin search lines *)
let
val linePos = BinSearch.equalOrMore (pos + 1, line)
in
if linePos = ~1 then
(* next line is not in this node *)
let
val absIdx = absIdx - pos
val absIdx = absIdx + String.size str
in
goToFirstLineAfter
(stl, ltl, posY, lineNumber, absIdx, cursorIdx, env, acc)
end
else
let
val lineOffset = Vector.sub (line, linePos)
val newStrPos = lineOffset + 1
val absIdx = absIdx - pos + newStrPos
val posY = posY + TC.ySpace
val lineNumber = lineNumber + 1
in
build
( newStrPos
, str
, stl
, line
, ltl
, #startX env
, posY
, 0
, lineNumber
, absIdx
, cursorIdx
, env
, acc
)
end
end
and build
( pos
, str
, stl
, line
, ltl
, posX
, posY
, column
, lineNumber
, absIdx
, cursorIdx
, env: Utils.env_data
, acc
) =
if pos = String.size str then
case (stl, ltl) of
(str :: stl, line :: ltl) =>
build
( 0
, str
, stl
, line
, ltl
, posX
, posY
, column
, lineNumber
, absIdx
, cursorIdx
, env
, acc
)
| (_, _) => acc
else
case String.sub (str, pos) of
#" " =>
let
val acc =
if absIdx = cursorIdx then
Utils.makeCursor (posX, posY, env) :: acc
else
acc
val acc =
if PersistentVector.isInRange (absIdx, #searchList env) then
Utils.makeSearchHighlight (posX, posY, env) :: acc
else
acc
val posX =
if column < #scrollColumnStart env then #startX env
else posX + TC.xSpace
in
build
( pos + 1
, str
, stl
, line
, ltl
, posX
, posY
, column + 1
, lineNumber
, absIdx + 1
, cursorIdx
, env
, acc
)
end
| #"\n" =>
if lineNumber + 1 > #lastLineNumber env then
acc
else
let
val acc =
if absIdx = cursorIdx then
Utils.makeCursor (posX, posY, env) :: acc
else
acc
in
build
( pos + 1
, str
, stl
, line
, ltl
, #startX env
, posY + TC.ySpace
, 0
, lineNumber + 1
, absIdx + 1
, cursorIdx
, env
, acc
)
end
| chr =>
if column < #scrollColumnStart env then
build
( pos + 1
, str
, stl
, line
, ltl
, #startX env
, posY
, column + 1
, lineNumber
, absIdx + 1
, cursorIdx
, env
, acc
)
else if column > #scrollColumnEnd env then
skipToNextLine
( pos
, str
, stl
, line
, ltl
, posY
, lineNumber
, absIdx
, cursorIdx
, env
, acc
)
else
let
val acc =
if absIdx = cursorIdx then
Utils.makeCursorHighlightedChr (chr, posX, posY, env)
:: Utils.makeCursor (posX, posY, env) :: acc
else if PersistentVector.isInRange (absIdx, #searchList env) then
Utils.makeSearchHighlightedChr (chr, posX, posY, env)
:: Utils.makeSearchHighlight (posX, posY, env) :: acc
else
Utils.makeChr (chr, posX, posY, env) :: acc
in
build
( pos + 1
, str
, stl
, line
, ltl
, posX + TC.xSpace
, posY
, column + 1
, lineNumber
, absIdx + 1
, cursorIdx
, env
, acc
)
end
end

View File

@@ -0,0 +1,10 @@
structure TextConstants =
struct
val xSpace = 13
val xSpace3 = xSpace * 3
val ySpace = 25
val scale: Real32.real = 2.0
val textLineCount = 80
val textLineWidth = xSpace * textLineCount
end

68
shf/fcore/text-scroll.sml Normal file
View File

@@ -0,0 +1,68 @@
structure TextScroll =
struct
structure TC = TextConstants
(* calculates new scroll column from integer arguments *)
fun calculateScrollColumn
(startOfLine, cursorIdx, windowWidth, prevScrollColumn) =
let
val newColumn = cursorIdx - startOfLine
val howManyColumnsCanWeFit =
if windowWidth >= TC.textLineWidth then TC.textLineCount
else windowWidth div TC.xSpace
val howManyColumnsCanWeFit = howManyColumnsCanWeFit - 1
in
if newColumn < prevScrollColumn then
(* we are moving the cursor backwards
* so make sure that newColumn is on the left side *)
newColumn
else if newColumn > prevScrollColumn + howManyColumnsCanWeFit then
(* we are scrolling forwards *)
newColumn - howManyColumnsCanWeFit
else
(* we can display the current column without moving the scroll column
* so we do that *)
prevScrollColumn
end
(* Preqreuisite: move buffer to cursorIdx *)
fun getScrollColumn (buffer, cursorIdx, windowWidth, prevScrollColumn) =
let
val startOfLine = Cursor.vi0 (buffer, cursorIdx)
in
calculateScrollColumn
(startOfLine, cursorIdx, windowWidth, prevScrollColumn)
end
fun getScrollColumnFromString (cursorIdx, windowWidth, prevScrollColumn) =
calculateScrollColumn (0, cursorIdx, windowWidth, prevScrollColumn)
fun getStartLine (prevLineNumber, cursorLine, windowHeight, totalLines) =
if cursorLine <= (prevLineNumber + 3) then
(* cursorLine is prior to or same as prevLineNumber,
* so use cursorLine to calculate the start line we want. *)
Int.max (cursorLine - 3, 0)
else
(* cursorLine > prevLineNumber *)
let
val howManyLinesWeCanFit = windowHeight div TC.ySpace
in
if cursorLine > prevLineNumber + (howManyLinesWeCanFit - 3) then
(* cursorLine is after the visible part of the screen
* so return the mimimum line where cursorLine is visible *)
if cursorLine >= totalLines - 3 then
Int.max (totalLines - howManyLinesWeCanFit, 0)
else
cursorLine - howManyLinesWeCanFit + 3
else
prevLineNumber
end
fun getLineCentre (cursorLine, windowHeight) =
let
val howManyLinesWeCanFit = windowHeight div TC.ySpace
val startLine = cursorLine - (howManyLinesWeCanFit div 2)
in
Int.max (startLine, 0)
end
end