From 649bcb66e143ea954d66844f86d9325050301ee1 Mon Sep 17 00:00:00 2001 From: Humza Shahid Date: Sat, 22 Mar 2025 05:18:25 +0000 Subject: [PATCH] a bit of refactoring to allow addition of more tests --- fcore/build-search-list.sml | 8 +- fcore/cursor.sml | 1389 ++++++++++++------------ fcore/text-constants.sml | 2 +- fcore/text-window.sml | 557 ++++++---- message-types/input-msg.sml | 14 +- message-types/mailbox-type.sml | 8 +- shf-tests.mlb | 3 +- test/normal-move.sml | 1825 ++++++++++++++++++++++++++++++++ test/test.sml | 1815 +------------------------------ 9 files changed, 2912 insertions(+), 2709 deletions(-) create mode 100644 test/normal-move.sml diff --git a/fcore/build-search-list.sml b/fcore/build-search-list.sml index 2cd3620..c00158e 100644 --- a/fcore/build-search-list.sml +++ b/fcore/build-search-list.sml @@ -107,11 +107,9 @@ struct end else let - val searchList = - if SearchList.exists (matchedIdx, searchList) then - searchList - else - SearchList.insert (matchedIdx, searchList) + val searchList = + if SearchList.exists (matchedIdx, searchList) then searchList + else SearchList.insert (matchedIdx, searchList) in helpFromRange ( origIdx diff --git a/fcore/cursor.sml b/fcore/cursor.sml index 663e167..aa539ff 100644 --- a/fcore/cursor.sml +++ b/fcore/cursor.sml @@ -14,7 +14,7 @@ struct * If line break is preceded by non-line break char, * then increment by 1. * *) - startAbsIdx + lineIdx + 1 + startAbsIdx + lineIdx + 1 end else helpVi0 (absIdx - String.size shd, stl, ltl) @@ -23,19 +23,18 @@ struct fun startVi0 (strPos, shd, lhd, absIdx, stl, ltl) = if String.sub (shd, strPos) = #"\n" then absIdx - else - if Vector.length lhd > 0 then - if Vector.sub (lhd, 0) < strPos then - let - val linePos = BinSearch.equalOrLess (strPos - 1, lhd) - val lineIdx = Vector.sub (lhd, linePos) - in - absIdx - strPos + lineIdx + 1 - end - else - helpVi0 (absIdx - strPos, stl, ltl) + else if Vector.length lhd > 0 then + if Vector.sub (lhd, 0) < strPos then + let + val linePos = BinSearch.equalOrLess (strPos - 1, lhd) + val lineIdx = Vector.sub (lhd, linePos) + in + absIdx - strPos + lineIdx + 1 + end else helpVi0 (absIdx - strPos, stl, ltl) + else + helpVi0 (absIdx - strPos, stl, ltl) fun vi0 (lineGap: LineGap.t, cursorIdx) = let @@ -51,8 +50,7 @@ struct in if strIdx < String.size strHd then (* strIdx is in this string *) - startVi0 - (strIdx, strHd, lnHd, cursorIdx, leftStrings, leftLines) + startVi0 (strIdx, strHd, lnHd, cursorIdx, leftStrings, leftLines) else (* strIdx must be in the strTl *) (case (strTl, lnTl) of @@ -71,9 +69,7 @@ struct end | (_, _) => cursorIdx) end - | (_, _) => - (* nowhere to go, so return cursorIdx *) - cursorIdx + | (_, _) => (* nowhere to go, so return cursorIdx *) cursorIdx end fun helpViDlr (strPos, str, absIdx, strTl, lineTl) = @@ -102,24 +98,22 @@ struct if String.sub (strHd, strIdx) <> #"\n" then (* not in double linebreak *) helpViDlr (strIdx + 1, strHd, cursorIdx + 1, strTl, lnTl) - else - (* check if we are in double linebreak *) - if strIdx - 1 >= 0 then - if String.sub (strHd, strIdx - 1) = #"\n" then - (* we are in double linebreak, so do nothing *) - cursorIdx - else - (* not in double linebreak, so iterate *) - helpViDlr (strIdx + 1, strHd, cursorIdx + 1, strTl, lnTl) + else (* check if we are in double linebreak *) if strIdx - 1 >= 0 then + if String.sub (strHd, strIdx - 1) = #"\n" then + (* we are in double linebreak, so do nothing *) + cursorIdx else - (* check if double linebreak in strTl *) - (case strTl of - nestStrHd :: _ => - if String.sub (nestStrHd, 0) = #"\n" then - cursorIdx - else - helpViDlr (strIdx + 1, strHd, cursorIdx + 1, strTl, lnTl) - | [] => cursorIdx) + (* not in double linebreak, so iterate *) + helpViDlr (strIdx + 1, strHd, cursorIdx + 1, strTl, lnTl) + else + (* check if double linebreak in strTl *) + (case strTl of + nestStrHd :: _ => + if String.sub (nestStrHd, 0) = #"\n" then + cursorIdx + else + helpViDlr (strIdx + 1, strHd, cursorIdx + 1, strTl, lnTl) + | [] => cursorIdx) else (* strIdx must be in the strTl *) (case (strTl, lnTl) of @@ -137,34 +131,30 @@ struct end | (_, _) => cursorIdx) end - | (_, _) => - (* nowhere to go, so return cursorIdx *) - cursorIdx + | (_, _) => (* nowhere to go, so return cursorIdx *) cursorIdx end fun helpViL (strIdx, hd, cursorIdx, tl) = if String.sub (hd, strIdx) = #"\n" then (* if line break, go to next idx, no matter what chr is there *) cursorIdx + 1 - else - if strIdx + 1 < String.size hd then - (* next chr is in hd *) - if String.sub (hd, strIdx + 1) = #"\n" then - (* if non-line break followed by line break, go to chr + 2 *) - cursorIdx + 2 - else - cursorIdx + 1 + else if strIdx + 1 < String.size hd then + (* next chr is in hd *) + if String.sub (hd, strIdx + 1) = #"\n" then + (* if non-line break followed by line break, go to chr + 2 *) + cursorIdx + 2 else - (case tl of - tlhd :: _ => - if String.sub (tlhd, 0) = #"\n" then - (* non-line break followed by line break *) - cursorIdx + 2 - else - (* non-line break followed by non-line break *) - cursorIdx + 1 - | [] => - cursorIdx) + cursorIdx + 1 + else + (case tl of + tlhd :: _ => + if String.sub (tlhd, 0) = #"\n" then + (* non-line break followed by line break *) + cursorIdx + 2 + else + (* non-line break followed by non-line break *) + cursorIdx + 1 + | [] => cursorIdx) (* Prerequisite: lineGap is moved to requested idx first * todo: check if we are in a \r\n pair, but this is not a priority *) @@ -184,14 +174,11 @@ struct else (* strIdx is in tl *) (case tl of - tlhd :: tltl => - let - val strIdx = strIdx - String.size hd - in - helpViL (strIdx, tlhd, cursorIdx, tltl) - end - | [] => - cursorIdx) + tlhd :: tltl => + let val strIdx = strIdx - String.size hd + in helpViL (strIdx, tlhd, cursorIdx, tltl) + end + | [] => cursorIdx) end | [] => (* return original cursorIdx if there is nothing to the right *) @@ -213,52 +200,50 @@ struct (* non-line break followed by line break * so we have to decrement by two, * skipping over line break *) - cursorIdx - 2 + cursorIdx - 2 else (* need to check two chars back in leftStrings *) (case leftStrings of - lhd :: ltl => - if String.sub (lhd, String.size lhd - 1) = #"\n" then - (* double line break *) - cursorIdx - 1 - else - (* non-line break precedes line break *) - cursorIdx - 2 - | [] => cursorIdx - 1) + lhd :: ltl => + if String.sub (lhd, String.size lhd - 1) = #"\n" then + (* double line break *) + cursorIdx - 1 + else + (* non-line break precedes line break *) + cursorIdx - 2 + | [] => cursorIdx - 1) else (* prev char is not line break so we can decrement by 1 *) cursorIdx - 1 else (* prev char is in leftStrings *) (case leftStrings of - lhd :: ltl => - if String.sub (lhd, String.size lhd - 1) = #"\n" then - (* one line break *) - if String.size lhd > 1 then - (* bounds check: prev-prev chr is in lhd *) - if String.sub (lhd, String.size lhd - 2) = #"\n" then - (* double line break *) - cursorIdx - 1 - else - (* non-line break precedes line break *) - cursorIdx - 2 - else - (* prev-prev chr is in ltl *) - (case ltl of - ltlhd :: _ => - if - String.sub (ltlhd, String.size ltlhd - 1) = #"\n" - then - (* double line break *) - cursorIdx - 1 - else - (* non-line break precedes line break *) - cursorIdx - 2 - | [] => cursorIdx - 1) - else - (* no line break *) - cursorIdx - 1 - | [] => 0) + lhd :: ltl => + if String.sub (lhd, String.size lhd - 1) = #"\n" then + (* one line break *) + if String.size lhd > 1 then + (* bounds check: prev-prev chr is in lhd *) + if String.sub (lhd, String.size lhd - 2) = #"\n" then + (* double line break *) + cursorIdx - 1 + else + (* non-line break precedes line break *) + cursorIdx - 2 + else + (* prev-prev chr is in ltl *) + (case ltl of + ltlhd :: _ => + if String.sub (ltlhd, String.size ltlhd - 1) = #"\n" then + (* double line break *) + cursorIdx - 1 + else + (* non-line break precedes line break *) + cursorIdx - 2 + | [] => cursorIdx - 1) + else + (* no line break *) + cursorIdx - 1 + | [] => 0) (* Prerequisite: lineGap is moved to requested idx first. *) fun viH (lineGap: LineGap.t, cursorIdx) = @@ -277,13 +262,11 @@ struct else (* strIdx in tl *) (case tl of - tlhd :: tltl => - let - val strIdx = strIdx - String.size hd - in - helpViH (strIdx, tlhd, cursorIdx, hd :: leftStrings) - end - | [] => cursorIdx) + tlhd :: tltl => + let val strIdx = strIdx - String.size hd + in helpViH (strIdx, tlhd, cursorIdx, hd :: leftStrings) + end + | [] => cursorIdx) end | [] => cursorIdx end @@ -313,17 +296,13 @@ struct val cursorIdx = cursorIdx - String.size lshd in if Vector.length llhd > 0 then - let - val lnIdx = - Vector.sub (llhd, Vector.length llhd - 1) - in - lnIdx + cursorIdx + let val lnIdx = Vector.sub (llhd, Vector.length llhd - 1) + in lnIdx + cursorIdx end else helpGetCursorColumnLeft (lstl, lltl, cursorIdx) end - | (_, _) => - Int.max (cursorIdx, 0) + | (_, _) => Int.max (cursorIdx, 0) fun getCursorColumn (strIdx, strHd, lnHd, leftStrings, leftLines, cursorIdx) = if Vector.length lnHd > 0 then @@ -333,14 +312,11 @@ struct if firstLn > strIdx then (* search left strings/lines *) let - val lineIdx = + val lineIdx = helpGetCursorColumnLeft (leftStrings, leftLines, cursorIdx - strIdx) in - if lineIdx = 0 then - cursorIdx - else - cursorIdx - lineIdx - 1 + if lineIdx = 0 then cursorIdx else cursorIdx - lineIdx - 1 end else if firstLn < strIdx then (* binary search in here @@ -350,34 +326,33 @@ struct val lnIdx = BinSearch.equalOrLess (strIdx, lnHd) val lnIdx = Vector.sub (lnHd, lnIdx) in - if lnIdx < strIdx then - strIdx - lnIdx - 1 - else - (* firstLn = strIdx *) - 0 + if lnIdx < strIdx then strIdx - lnIdx - 1 + else (* firstLn = strIdx *) 0 end else (* firstLn = strIdx * meaning that we are already at a line break * and that the column is 0 *) - 0 + 0 end else let - val lineIdx = - helpGetCursorColumnLeft - (leftStrings, leftLines, cursorIdx - strIdx) + val lineIdx = + helpGetCursorColumnLeft (leftStrings, leftLines, cursorIdx - strIdx) in - if lineIdx = 0 then - cursorIdx - else - cursorIdx - lineIdx - 1 + if lineIdx = 0 then cursorIdx else cursorIdx - lineIdx - 1 end fun helpViJ - ( strPos, str, absIdx - , lineColumn, preferredColumn, hasPassedLine - , strTl, lineTl, prevIsLn + ( strPos + , str + , absIdx + , lineColumn + , preferredColumn + , hasPassedLine + , strTl + , lineTl + , prevIsLn ) = if strPos = String.size str then case (strTl, lineTl) of @@ -389,13 +364,17 @@ struct * as we switch to searching in string to searcing in line vector * so perhaps not. *) helpViJ - ( 0, shd, absIdx - , lineColumn, preferredColumn, hasPassedLine - , stl, ltl, prevIsLn + ( 0 + , shd + , absIdx + , lineColumn + , preferredColumn + , hasPassedLine + , stl + , ltl + , prevIsLn ) - | (_, _) => - (* empty, so return end of previous string *) - absIdx - 1 + | (_, _) => (* empty, so return end of previous string *) absIdx - 1 else case String.sub (str, strPos) of #"\n" => @@ -409,14 +388,20 @@ struct (* line break is preceded by graphical chr * so go to graphical chr *) absIdx - 1 - else + else (* reached end of line once; * continue iterationg *) - helpViJ - ( strPos + 1, str, absIdx + 1 - , 0, preferredColumn, true - , strTl, lineTl, true - ) + helpViJ + ( strPos + 1 + , str + , absIdx + 1 + , 0 + , preferredColumn + , true + , strTl + , lineTl + , true + ) | _ => if lineColumn = preferredColumn andalso hasPassedLine then (* we're at the preferredColumn so return absIdx *) @@ -424,9 +409,15 @@ struct else (* we're not in the preferred column, so keep iterating *) helpViJ - ( strPos + 1, str, absIdx + 1 - , lineColumn + 1, preferredColumn, hasPassedLine - , strTl, lineTl, false + ( strPos + 1 + , str + , absIdx + 1 + , lineColumn + 1 + , preferredColumn + , hasPassedLine + , strTl + , lineTl + , false ) fun viJ (lineGap: LineGap.t, cursorIdx) = @@ -444,16 +435,19 @@ struct if strIdx < String.size strHd then (* strIdx is in this string *) let - val lineColumn = - getCursorColumn - ( strIdx, strHd, lnHd - , leftStrings, leftLines, cursorIdx - ) + val lineColumn = getCursorColumn + (strIdx, strHd, lnHd, leftStrings, leftLines, cursorIdx) in helpViJ - ( strIdx, strHd, cursorIdx - , lineColumn, lineColumn, false - , strTl, lnTl, false + ( strIdx + , strHd + , cursorIdx + , lineColumn + , lineColumn + , false + , strTl + , lnTl + , false ) end else @@ -464,41 +458,58 @@ struct val strIdx = strIdx - String.size strHd val leftStrings = strHd :: leftStrings val leftLines = lnHd :: leftLines - val lineColumn = - getCursorColumn - ( strIdx, nestStrHd, nestLnHd - , leftStrings, leftLines, cursorIdx - ) + val lineColumn = getCursorColumn + ( strIdx + , nestStrHd + , nestLnHd + , leftStrings + , leftLines + , cursorIdx + ) in helpViJ - ( strIdx, nestStrHd, cursorIdx - , lineColumn, lineColumn, false - , nestStrTl, nestLnTl, false + ( strIdx + , nestStrHd + , cursorIdx + , lineColumn + , lineColumn + , false + , nestStrTl + , nestLnTl + , false ) end | (_, _) => cursorIdx) end - | (_, _) => - (* nowhere to go rightward, so return cursorIdx *) - cursorIdx + | (_, _) => (* nowhere to go rightward, so return cursorIdx *) cursorIdx end fun helpViK - ( strPos, str, absIdx - , lineColumn, preferredColumn, hasPassedLine - , strTl, lineHd, lineTl + ( strPos + , str + , absIdx + , lineColumn + , preferredColumn + , hasPassedLine + , strTl + , lineHd + , lineTl ) = if strPos < 0 then case (strTl, lineTl) of (shd :: stl, lhd :: ltl) => helpViK - ( String.size shd - 1, shd, absIdx - , lineColumn, preferredColumn, hasPassedLine - , stl, lhd, ltl + ( String.size shd - 1 + , shd + , absIdx + , lineColumn + , preferredColumn + , hasPassedLine + , stl + , lhd + , ltl ) - | (_, _) => - (* empty, so return start of previous string *) - absIdx + 1 + | (_, _) => (* empty, so return start of previous string *) absIdx + 1 else case String.sub (str, strPos) of #"\n" => @@ -506,22 +517,27 @@ struct (* reached line break twice, * but line has fewer chars than preferredColumn * so go back to chr immediately after this second line break *) - absIdx + 1 - else + absIdx + 1 + else (* reached start of line once; * have to check if this is a double linebreak, - * and return idx of second linebreak if so *) + * and return idx of second linebreak if so *) let (* have to calculate column of current line * so we know which line to stop searching at *) - val lineColumn = - getCursorColumn - (strPos - 1, str, lineHd, strTl, lineTl, absIdx - 1) + val lineColumn = getCursorColumn + (strPos - 1, str, lineHd, strTl, lineTl, absIdx - 1) in helpViK - ( strPos - 1, str, absIdx - 1 - , lineColumn, preferredColumn, true - , strTl, lineHd, lineTl + ( strPos - 1 + , str + , absIdx - 1 + , lineColumn + , preferredColumn + , true + , strTl + , lineHd + , lineTl ) end | _ => @@ -535,9 +551,15 @@ struct else (* we're not in the preferred column, so keep iterating *) helpViK - ( strPos - 1, str, absIdx - 1 - , lineColumn - 1, preferredColumn, hasPassedLine - , strTl, lineHd, lineTl + ( strPos - 1 + , str + , absIdx - 1 + , lineColumn - 1 + , preferredColumn + , hasPassedLine + , strTl + , lineHd + , lineTl ) fun startViK (strIdx, shd, cursorIdx, leftStrings, lhd, leftLines) = @@ -564,15 +586,21 @@ struct case (leftStrings, leftLines) of (lshd :: lstl, llhd :: lltl) => if String.sub (lshd, String.size lshd - 1) = #"\n" then - (* \n -> \n -> \n - * so it is safe to decrement cursorIdx by 1 *) - cursorIdx - 1 + (* \n -> \n -> \n + * so it is safe to decrement cursorIdx by 1 *) + cursorIdx - 1 else - (* graphical-chr -> \n -> \n - * so go to beginning of line, - * starting from graphical-chr *) - startVi0 - (String.size lshd - 1, lshd, llhd, cursorIdx - 2, lstl, lltl) + (* graphical-chr -> \n -> \n + * so go to beginning of line, + * starting from graphical-chr *) + startVi0 + ( String.size lshd - 1 + , lshd + , llhd + , cursorIdx - 2 + , lstl + , lltl + ) | (_, _) => (* nothing to the left, so we are at start of buffer *) 0 @@ -580,8 +608,7 @@ struct (* ? -> graphical-chr -> \n * Don't expect this case to happen * but if it does, go to start of line. *) - startVi0 - (strIdx - 1, shd, lhd, cursorIdx - 1, leftStrings, leftLines) + startVi0 (strIdx - 1, shd, lhd, cursorIdx - 1, leftStrings, leftLines) else (* strIdx - 1 is in leftStrings *) case (leftStrings, leftLines) of @@ -596,38 +623,59 @@ struct else (* graphical-chr -> \n -> \n *) startVi0 - (String.size lshd - 2, lshd, llhd, cursorIdx - 2, lstl, lltl) + ( String.size lshd - 2 + , lshd + , llhd + , cursorIdx - 2 + , lstl + , lltl + ) else (* cursorIdx - 2 is in lstl *) (case (lstl, lltl) of - (stlhd :: stltl, ltlhd :: lltl) => - if String.sub (stlhd, String.size stlhd - 1) = #"\n" then - (* \n -> \n -> \n *) - cursorIdx - 1 - else - (* graphical-chr -> \n -> \n *) - startVi0 - (String.size stlhd - 1, stlhd, ltlhd, cursorIdx - 2, lstl, lltl) - | (_, _) => 0) + (stlhd :: stltl, ltlhd :: lltl) => + if String.sub (stlhd, String.size stlhd - 1) = #"\n" then + (* \n -> \n -> \n *) + cursorIdx - 1 + else + (* graphical-chr -> \n -> \n *) + startVi0 + ( String.size stlhd - 1 + , stlhd + , ltlhd + , cursorIdx - 2 + , lstl + , lltl + ) + | (_, _) => 0) else (* ? -> graphical-chr -> \n *) startVi0 - (String.size lshd - 1, lshd, llhd, cursorIdx - 1, leftStrings, leftLines) - | (_, _) => - (* leftStrings is empty so go to start of buffer *) - 0 + ( String.size lshd - 1 + , lshd + , llhd + , cursorIdx - 1 + , leftStrings + , leftLines + ) + | (_, _) => (* leftStrings is empty so go to start of buffer *) 0 else (* ? -> ? -> graphical-chr * Normal case where we call startViK. *) let - val lineColumn = - getCursorColumn - (strIdx, shd, lhd, leftStrings, leftLines, cursorIdx) + val lineColumn = getCursorColumn + (strIdx, shd, lhd, leftStrings, leftLines, cursorIdx) in helpViK - ( strIdx, shd, cursorIdx - , lineColumn, lineColumn, false - , leftStrings, lhd, leftLines + ( strIdx + , shd + , cursorIdx + , lineColumn + , lineColumn + , false + , leftStrings + , lhd + , leftLines ) end @@ -644,8 +692,7 @@ struct val strIdx = cursorIdx - bufferIdx in if strIdx < String.size shd then - startViK - (strIdx, shd, cursorIdx, leftStrings, lhd, leftLines) + startViK (strIdx, shd, cursorIdx, leftStrings, lhd, leftLines) else case (stl, ltl) of (stlhd :: stltl, ltlhd :: ltltl) => @@ -659,9 +706,7 @@ struct end | (_, _) => cursorIdx end - | (_, _) => - (* nowhere to go rightward, so return cursorIdx *) - cursorIdx + | (_, _) => (* nowhere to go rightward, so return cursorIdx *) cursorIdx end fun isNextChrSpace (strPos, str, strTl) = @@ -768,8 +813,7 @@ struct fun helpNextWord (strPos, str, absIdx, strTl, lineTl) = if strPos = String.size str then case (strTl, lineTl) of - (shd :: stl, lhd :: ltl) => - helpNextWord (0, shd, absIdx, stl, ltl) + (shd :: stl, lhd :: ltl) => helpNextWord (0, shd, absIdx, stl, ltl) | (_, _) => (* reached end of lineGap; * return last valid chr position *) @@ -779,29 +823,24 @@ struct val chr = String.sub (str, strPos) in if Char.isAlphaNum chr orelse chr = #"_" then - if isNextChrNonBlank (strPos, str, strTl) then - absIdx + 1 - else - helpNextWord (strPos + 1, str, absIdx + 1, strTl, lineTl) + if isNextChrNonBlank (strPos, str, strTl) then absIdx + 1 + else helpNextWord (strPos + 1, str, absIdx + 1, strTl, lineTl) else if Char.isSpace chr then if notIsNextChrSpace (strPos, str, strTl) then absIdx + 1 else (* nothing to do on space, except keep iterating *) helpNextWord (strPos + 1, str, absIdx + 1, strTl, lineTl) - else - (* chr is NON_BLANK. *) - if isNextChrAlphaNum (strPos, str, strTl) then - absIdx + 1 - else - helpNextWord (strPos + 1, str, absIdx + 1, strTl, lineTl) + else (* chr is NON_BLANK. *) if isNextChrAlphaNum (strPos, str, strTl) then + absIdx + 1 + else + helpNextWord (strPos + 1, str, absIdx + 1, strTl, lineTl) end fun helpNextWORD (strPos, str, absIdx, strTl, lineTl) = if strPos = String.size str then case (strTl, lineTl) of - (shd :: stl, lhd :: ltl) => - helpNextWORD (0, shd, absIdx, stl, ltl) + (shd :: stl, lhd :: ltl) => helpNextWORD (0, shd, absIdx, stl, ltl) | (_, _) => (* reached end of lineGap; * return last valid chr position *) @@ -811,11 +850,9 @@ struct val chr = String.sub (str, strPos) in if Char.isSpace chr then - if notIsNextChrSpace (strPos, str, strTl) then - absIdx + 1 - else - helpNextWORD (strPos + 1, str, absIdx + 1, strTl, lineTl) - else + if notIsNextChrSpace (strPos, str, strTl) then absIdx + 1 + else helpNextWORD (strPos + 1, str, absIdx + 1, strTl, lineTl) + else helpNextWORD (strPos + 1, str, absIdx + 1, strTl, lineTl) end @@ -865,21 +902,25 @@ struct let val chr = String.sub (str, strPos) in - if Char.isAlphaNum chr orelse chr = #"_" then - if isPrevChrSpace (strPos, str, strTl) + if + Char.isAlphaNum chr orelse chr = #"_" + then + if + isPrevChrSpace (strPos, str, strTl) orelse isPrevChrNonBlank (strPos, str, strTl) then absIdx else helpPrevWord (strPos - 1, str, absIdx - 1, strTl, lineTl) - else if Char.isSpace chr then + else if + Char.isSpace chr + then + helpPrevWord (strPos - 1, str, absIdx - 1, strTl, lineTl) + else (* is NON_BLANK *) if + isPrevChrSpace (strPos, str, strTl) + orelse isPrevChrAlphaNum (strPos, str, strTl) + then + absIdx + else helpPrevWord (strPos - 1, str, absIdx - 1, strTl, lineTl) - else - (* is NON_BLANK *) - if isPrevChrSpace (strPos, str, strTl) - orelse isPrevChrAlphaNum (strPos, str, strTl) - then - absIdx - else - helpPrevWord (strPos - 1, str, absIdx - 1, strTl, lineTl) end fun helpPrevWORD (strPos, str, absIdx, strTl, lineTl) = @@ -896,71 +937,54 @@ struct val chr = String.sub (str, strPos) in if Char.isSpace chr then - helpPrevWORD - (strPos - 1, str, absIdx - 1, strTl, lineTl) - else - if isPrevChrSpace (strPos, str, strTl) then - absIdx - else - helpPrevWORD - (strPos - 1, str, absIdx - 1, strTl, lineTl) + helpPrevWORD (strPos - 1, str, absIdx - 1, strTl, lineTl) + else if isPrevChrSpace (strPos, str, strTl) then + absIdx + else + helpPrevWORD (strPos - 1, str, absIdx - 1, strTl, lineTl) end fun helpEndOfPrevWord (strPos, str, absIdx, strTl, lineTl) = if strPos < 0 then case (strTl, lineTl) of (shd :: stl, lhd :: ltl) => - helpEndOfPrevWord - (String.size shd - 1, shd, absIdx, stl, ltl) - | (_, _) => - 0 + helpEndOfPrevWord (String.size shd - 1, shd, absIdx, stl, ltl) + | (_, _) => 0 else let val chr = String.sub (str, strPos) in if Char.isAlphaNum chr orelse chr = #"_" then - if isPrevChrNonBlank (strPos, str, strTl) then - absIdx - 1 - else - helpEndOfPrevWord - (strPos - 1, str, absIdx - 1, strTl, lineTl) - else - if Char.isSpace chr then - if isPrevChrSpace (strPos, str, strTl) then - helpEndOfPrevWord - (strPos - 1, str, absIdx - 1, strTl, lineTl) - else - absIdx - 1 - else - (* is NON_BLANK *) - if isPrevChrAlphaNum (strPos, str, strTl) then - absIdx - 1 + if isPrevChrNonBlank (strPos, str, strTl) then absIdx - 1 + else helpEndOfPrevWord (strPos - 1, str, absIdx - 1, strTl, lineTl) + else if Char.isSpace chr then + if isPrevChrSpace (strPos, str, strTl) then + helpEndOfPrevWord (strPos - 1, str, absIdx - 1, strTl, lineTl) else - helpEndOfPrevWord - (strPos - 1, str, absIdx - 1, strTl, lineTl) + absIdx - 1 + else (* is NON_BLANK *) if isPrevChrAlphaNum (strPos, str, strTl) then + absIdx - 1 + else + helpEndOfPrevWord (strPos - 1, str, absIdx - 1, strTl, lineTl) end fun helpEndOfPrevWORD (strPos, str, absIdx, strTl, lineTl) = if strPos < 0 then case (strTl, lineTl) of (shd :: stl, lhd :: ltl) => - helpEndOfPrevWORD - (String.size shd - 1, shd, absIdx, stl, ltl) - | (_, _) => - 0 + helpEndOfPrevWORD (String.size shd - 1, shd, absIdx, stl, ltl) + | (_, _) => 0 else let val chr = String.sub (str, strPos) in if Char.isSpace chr then if isPrevChrSpace (strPos, str, strTl) then - helpEndOfPrevWORD - (strPos - 1, str, absIdx - 1, strTl, lineTl) + helpEndOfPrevWORD (strPos - 1, str, absIdx - 1, strTl, lineTl) else absIdx - 1 else - helpEndOfPrevWORD - (strPos - 1, str, absIdx - 1, strTl, lineTl) + helpEndOfPrevWORD (strPos - 1, str, absIdx - 1, strTl, lineTl) end fun startPrevWord (shd, strIdx, absIdx, stl, ltl, fPrev) = @@ -975,9 +999,7 @@ struct let val prevIdx = String.size stlhd - 1 in fPrev (prevIdx, stlhd, absIdx - 1, stltl, ltltl) end - | (_, _) => - (* tl is empty; just return idx 0 *) - 0 + | (_, _) => (* tl is empty; just return idx 0 *) 0 fun toPrevWord (lineGap: LineGap.t, cursorIdx, fPrev) = let @@ -993,7 +1015,7 @@ struct in if strIdx < String.size shd then (* strIdx is in this string *) - startPrevWord + startPrevWord (shd, strIdx, cursorIdx, leftStrings, leftLines, fPrev) else (* strIdx is in tl *) @@ -1044,7 +1066,7 @@ struct val leftStrings = shd :: leftStrings val leftLines = lhd :: leftLines in - fPrev (strIdx, shd, cursorIdx, leftStrings, leftLines) + fPrev (strIdx, shd, cursorIdx, leftStrings, leftLines) end | (_, _) => cursorIdx) end @@ -1068,21 +1090,25 @@ struct let val chr = String.sub (str, strPos) in - if Char.isAlphaNum chr orelse chr = #"_" then - if isNextChrSpace (strPos, str, stl) + if + Char.isAlphaNum chr orelse chr = #"_" + then + if + isNextChrSpace (strPos, str, stl) orelse isNextChrNonBlank (strPos, str, stl) then absIdx else helpEndOfWord (strPos + 1, str, absIdx + 1, stl, ltl) - else if Char.isSpace chr then + else if + Char.isSpace chr + then + helpEndOfWord (strPos + 1, str, absIdx + 1, stl, ltl) + else (* is NON_BLANK *) if + isNextChrSpace (strPos, str, stl) + orelse isNextChrAlphaNum (strPos, str, stl) + then + absIdx + else helpEndOfWord (strPos + 1, str, absIdx + 1, stl, ltl) - else - (* is NON_BLANK *) - if isNextChrSpace (strPos, str, stl) - orelse isNextChrAlphaNum (strPos, str, stl) - then - absIdx - else - helpEndOfWord (strPos + 1, str, absIdx + 1, stl, ltl) end fun helpEndOfWORD (strPos, str, absIdx, stl, ltl) = @@ -1096,11 +1122,10 @@ struct in if Char.isSpace chr then helpEndOfWORD (strPos + 1, str, absIdx + 1, stl, ltl) - else - if isNextChrSpace (strPos, str, stl) then - absIdx - else - helpEndOfWORD (strPos + 1, str, absIdx + 1, stl, ltl) + else if isNextChrSpace (strPos, str, stl) then + absIdx + else + helpEndOfWORD (strPos + 1, str, absIdx + 1, stl, ltl) end fun startEndOfWord (shd, strIdx, absIdx, stl, ltl, fEnd) = @@ -1111,9 +1136,7 @@ struct case (stl, ltl) of (stlhd :: stltl, ltlhd :: ltltl) => fEnd (0, stlhd, absIdx + 1, stltl, ltltl) - | (_, _) => - (* tl is empty; just return absIdx *) - absIdx + | (_, _) => (* tl is empty; just return absIdx *) absIdx fun toEndOfWord (lineGap: LineGap.t, cursorIdx, fEnd) = let @@ -1128,16 +1151,15 @@ struct in if strIdx < String.size shd then (* strIdx is in this string *) - startEndOfWord - (shd, strIdx, cursorIdx, stl, ltl, fEnd) + startEndOfWord (shd, strIdx, cursorIdx, stl, ltl, fEnd) else (* strIdx is in tl *) (case (stl, ltl) of (stlhd :: stltl, ltlhd :: ltltl) => - let + let val strIdx = strIdx - String.size shd - in - startEndOfWord + in + startEndOfWord (stlhd, strIdx, cursorIdx, stltl, ltltl, fEnd) end | (_, _) => cursorIdx) @@ -1146,11 +1168,11 @@ struct end (* equivalent of vi's `e` command *) - fun endOfWord (lineGap, cursorIdx) = + fun endOfWord (lineGap, cursorIdx) = toEndOfWord (lineGap, cursorIdx, helpEndOfWord) (* equivalent of vi's `E` command *) - fun endOfWORD (lineGap, cursorIdx) = + fun endOfWORD (lineGap, cursorIdx) = toEndOfWord (lineGap, cursorIdx, helpEndOfWORD) (* vi's 'e' command takes user last char in word @@ -1163,36 +1185,29 @@ struct endOfWORD (lineGap, cursorIdx) + 1 fun helpFirstNonSpaceChr (strPos, str, absIdx, stl, ltl) = - if strPos = String.size str then - case (stl, ltl) of - (shd :: stl, lhd :: ltl) => - helpFirstNonSpaceChr - (0, shd, absIdx, stl, ltl) - | (_, _) => - absIdx - 1 - else - let - val chr = String.sub (str, strPos) - in - if chr = #" " then - helpFirstNonSpaceChr - (strPos + 1, str, absIdx + 1, stl, ltl) - else - absIdx - end + if strPos = String.size str then + case (stl, ltl) of + (shd :: stl, lhd :: ltl) => + helpFirstNonSpaceChr (0, shd, absIdx, stl, ltl) + | (_, _) => absIdx - 1 + else + let + val chr = String.sub (str, strPos) + in + if chr = #" " then + helpFirstNonSpaceChr (strPos + 1, str, absIdx + 1, stl, ltl) + else + absIdx + end fun startFirstNonSpaceChr (shd, strIdx, absIdx, stl, ltl) = - if strIdx < String.size shd then - helpFirstNonSpaceChr - (strIdx, shd, absIdx, stl, ltl) + if strIdx < String.size shd then + helpFirstNonSpaceChr (strIdx, shd, absIdx, stl, ltl) else case (stl, ltl) of (stlhd :: stltl, ltlhd :: ltltl) => - helpFirstNonSpaceChr - (0, stlhd, absIdx, stltl, ltltl) - | (_, _) => - (* tl is empty; just return absIdx *) - absIdx + helpFirstNonSpaceChr (0, stlhd, absIdx, stltl, ltltl) + | (_, _) => (* tl is empty; just return absIdx *) absIdx (* Prerequisite: * LineGap has been moved to start of line (provided with vi0). *) @@ -1208,108 +1223,101 @@ struct in if strIdx < String.size shd then (* strIdx is in this string *) - startFirstNonSpaceChr - (shd, strIdx, cursorIdx, stl, ltl) + startFirstNonSpaceChr (shd, strIdx, cursorIdx, stl, ltl) else (* strIdx is in tl *) (case (stl, ltl) of (stlhd :: stltl, ltlhd :: ltltl) => - let + let val strIdx = strIdx - String.size shd - in - startFirstNonSpaceChr + in + startFirstNonSpaceChr (stlhd, strIdx, cursorIdx, stltl, ltltl) end | (_, _) => cursorIdx) end - | (_, _) => - cursorIdx + | (_, _) => cursorIdx end fun helpToNextChr (strPos, str, absIdx, stl, ltl, origIdx, findChr) = if strPos = String.size str then case (stl, ltl) of - (shd :: stl, lhd :: ltl) => - helpToNextChr - (0, shd, absIdx, stl, ltl, origIdx, findChr) - | (_, _) => - origIdx - else - if String.sub (str, strPos) = findChr then - absIdx - else - helpToNextChr - (strPos + 1, str, absIdx + 1, stl, ltl, origIdx, findChr) + (shd :: stl, lhd :: ltl) => + helpToNextChr (0, shd, absIdx, stl, ltl, origIdx, findChr) + | (_, _) => origIdx + else if String.sub (str, strPos) = findChr then + absIdx + else + helpToNextChr (strPos + 1, str, absIdx + 1, stl, ltl, origIdx, findChr) fun startToNextChr (shd, strIdx, absIdx, stl, ltl, findChr) = (* we want to start iterating from next char after strIdx *) if strIdx - 1 < String.size shd then - helpToNextChr - (strIdx + 1, shd, absIdx + 1, stl, ltl, absIdx, findChr) + helpToNextChr (strIdx + 1, shd, absIdx + 1, stl, ltl, absIdx, findChr) else case (stl, ltl) of (stlhd :: stltl, ltlhd :: ltltl) => - helpToNextChr - (0, stlhd, absIdx + 1, stltl, ltltl, absIdx, findChr) - | (_, _) => - (* tl is empty; just return absIdx *) - absIdx + helpToNextChr (0, stlhd, absIdx + 1, stltl, ltltl, absIdx, findChr) + | (_, _) => (* tl is empty; just return absIdx *) absIdx - fun helpTillNextChr - ( strPos, str, absIdx, stl, ltl - , origIdx, findChr, lastNonLine, lastLine - ) = - if strPos = String.size str then - case (stl, ltl) of - (shd :: stl, lhd :: ltl) => - helpTillNextChr - (0, shd, absIdx, stl, ltl, origIdx, findChr, lastNonLine, lastLine) - | (_, _) => - origIdx - else - let - val chr = String.sub (str, strPos) - in - if chr = findChr then - if lastLine = lastNonLine + 1 then - (* graphical-chr -> \n - * so return graphical-chr *) - lastNonLine - else - Int.max (lastLine, lastNonLine) + fun helpTillNextChr + (strPos, str, absIdx, stl, ltl, origIdx, findChr, lastNonLine, lastLine) = + if strPos = String.size str then + case (stl, ltl) of + (shd :: stl, lhd :: ltl) => + helpTillNextChr + (0, shd, absIdx, stl, ltl, origIdx, findChr, lastNonLine, lastLine) + | (_, _) => origIdx + else + let + val chr = String.sub (str, strPos) + in + if chr = findChr then + if lastLine = lastNonLine + 1 then + (* graphical-chr -> \n + * so return graphical-chr *) + lastNonLine else - let - val lastLine = - if chr = #"\n" then - absIdx - else - lastLine - val lastNonLine = - if chr = #"\n" then - lastNonLine - else - absIdx - in - helpTillNextChr - ( strPos + 1, str, absIdx + 1, stl, ltl - , origIdx, findChr, lastNonLine, lastLine - ) - end - end + Int.max (lastLine, lastNonLine) + else + let + val lastLine = if chr = #"\n" then absIdx else lastLine + val lastNonLine = if chr = #"\n" then lastNonLine else absIdx + in + helpTillNextChr + ( strPos + 1 + , str + , absIdx + 1 + , stl + , ltl + , origIdx + , findChr + , lastNonLine + , lastLine + ) + end + end fun startTillNextChr (shd, strIdx, absIdx, stl, ltl, findChr) = (* we want to start iterating from next char after strIdx *) if strIdx + 1 < String.size shd then - helpTillNextChr + helpTillNextChr (strIdx + 1, shd, absIdx + 1, stl, ltl, absIdx, findChr, absIdx, absIdx) else case (stl, ltl) of (stlhd :: stltl, ltlhd :: ltltl) => helpTillNextChr - (0, stlhd, absIdx + 1, stltl, ltltl, absIdx, findChr, absIdx, absIdx) - | (_, _) => - (* tl is empty; just return absIdx *) - absIdx + ( 0 + , stlhd + , absIdx + 1 + , stltl + , ltltl + , absIdx + , findChr + , absIdx + , absIdx + ) + | (_, _) => (* tl is empty; just return absIdx *) absIdx fun nextChr (lineGap: LineGap.t, cursorIdx, chr, fStart) = let @@ -1323,17 +1331,13 @@ struct in if strIdx < String.size shd then (* strIdx is in this string *) - fStart - (shd, strIdx, cursorIdx, stl, ltl, chr) + fStart (shd, strIdx, cursorIdx, stl, ltl, chr) else (* strIdx is in tl *) (case (stl, ltl) of (stlhd :: stltl, ltlhd :: ltltl) => - let - val strIdx = strIdx - String.size shd - in - fStart - (stlhd, strIdx, cursorIdx, stltl, ltltl, chr) + let val strIdx = strIdx - String.size shd + in fStart (stlhd, strIdx, cursorIdx, stltl, ltltl, chr) end | (_, _) => cursorIdx) end @@ -1349,122 +1353,144 @@ struct fun helpToPrevChr (strPos, str, absIdx, stl, ltl, origIdx, findChr) = if strPos < 0 then case (stl, ltl) of - (shd :: stl, lhd :: ltl) => - helpToPrevChr + (shd :: stl, lhd :: ltl) => + helpToPrevChr (String.size shd - 1, shd, absIdx, stl, ltl, origIdx, findChr) - | (_, _) => - origIdx - else - if String.sub (str, strPos) = findChr then - absIdx - else - helpToPrevChr - (strPos - 1, str, absIdx - 1, stl, ltl, origIdx, findChr) + | (_, _) => origIdx + else if String.sub (str, strPos) = findChr then + absIdx + else + helpToPrevChr (strPos - 1, str, absIdx - 1, stl, ltl, origIdx, findChr) fun startToPrevChr (shd, strIdx, absIdx, stl, ltl, findChr) = (* we want to start iterating from Prev char after strIdx *) if strIdx > 0 then - helpToPrevChr - (strIdx - 1, shd, absIdx - 1, stl, ltl, absIdx, findChr) + helpToPrevChr (strIdx - 1, shd, absIdx - 1, stl, ltl, absIdx, findChr) else case (stl, ltl) of (stlhd :: stltl, ltlhd :: ltltl) => helpToPrevChr - (String.size stlhd - 1, stlhd, absIdx - 1, stltl, ltltl, absIdx, findChr) - | (_, _) => - (* tl is empty; return 0 for lineGap start *) - 0 + ( String.size stlhd - 1 + , stlhd + , absIdx - 1 + , stltl + , ltltl + , absIdx + , findChr + ) + | (_, _) => (* tl is empty; return 0 for lineGap start *) 0 - fun helpTillPrevChr - ( strPos, str, absIdx, stl, ltl - , origIdx, findChr - , lastNonLine, lastLine, lastValid + fun helpTillPrevChr + ( strPos + , str + , absIdx + , stl + , ltl + , origIdx + , findChr + , lastNonLine + , lastLine + , lastValid ) = - if strPos < 0 then - case (stl, ltl) of - (shd :: stl, lhd :: ltl) => - helpTillPrevChr - ( String.size shd - 1, shd, absIdx, stl, ltl - , origIdx, findChr - , lastNonLine, lastLine, lastValid - ) - | (_, _) => - origIdx - else - let - val chr = String.sub (str, strPos) - in - if chr = findChr then - if lastLine = lastNonLine then - lastNonLine - else if absIdx + 1 = lastLine then - lastValid + 1 - else - Int.min (lastLine, lastNonLine) - else - let - val lastLine = - if chr = #"\n" then - absIdx - else - lastLine - val lastNonLine = - if chr = #"\n" then - lastNonLine - else - absIdx + if strPos < 0 then + case (stl, ltl) of + (shd :: stl, lhd :: ltl) => + helpTillPrevChr + ( String.size shd - 1 + , shd + , absIdx + , stl + , ltl + , origIdx + , findChr + , lastNonLine + , lastLine + , lastValid + ) + | (_, _) => origIdx + else + let + val chr = String.sub (str, strPos) + in + if chr = findChr then + if lastLine = lastNonLine then lastNonLine + else if absIdx + 1 = lastLine then lastValid + 1 + else Int.min (lastLine, lastNonLine) + else + let + val lastLine = if chr = #"\n" then absIdx else lastLine + val lastNonLine = if chr = #"\n" then lastNonLine else absIdx - (* There is a slightly tricky edge case - * which is the reason the lastValid variable. - * Say we have a string "a\n\n\nbcd" - * and we type "Ta" with the cursor at the end. - * We want the cursor to go to the second line break - * because (graphical-chr -> \n) should not be selectable. - * However, with only lastLine and lastNonLine variables, - * we only have information about the most recent \n - * and the most recent graphical-chr. - * This means we don't have information about the case - * where a graphical-chr is followed by multiple '\n's. - * The lastValid variable keeps track of this information - * so we can use it to provide the expected behaviour. - * *) - val lastValid = - if lastLine = lastNonLine + 1 then - lastLine + 1 - else - Int.min (lastLine, lastNonLine) - in - helpTillPrevChr - ( strPos - 1, str, absIdx - 1 - , stl, ltl, origIdx, findChr - , lastNonLine, lastLine, lastValid - ) - end - end + (* There is a slightly tricky edge case + * which is the reason the lastValid variable. + * Say we have a string "a\n\n\nbcd" + * and we type "Ta" with the cursor at the end. + * We want the cursor to go to the second line break + * because (graphical-chr -> \n) should not be selectable. + * However, with only lastLine and lastNonLine variables, + * we only have information about the most recent \n + * and the most recent graphical-chr. + * This means we don't have information about the case + * where a graphical-chr is followed by multiple '\n's. + * The lastValid variable keeps track of this information + * so we can use it to provide the expected behaviour. + * *) + val lastValid = + if lastLine = lastNonLine + 1 then lastLine + 1 + else Int.min (lastLine, lastNonLine) + in + helpTillPrevChr + ( strPos - 1 + , str + , absIdx - 1 + , stl + , ltl + , origIdx + , findChr + , lastNonLine + , lastLine + , lastValid + ) + end + end fun startTillPrevChr (shd, strIdx, absIdx, stl, ltl, findChr) = (* we want to start iterating from Prev char after strIdx *) if strIdx > 0 then - helpTillPrevChr - ( strIdx - 1, shd, absIdx - 1 - , stl, ltl, absIdx, findChr - , absIdx, absIdx, absIdx + helpTillPrevChr + ( strIdx - 1 + , shd + , absIdx - 1 + , stl + , ltl + , absIdx + , findChr + , absIdx + , absIdx + , absIdx ) else case (stl, ltl) of (stlhd :: stltl, ltlhd :: ltltl) => helpTillPrevChr - ( String.size stlhd - 1, stlhd, absIdx - 1 - , stltl, ltltl, absIdx, findChr - , absIdx, absIdx, absIdx + ( String.size stlhd - 1 + , stlhd + , absIdx - 1 + , stltl + , ltltl + , absIdx + , findChr + , absIdx + , absIdx + , absIdx ) - | (_, _) => - (* tl is empty; return 0 for lineGap start *) - 0 + | (_, _) => (* tl is empty; return 0 for lineGap start *) 0 fun prevChr (lineGap: LineGap.t, cursorIdx, chr, fStart) = let - val {rightStrings, rightLines, idx = bufferIdx, leftStrings, leftLines, ...} = lineGap + val + {rightStrings, rightLines, idx = bufferIdx, leftStrings, leftLines, ...} = + lineGap in case (rightStrings, rightLines) of (shd :: stl, lhd :: ltl) => @@ -1474,17 +1500,16 @@ struct in if strIdx < String.size shd then (* strIdx is in this string *) - fStart - (shd, strIdx, cursorIdx, leftStrings, leftLines, chr) + fStart (shd, strIdx, cursorIdx, leftStrings, leftLines, chr) else (* strIdx is in tl *) (case (stl, ltl) of (stlhd :: stltl, ltlhd :: ltltl) => - let + let val strIdx = strIdx - String.size shd val leftStrings = shd :: leftStrings val leftLines = lhd :: leftLines - in + in fStart (shd, strIdx, cursorIdx, leftStrings, leftLines, chr) end @@ -1499,104 +1524,173 @@ struct fun tillPrevChr (lineGap, cursorIdx, chr) = prevChr (lineGap, cursorIdx, chr, startTillPrevChr) - fun helpMatchPairNext - ( strPos, str, absIdx, stl, origIdx - , openChr, openNum, closeChr, closeNum - ) = - if strPos = String.size str then - case stl of - hd :: tl => - helpMatchPairNext - ( 0, hd, absIdx, tl, origIdx - , openChr, openNum, closeChr, closeNum - ) - | [] => - origIdx - else - let - val chr = String.sub (str, strPos) - val openNum = if chr = openChr then openNum + 1 else openNum - val closeNum = if chr = closeChr then closeNum + 1 else closeNum - in - if openNum = closeNum then - absIdx - else - helpMatchPairNext - ( strPos + 1, str, absIdx + 1, stl, origIdx - , openChr, openNum, closeChr, closeNum - ) - end - - fun helpMatchPairPrev - ( strPos, str, absIdx, stl, origIdx - , openChr, openNum, closeChr, closeNum - ) = - if strPos < 0 then - case stl of - hd :: tl => - helpMatchPairPrev - ( String.size hd - 1, hd, absIdx, tl, origIdx - , openChr, openNum, closeChr, closeNum - ) - | [] => - origIdx + fun helpMatchPairNext + (strPos, str, absIdx, stl, origIdx, openChr, openNum, closeChr, closeNum) = + if strPos = String.size str then + case stl of + hd :: tl => + helpMatchPairNext + (0, hd, absIdx, tl, origIdx, openChr, openNum, closeChr, closeNum) + | [] => origIdx + else + let + val chr = String.sub (str, strPos) + val openNum = if chr = openChr then openNum + 1 else openNum + val closeNum = if chr = closeChr then closeNum + 1 else closeNum + in + if openNum = closeNum then + absIdx else - let - val chr = String.sub (str, strPos) - val openNum = if chr = openChr then openNum + 1 else openNum - val closeNum = if chr = closeChr then closeNum + 1 else closeNum - in - if openNum = closeNum then - absIdx - else - helpMatchPairPrev - ( strPos - 1, str, absIdx - 1, stl, origIdx - , openChr, openNum, closeChr, closeNum - ) - end + helpMatchPairNext + ( strPos + 1 + , str + , absIdx + 1 + , stl + , origIdx + , openChr + , openNum + , closeChr + , closeNum + ) + end + + fun helpMatchPairPrev + (strPos, str, absIdx, stl, origIdx, openChr, openNum, closeChr, closeNum) = + if strPos < 0 then + case stl of + hd :: tl => + helpMatchPairPrev + ( String.size hd - 1 + , hd + , absIdx + , tl + , origIdx + , openChr + , openNum + , closeChr + , closeNum + ) + | [] => origIdx + else + let + val chr = String.sub (str, strPos) + val openNum = if chr = openChr then openNum + 1 else openNum + val closeNum = if chr = closeChr then closeNum + 1 else closeNum + in + if openNum = closeNum then + absIdx + else + helpMatchPairPrev + ( strPos - 1 + , str + , absIdx - 1 + , stl + , origIdx + , openChr + , openNum + , closeChr + , closeNum + ) + end fun startMatchPair (strIdx, shd, leftStrings, rightStrings, cursorIdx) = case String.sub (shd, strIdx) of #"(" => - helpMatchPairNext - ( strIdx + 1, shd, cursorIdx + 1, rightStrings, cursorIdx - , #"(", 1, #")", 0 - ) + helpMatchPairNext + ( strIdx + 1 + , shd + , cursorIdx + 1 + , rightStrings + , cursorIdx + , #"(" + , 1 + , #")" + , 0 + ) | #")" => - helpMatchPairPrev - ( strIdx - 1, shd, cursorIdx - 1, leftStrings, cursorIdx - , #"(", 0, #")", 1 - ) + helpMatchPairPrev + ( strIdx - 1 + , shd + , cursorIdx - 1 + , leftStrings + , cursorIdx + , #"(" + , 0 + , #")" + , 1 + ) | #"[" => - helpMatchPairNext - ( strIdx + 1, shd, cursorIdx + 1, rightStrings, cursorIdx - , #"[", 1, #"]", 0 - ) + helpMatchPairNext + ( strIdx + 1 + , shd + , cursorIdx + 1 + , rightStrings + , cursorIdx + , #"[" + , 1 + , #"]" + , 0 + ) | #"]" => - helpMatchPairPrev - ( strIdx - 1, shd, cursorIdx - 1, leftStrings, cursorIdx - , #"[", 0, #"]", 1 - ) + helpMatchPairPrev + ( strIdx - 1 + , shd + , cursorIdx - 1 + , leftStrings + , cursorIdx + , #"[" + , 0 + , #"]" + , 1 + ) | #"{" => - helpMatchPairNext - ( strIdx + 1, shd, cursorIdx + 1, rightStrings, cursorIdx - , #"{", 1, #"}", 0 - ) + helpMatchPairNext + ( strIdx + 1 + , shd + , cursorIdx + 1 + , rightStrings + , cursorIdx + , #"{" + , 1 + , #"}" + , 0 + ) | #"}" => - helpMatchPairPrev - ( strIdx - 1, shd, cursorIdx - 1, leftStrings, cursorIdx - , #"{", 0, #"}", 1 - ) + helpMatchPairPrev + ( strIdx - 1 + , shd + , cursorIdx - 1 + , leftStrings + , cursorIdx + , #"{" + , 0 + , #"}" + , 1 + ) | #"<" => - helpMatchPairNext - ( strIdx + 1, shd, cursorIdx + 1, rightStrings, cursorIdx - , #"<", 1, #">", 0 - ) + helpMatchPairNext + ( strIdx + 1 + , shd + , cursorIdx + 1 + , rightStrings + , cursorIdx + , #"<" + , 1 + , #">" + , 0 + ) | #">" => - helpMatchPairPrev - ( strIdx - 1, shd, cursorIdx - 1, leftStrings, cursorIdx - , #"<", 0, #">", 1 - ) + helpMatchPairPrev + ( strIdx - 1 + , shd + , cursorIdx - 1 + , leftStrings + , cursorIdx + , #"<" + , 0 + , #">" + , 1 + ) | _ => cursorIdx fun matchPair (lineGap: LineGap.t, cursorIdx) = @@ -1616,11 +1710,12 @@ struct (* strIdx is in tl *) (case stl of stlhd :: stltl => - let + let val strIdx = strIdx - String.size shd val leftStrings = shd :: leftStrings - in - startMatchPair (strIdx, stlhd, leftStrings, stltl, cursorIdx) + in + startMatchPair + (strIdx, stlhd, leftStrings, stltl, cursorIdx) end | [] => cursorIdx) end @@ -1630,7 +1725,8 @@ struct (* Prerequisite: move lineGap to reqLine *) fun getLineStartIdx (lineGap: LineGap.t, reqLine) = let - val {rightStrings, rightLines, line = bufferLine, idx = bufferIdx, ...} = lineGap + val {rightStrings, rightLines, line = bufferLine, idx = bufferIdx, ...} = + lineGap in case (rightStrings, rightLines) of (shd :: stl, lhd :: ltl) => @@ -1652,18 +1748,20 @@ struct val {leftStrings, leftLines, ...} = lineGap in (case (leftStrings, leftLines) of - (lshd :: lstl, llhd :: lltl) => - let - val result = - startVi0 - (String.size lshd - 1, lshd, llhd, bufferIdx - 1, lstl, lltl) - in - if result = bufferIdx then - bufferIdx - 1 - else - result - end - | (_, _) => 0) + (lshd :: lstl, llhd :: lltl) => + let + val result = startVi0 + ( String.size lshd - 1 + , lshd + , llhd + , bufferIdx - 1 + , lstl + , lltl + ) + in + if result = bufferIdx then bufferIdx - 1 else result + end + | (_, _) => 0) end end @@ -1684,13 +1782,11 @@ struct else (* chr is in tl *) (case tl of - tlhd :: _ => - let - val strIdx = strIdx - String.size hd - in - String.sub (tlhd, strIdx) = #"\n" - end - | [] => true) + tlhd :: _ => + let val strIdx = strIdx - String.size hd + in String.sub (tlhd, strIdx) = #"\n" + end + | [] => true) end | [] => true end @@ -1712,14 +1808,12 @@ struct else (* prev chr if in leftStrings *) (case leftStrings of - lhd :: _ => - String.sub (lhd, String.size lhd - 1) = #"\n" - | [] => - (* cursorIdx = 0 which means we are at start of file/line *) - true) + lhd :: _ => String.sub (lhd, String.size lhd - 1) = #"\n" + | [] => + (* cursorIdx = 0 which means we are at start of file/line *) + true) end - | [] => - true + | [] => true end fun helpIsNextChrEndOfLine (strIdx, hd, tl) = @@ -1729,10 +1823,8 @@ struct else (* next chr, if it exists, is in tl *) (case tl of - tlhd :: _ => - String.sub (tlhd, 0) = #"\n" - | [] => - true) + tlhd :: _ => String.sub (tlhd, 0) = #"\n" + | [] => true) (* Prerequisite: lineGap is moved to cursorIdx *) fun isNextChrEndOfLine (lineGap: LineGap.t, cursorIdx) = @@ -1750,12 +1842,11 @@ struct else (* strIdx is in tl *) (case tl of - tlhd :: tltl => - helpIsNextChrEndOfLine - (strIdx - String.size hd, tlhd, tltl) - | [] => - (* strIdx is at end of lineGap - * which also means at end of line *) + tlhd :: tltl => + helpIsNextChrEndOfLine (strIdx - String.size hd, tlhd, tltl) + | [] => + (* strIdx is at end of lineGap + * which also means at end of line *) true) end | [] => true @@ -1768,7 +1859,7 @@ struct in (* We are trying to check if cursorIdx is within the buffer. *) case rightStrings of - _ :: _ :: _ => + _ :: _ :: _ => (* if there is a string after the hd, * we are definitely in a valid idx and should return it *) cursorIdx @@ -1789,7 +1880,7 @@ struct * else, 0 *) Int.max (bufferIdx + String.size hd - 2, 0) end - | [] => + | [] => (* if end of buffer - 2 is greater than 0, then that; * else, 0 *) Int.max (bufferIdx - 2, 0) diff --git a/fcore/text-constants.sml b/fcore/text-constants.sml index 36b207b..b305401 100644 --- a/fcore/text-constants.sml +++ b/fcore/text-constants.sml @@ -3,5 +3,5 @@ struct val xSpace = 13 val xSpace3 = xSpace * 3 val ySpace = 25 - val fontSize : Real32.real = 30.0 + val fontSize: Real32.real = 30.0 end diff --git a/fcore/text-window.sml b/fcore/text-window.sml index eb3f774..1dc157e 100644 --- a/fcore/text-window.sml +++ b/fcore/text-window.sml @@ -6,91 +6,153 @@ struct if sIdx < 0 then case stl of hd :: tl => - getStartLineBefore + getStartLineBefore (String.size hd - 1, hd, lineNum, absIdx, cursorIdx, tl) - | [] => - 0 + | [] => 0 + else if absIdx = cursorIdx then + Int.max (lineNum - 1, 0) else - if absIdx = cursorIdx then - Int.max (lineNum - 1, 0) - else - let - val chr = String.sub (shd, sIdx) - in - if chr = #"\n" then - getStartLineBefore - (sIdx - 1, shd, lineNum - 1, absIdx - 1, cursorIdx, stl) - else - getStartLineBefore - (sIdx - 1, shd, lineNum, absIdx - 1, cursorIdx, stl) + let + val chr = String.sub (shd, sIdx) + in + if chr = #"\n" then + getStartLineBefore + (sIdx - 1, shd, lineNum - 1, absIdx - 1, cursorIdx, stl) + else + getStartLineBefore + (sIdx - 1, shd, lineNum, absIdx - 1, cursorIdx, stl) end - fun getStartLineAfter - ( sIdx, shd, lineNum, absIdx, cursorIdx, stl - , maxWidth, maxHeight, curWidth, curHeight - , origLine - ) = - if sIdx = String.size shd then - case stl of - hd :: tl => - getStartLineAfter - ( 0, hd, lineNum, absIdx, cursorIdx, tl - , maxWidth, maxHeight, curWidth, curHeight + fun getStartLineAfter + ( sIdx + , shd + , lineNum + , absIdx + , cursorIdx + , stl + , maxWidth + , maxHeight + , curWidth + , curHeight + , origLine + ) = + if sIdx = String.size shd then + case stl of + hd :: tl => + getStartLineAfter + ( 0 + , hd + , lineNum + , absIdx + , cursorIdx + , tl + , maxWidth + , maxHeight + , curWidth + , curHeight + , origLine + ) + | [] => origLine + else if absIdx = cursorIdx then + origLine + else + let + val chr = String.sub (shd, sIdx) + in + if + chr = #"\n" + then + if curHeight + (ySpace * 3) >= maxHeight then + getStartLineAfter + ( sIdx + 1 + , shd + , lineNum + 1 + , absIdx + 1 + , cursorIdx + , stl + , maxWidth + , maxHeight + , 0 + , curHeight + ySpace + , origLine + 1 + ) + else + getStartLineAfter + ( sIdx + 1 + , shd + , lineNum + 1 + , absIdx + 1 + , cursorIdx + , stl + , maxWidth + , maxHeight + , 0 + , curHeight + ySpace , origLine ) - | [] => - origLine - else - if absIdx = cursorIdx then - origLine - else + else if + curWidth + xSpace <= maxWidth + then let - val chr = String.sub (shd, sIdx) + val curWidth = curWidth + xSpace in - if chr = #"\n" then - if curHeight + (ySpace * 3) >= maxHeight then - getStartLineAfter - ( sIdx + 1, shd, lineNum + 1, absIdx + 1, cursorIdx, stl - , maxWidth, maxHeight, 0, curHeight + ySpace - , origLine + 1 - ) - else - getStartLineAfter - ( sIdx + 1, shd, lineNum + 1, absIdx + 1, cursorIdx, stl - , maxWidth, maxHeight, 0, curHeight + ySpace - , origLine - ) - else - if curWidth + xSpace <= maxWidth then - let - val curWidth = curWidth + xSpace - in - getStartLineAfter - ( sIdx + 1, shd, lineNum, absIdx + 1, cursorIdx, stl - , maxWidth, maxHeight, curWidth, curHeight - , origLine - ) - end - else - (* have to create visual line break *) - if curHeight + (ySpace * 3) >= maxHeight then - getStartLineAfter - ( sIdx + 1, shd, lineNum + 1, absIdx + 1, cursorIdx, stl - , maxWidth, maxHeight, 0, curHeight + ySpace - , origLine + 1 - ) - else - getStartLineAfter - ( sIdx + 1, shd, lineNum + 1, absIdx + 1, cursorIdx, stl - , maxWidth, maxHeight, 0, curHeight + ySpace - , origLine - ) + getStartLineAfter + ( sIdx + 1 + , shd + , lineNum + , absIdx + 1 + , cursorIdx + , stl + , maxWidth + , maxHeight + , curWidth + , curHeight + , origLine + ) end + else (* have to create visual line break *) if + curHeight + (ySpace * 3) >= maxHeight + then + getStartLineAfter + ( sIdx + 1 + , shd + , lineNum + 1 + , absIdx + 1 + , cursorIdx + , stl + , maxWidth + , maxHeight + , 0 + , curHeight + ySpace + , origLine + 1 + ) + else + getStartLineAfter + ( sIdx + 1 + , shd + , lineNum + 1 + , absIdx + 1 + , cursorIdx + , stl + , maxWidth + , maxHeight + , 0 + , curHeight + ySpace + , origLine + ) + end (* Prerequisite: LineGap is moved to oldLine first. *) fun getStartLine (lineGap: LineGap.t, oldLine, cursorIdx, maxWidth, maxHeight) = let - val {rightStrings, rightLines, line = curLine, idx = curIdx, leftStrings, ...} = lineGap + val + { rightStrings + , rightLines + , line = curLine + , idx = curIdx + , leftStrings + , ... + } = lineGap in case (rightStrings, rightLines) of (rStrHd :: rStrTl, rLnHd :: _) => @@ -106,73 +168,81 @@ struct end else 0 - val absIdx = curIdx + startIdx + val absIdx = curIdx + startIdx in if cursorIdx < absIdx then (* move upwards *) - getStartLineBefore + getStartLineBefore (startIdx, rStrHd, oldLine, absIdx, cursorIdx, leftStrings) else if cursorIdx = absIdx + 1 then (* double linebreak *) - getStartLineBefore - (startIdx + 1, rStrHd, oldLine, absIdx + 1, cursorIdx, leftStrings) + getStartLineBefore + ( startIdx + 1 + , rStrHd + , oldLine + , absIdx + 1 + , cursorIdx + , leftStrings + ) else if cursorIdx > absIdx then (* possibly move downwards *) - getStartLineAfter - ( startIdx, rStrHd, oldLine, absIdx, cursorIdx, rStrTl - , maxWidth, maxHeight, 0, 0 - , oldLine - ) + getStartLineAfter + ( startIdx + , rStrHd + , oldLine + , absIdx + , cursorIdx + , rStrTl + , maxWidth + , maxHeight + , 0 + , 0 + , oldLine + ) else (* keep current line *) Int.max (oldLine - 1, 0) end - | (_, _) => - oldLine + | (_, _) => oldLine end fun helpCentreCursor (strPos, str, lineNum, stl, maxW, halfH, curW, curH) = if strPos < 0 then case stl of hd :: tl => - helpCentreCursor + helpCentreCursor (String.size hd - 1, hd, lineNum, tl, maxW, halfH, curW, curH) - | [] => - (* return 0 for start of buffer *) - 0 + | [] => (* return 0 for start of buffer *) 0 else let val chr = String.sub (str, strPos) in - if chr = #"\n" then + if + chr = #"\n" + then if curH + (ySpace * 3) >= halfH then (* if we exceeded half the screen *) lineNum else - helpCentreCursor - ( strPos - 1, str, lineNum - 1, stl, maxW, halfH - , 0, curH + ySpace - ) - else - if curW + xSpace <= maxW then - let - val curW = curW + xSpace - in - helpCentreCursor - ( strPos - 1, str, lineNum, stl, maxW, halfH - , curW + xSpace, curH - ) - end - else - (* have to create visual line break *) - if curH + (ySpace * 3) >= halfH then - (* if at limit, return current line lineNum *) - lineNum - else - helpCentreCursor - ( strPos - 1, str, lineNum - 1, stl, maxW, halfH - , 0, curH + ySpace - ) + helpCentreCursor + (strPos - 1, str, lineNum - 1, stl, maxW, halfH, 0, curH + ySpace) + else if + curW + xSpace <= maxW + then + let + val curW = curW + xSpace + in + helpCentreCursor + (strPos - 1, str, lineNum, stl, maxW, halfH, curW + xSpace, curH) + end + else (* have to create visual line break *) if + curH + (ySpace * 3) >= halfH + then + (* if at limit, return current line lineNum *) + lineNum + else + helpCentreCursor + (strPos - 1, str, lineNum - 1, stl, maxW, halfH, 0, curH + ySpace) end (* search for prev \n, and once found, @@ -181,10 +251,8 @@ struct if strPos < 0 then case stl of hd :: tl => - getCursorStartLine - (String.size hd - 1, hd, lineNum, tl, maxW, halfH) - | [] => - 0 + getCursorStartLine (String.size hd - 1, hd, lineNum, tl, maxW, halfH) + | [] => 0 else let val chr = String.sub (str, strPos) @@ -194,21 +262,15 @@ struct helpCentreCursor (strPos - 1, str, lineNum - 1, stl, maxW, halfH, xSpace, ySpace) else - getCursorStartLine - (strPos - 1, str, lineNum, stl, maxW, halfH) + getCursorStartLine (strPos - 1, str, lineNum, stl, maxW, halfH) end fun getLineNum (strIdx, lhd, bufferLine) = if Vector.length lhd = 0 then bufferLine else if Vector.length lhd = 1 then - let - val lineIdx = Vector.sub (lhd, 0) - in - if lineIdx < strIdx then - bufferLine + 1 - else - bufferLine + let val lineIdx = Vector.sub (lhd, 0) + in if lineIdx < strIdx then bufferLine + 1 else bufferLine end else let @@ -223,125 +285,164 @@ struct end (* Prerequisite: LineGap is moved to cursor *) - fun getStartLineWithCursorCentered + fun getStartLineWithCursorCentered (lineGap: LineGap.t, cursorIdx, origLine, maxWidth, maxHeight) = - let - val {rightStrings, rightLines, idx = bufferIdx, line = bufferLine, leftStrings, ...} = lineGap - in - case (rightStrings, rightLines) of - (shd :: stl, lhd :: ltl) => - let - (* convert absolute cursorIdx to idx relative to hd string *) - val strIdx = cursorIdx - bufferIdx - in - if strIdx < String.size shd then - (* strIdx is in hd *) - let - val lineNum = getLineNum (strIdx, lhd, bufferLine) - in - getCursorStartLine - (strIdx, shd, lineNum, leftStrings, maxWidth, maxHeight) - end - else - (* strIdx is in tl *) - case (stl, ltl) of - (stlhd :: stltl, ltlhd :: _) => - let - val strIdx = strIdx - String.size shd - val bufferLine = bufferLine + Vector.length lhd - val lineNum = getLineNum (strIdx, ltlhd, bufferLine) - val leftStrings = shd :: leftStrings - in - getCursorStartLine - (strIdx, stlhd, lineNum, leftStrings, maxWidth, maxHeight) - end - | (_, _) => - origLine - end - | (_, _) => - origLine - end + let + val + { rightStrings + , rightLines + , idx = bufferIdx + , line = bufferLine + , leftStrings + , ... + } = lineGap + in + case (rightStrings, rightLines) of + (shd :: stl, lhd :: ltl) => + let + (* convert absolute cursorIdx to idx relative to hd string *) + val strIdx = cursorIdx - bufferIdx + in + if strIdx < String.size shd then + (* strIdx is in hd *) + let + val lineNum = getLineNum (strIdx, lhd, bufferLine) + in + getCursorStartLine + (strIdx, shd, lineNum, leftStrings, maxWidth, maxHeight) + end + else + (* strIdx is in tl *) + case (stl, ltl) of + (stlhd :: stltl, ltlhd :: _) => + let + val strIdx = strIdx - String.size shd + val bufferLine = bufferLine + Vector.length lhd + val lineNum = getLineNum (strIdx, ltlhd, bufferLine) + val leftStrings = shd :: leftStrings + in + getCursorStartLine + (strIdx, stlhd, lineNum, leftStrings, maxWidth, maxHeight) + end + | (_, _) => origLine + end + | (_, _) => origLine + end fun helpIsCursorVisible (strPos, str, stl, absIdx, maxW, maxH, curW, curH, newCursorIdx) = - if strPos = String.size str then - case stl of - hd :: tl => - helpIsCursorVisible - (0, hd, tl, absIdx, maxW, maxH, curW, curH, newCursorIdx) - | [] => - true - else - if absIdx = newCursorIdx then - true - else - let - val chr = String.sub (str, strPos) - in - if chr = #"\n" then - if curH + (ySpace * 3) >= maxH then - false - else - helpIsCursorVisible - ( strPos + 1, str, stl, absIdx + 1 - , maxW, maxH, 0, curH + ySpace, newCursorIdx - ) - else - if curW + xSpace <= maxW then - helpIsCursorVisible - ( strPos + 1, str, stl, absIdx + 1 - , maxW, maxH, curW + xSpace, curH, newCursorIdx - ) - else - (* have to create visual line break *) - if curH + (ySpace * 3) >= maxH then - false - else - helpIsCursorVisible - ( strPos + 1, str, stl, absIdx + 1 - , maxW, maxH, 0, curH + ySpace, newCursorIdx - ) - end - - fun startIsCursorVisible - (curIdx, shd, stl, lhd, startLine, curLine, maxW, maxH, newCursorIdx) = - if startLine = curLine then - helpIsCursorVisible - (0, shd, stl, curIdx, maxW, maxH, 0, 0, newCursorIdx) - else - let - val relativeLine = (curLine + Vector.length lhd) - startLine - val lineIdx = Vector.sub (lhd, relativeLine) - val absIdx = curIdx + lineIdx - in + if strPos = String.size str then + case stl of + hd :: tl => helpIsCursorVisible - (lineIdx, shd, stl, absIdx, maxW, maxH, 0, 0, newCursorIdx) - end + (0, hd, tl, absIdx, maxW, maxH, curW, curH, newCursorIdx) + | [] => true + else if absIdx = newCursorIdx then + true + else + let + val chr = String.sub (str, strPos) + in + if + chr = #"\n" + then + if curH + (ySpace * 3) >= maxH then + false + else + helpIsCursorVisible + ( strPos + 1 + , str + , stl + , absIdx + 1 + , maxW + , maxH + , 0 + , curH + ySpace + , newCursorIdx + ) + else if + curW + xSpace <= maxW + then + helpIsCursorVisible + ( strPos + 1 + , str + , stl + , absIdx + 1 + , maxW + , maxH + , curW + xSpace + , curH + , newCursorIdx + ) + else (* have to create visual line break *) if + curH + (ySpace * 3) >= maxH + then + false + else + helpIsCursorVisible + ( strPos + 1 + , str + , stl + , absIdx + 1 + , maxW + , maxH + , 0 + , curH + ySpace + , newCursorIdx + ) + end + + fun startIsCursorVisible + (curIdx, shd, stl, lhd, startLine, curLine, maxW, maxH, newCursorIdx) = + if startLine = curLine then + helpIsCursorVisible (0, shd, stl, curIdx, maxW, maxH, 0, 0, newCursorIdx) + else + let + val relativeLine = (curLine + Vector.length lhd) - startLine + val lineIdx = Vector.sub (lhd, relativeLine) + val absIdx = curIdx + lineIdx + in + helpIsCursorVisible + (lineIdx, shd, stl, absIdx, maxW, maxH, 0, 0, newCursorIdx) + end (* Prerequisite: move LineGap.t to startLine *) fun isCursorVisible (lineGap: LineGap.t, newCursorIdx, startLine, maxW, maxH) = - let - val {rightStrings, rightLines, line = curLine, idx = curIdx, ...} = lineGap + let + val {rightStrings, rightLines, line = curLine, idx = curIdx, ...} = + lineGap in case (rightStrings, rightLines) of (shd :: stl, lhd :: ltl) => if startLine < curLine + Vector.length lhd then (* startLine in this node *) - startIsCursorVisible - ( curIdx, shd, stl, lhd, startLine, curLine - , maxW, maxH, newCursorIdx + startIsCursorVisible + ( curIdx + , shd + , stl + , lhd + , startLine + , curLine + , maxW + , maxH + , newCursorIdx ) else (* startLine is in stl *) (case (stl, ltl) of - (stlhd :: stltl, ltlhd :: ltltl) => - startIsCursorVisible - ( curIdx, stlhd, stltl, ltlhd, startLine, curLine - , maxW, maxH, newCursorIdx - ) - | (_, _) => - true) - | (_, _) => - true + (stlhd :: stltl, ltlhd :: ltltl) => + startIsCursorVisible + ( curIdx + , stlhd + , stltl + , ltlhd + , startLine + , curLine + , maxW + , maxH + , newCursorIdx + ) + | (_, _) => true) + | (_, _) => true end end diff --git a/message-types/input-msg.sml b/message-types/input-msg.sml index d453f6c..7818351 100644 --- a/message-types/input-msg.sml +++ b/message-types/input-msg.sml @@ -1,15 +1,7 @@ signature INPUT_MSG = sig - datatype t = - CHAR_EVENT of char - | KEY_ESC - | RESIZE_EVENT of int * int + datatype t = CHAR_EVENT of char | KEY_ESC | RESIZE_EVENT of int * int end -structure InputMsg :> INPUT_MSG = -struct - datatype t = - CHAR_EVENT of char - | KEY_ESC - | RESIZE_EVENT of int * int -end +structure InputMsg :> INPUT_MSG = +struct datatype t = CHAR_EVENT of char | KEY_ESC | RESIZE_EVENT of int * int end diff --git a/message-types/mailbox-type.sml b/message-types/mailbox-type.sml index 8ce864d..4bab026 100644 --- a/message-types/mailbox-type.sml +++ b/message-types/mailbox-type.sml @@ -1,11 +1,7 @@ signature MAILBOX_TYPE = sig - datatype t = - DRAW of DrawMsg.t + datatype t = DRAW of DrawMsg.t end structure MailboxType :> MAILBOX_TYPE = -struct - datatype t = - DRAW of DrawMsg.t -end +struct datatype t = DRAW of DrawMsg.t end diff --git a/shf-tests.mlb b/shf-tests.mlb index 6314134..4c3add8 100644 --- a/shf-tests.mlb +++ b/shf-tests.mlb @@ -29,7 +29,8 @@ fcore/text-window.sml fcore/finish.sml fcore/move.sml fcore/app-update.sml -(* TEST FILES *) +(* TEST FILES *) test/Railroad/src/railroad.mlb +test/normal-move.sml test/test.sml diff --git a/test/normal-move.sml b/test/normal-move.sml new file mode 100644 index 0000000..539ef8e --- /dev/null +++ b/test/normal-move.sml @@ -0,0 +1,1825 @@ +structure NormalMove = +struct + open Railroad + open Railroad.Test + open InputMsg + + local + fun helpCountLineBreaks (pos, acc, str) = + if pos < 0 then + Vector.fromList acc + else + let + val chr = String.sub (str, pos) + in + if chr = #"\n" then + (* Is this a \r\n pair? Then the position of \r should be consed. *) + if pos = 0 then + Vector.fromList (0 :: acc) + else + let + val prevChar = String.sub (str, pos - 1) + in + if prevChar = #"\r" then + helpCountLineBreaks (pos - 2, (pos - 1) :: acc, str) + else + helpCountLineBreaks (pos - 1, pos :: acc, str) + end + else if chr = #"\r" then + helpCountLineBreaks (pos - 1, pos :: acc, str) + else + helpCountLineBreaks (pos - 1, acc, str) + end + + fun countLineBreaks str = + helpCountLineBreaks (String.size str - 1, [], str) + in + (* creates a LineGap.t with valid metadata from a list of strings *) + fun fromList lst = + { idx = 0 + , line = 0 + , leftStrings = [] + , leftLines = [] + , rightStrings = lst + , rightLines = List.map countLineBreaks lst + } + end + + fun withIdx (app: AppType.app_type, idx) = + let + val + { startLine + , buffer + , searchList + , searchString + , mode + , windowWidth + , windowHeight + , msgs + , cursorIdx = _ + } = app + in + { startLine = startLine + , buffer = buffer + , searchList = searchList + , searchString = searchString + , mode = mode + , windowWidth = windowWidth + , windowHeight = windowHeight + , msgs = msgs + , cursorIdx = idx + } + end + + fun getChr (app: AppType.app_type) = + let + val {cursorIdx, buffer, ...} = app + val c = LineGap.substring (cursorIdx, 1, buffer) + in + String.sub (c, 0) + end + + val hMove = describe "move motion 'h'" + [ test "moves cursor left by one in contiguous string when cursorIdx > 0" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 1) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"h") + in + (* assert *) + Expect.isTrue (cursorIdx = 0) + end) + , test "moves cursor left by one in split string when cursorIdx > 0" + (fn _ => + let + (* arrange *) + val buffer = fromList ["hello", " world"] + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 5) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"h") + in + (* assert *) + Expect.isTrue (cursorIdx = 4) + end) + , test "does not move cursor when cursorIdx = 0" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world" + val app = AppType.init (buffer, 0, 0) + val {cursorIdx = oldCursorIdx, ...} = app + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"h") + in + (* assert *) + Expect.isTrue (oldCursorIdx = 0 andalso cursorIdx = 0) + end) + , test "moves cursor left by two in contiguous string when prev chr is \\n" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello\nworld" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 6) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"h") + in + (* assert *) + Expect.isTrue (cursorIdx = 4) + end) + , test "moves cursor left by two in split string when prev chr is \\n" + (fn _ => + let + (* arrange *) + val buffer = fromList ["hello\n", " world"] + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 6) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"h") + in + (* assert *) + Expect.isTrue (cursorIdx = 4) + end) + ] + + val lMove = describe "move motion 'l'" + [ test + "moves cursor right by one in contiguous string when cursorIdx < length" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world" + val app = AppType.init (buffer, 0, 0) + val {cursorIdx = oldCursorIdx, ...} = app + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"l") + in + (* assert *) + Expect.isTrue (oldCursorIdx = 0 andalso cursorIdx = 1) + end) + , test "moves cursor right by one in split string when cursorIdx < length" + (fn _ => + let + (* arrange *) + val buffer = fromList ["hello ", "world"] + val app = AppType.init (buffer, 0, 0) + val {cursorIdx = oldCursorIdx, ...} = app + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"l") + in + (* assert *) + Expect.isTrue (oldCursorIdx = 0 andalso cursorIdx = 1) + end) + , test "does not move cursor when cursorIdx = length" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 10) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"l") + in + (* assert *) + Expect.isTrue (cursorIdx = 10) + end) + , test + "moves right by two in contiguous string when char is followed by \\n" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello\nworld\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 4) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"l") + in + (* assert *) + Expect.isTrue (cursorIdx = 6) + end) + , test "moves right by two in split string when char is followed by \\n" + (fn _ => + let + (* arrange *) + val buffer = fromList ["hello\n", "world"] + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 4) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"l") + in + (* assert *) + Expect.isTrue (cursorIdx = 6) + end) + ] + + val jMove = describe "move motion 'j'" + [ test "moves cursur down one column in contiguous string when column = 0" + (fn _ => + let + (* arrange *) + (* "world" at end of string is intentionally misspelled as "qorld" + * since "world" appears twice and it is useful to differentiate them + * *) + val buffer = LineGap.fromString + "hello \nworld \ngoodbye \nqorld \n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app1 = AppUpdate.update (app, CHAR_EVENT #"j") + val app2 = AppUpdate.update (app1, CHAR_EVENT #"j") + val app3 = AppUpdate.update (app2, CHAR_EVENT #"j") + + (* assert *) + val c1 = getChr app1 = #"w" + val c2 = getChr app2 = #"g" + val c3 = getChr app3 = #"q" + in + Expect.isTrue (c1 andalso c2 andalso c3) + end) + , test "moves cursur down one column in split string when column = 0" + (fn _ => + let + (* arrange *) + val buffer = + fromList ["hello \n", "world \n", "goodbye \n", "qorld"] + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app1 = AppUpdate.update (app, CHAR_EVENT #"j") + val app2 = AppUpdate.update (app1, CHAR_EVENT #"j") + val app3 = AppUpdate.update (app2, CHAR_EVENT #"j") + + (* assert *) + val c1 = getChr app1 = #"w" + val c2 = getChr app2 = #"g" + val c3 = getChr app3 = #"q" + in + Expect.isTrue (c1 andalso c2 andalso c3) + end) + , test "moves cursur down one column in contiguous string when column = 1" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello \nworld \nbye \nfriends \n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 1) + + (* act *) + val app1 = AppUpdate.update (app, CHAR_EVENT #"j") + val app2 = AppUpdate.update (app1, CHAR_EVENT #"j") + val app3 = AppUpdate.update (app2, CHAR_EVENT #"j") + + (* assert *) + val c1 = getChr app1 = #"o" + val c2 = getChr app2 = #"y" + val c3 = getChr app3 = #"r" + in + Expect.isTrue (c1 andalso c2 andalso c3) + end) + , test "moves cursur down one column in split string when column = 1" + (fn _ => + let + (* arrange *) + val buffer = + fromList ["hello \n", "world ", "\nb", "ye \nfriends \n"] + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 1) + + (* act *) + val app1 = AppUpdate.update (app, CHAR_EVENT #"j") + val app2 = AppUpdate.update (app1, CHAR_EVENT #"j") + val app3 = AppUpdate.update (app2, CHAR_EVENT #"j") + + (* assert *) + val c1 = getChr app1 = #"o" + val c2 = getChr app2 = #"y" + val c3 = getChr app3 = #"r" + in + Expect.isTrue (c1 andalso c2 andalso c3) + end) + , test "moves cursur down one column in contiguous string when column = 2" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello \nworld \nbye \nfriends \n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 2) + + (* act *) + val app1 = AppUpdate.update (app, CHAR_EVENT #"j") + val app2 = AppUpdate.update (app1, CHAR_EVENT #"j") + val app3 = AppUpdate.update (app2, CHAR_EVENT #"j") + + (* assert *) + val c1 = getChr app1 = #"r" + val c2 = getChr app2 = #"e" + val c3 = getChr app3 = #"i" + in + Expect.isTrue (c1 andalso c2 andalso c3) + end) + , test "moves cursur down one column in split string when column = 2" + (fn _ => + let + (* arrange *) + val buffer = + fromList ["hello \n", "world ", "\nb", "ye \nfriends \n"] + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 2) + + (* act *) + val app1 = AppUpdate.update (app, CHAR_EVENT #"j") + val app2 = AppUpdate.update (app1, CHAR_EVENT #"j") + val app3 = AppUpdate.update (app2, CHAR_EVENT #"j") + + (* assert *) + val c1 = getChr app1 = #"r" + val c2 = getChr app2 = #"e" + val c3 = getChr app3 = #"i" + in + Expect.isTrue (c1 andalso c2 andalso c3) + end) + , test "skips '\\n' when cursor is on non-\\n and is followed by two '\\n's" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello\n\n nworld\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"j") + + (* assert *) + val isSkipped = cursorIdx = 6 + in + Expect.isTrue isSkipped + end) + , test "moves to end of buffer when on last line" (fn _ => + let + (* arrange *) + val str = "hello \nworld \ntime to go\n" + val buffer = LineGap.fromString str + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 15) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"j") + + (* assert *) + (* String.size str - 1 is a valid char position + * but we are counting String.size str - 2 as the end + * because, in Vim, saved files always end with \n + * but the last char, \n, is not visible *) + val isAtEnd = cursorIdx = String.size str - 2 + in + Expect.isTrue isAtEnd + end) + , test "leaves cursor at same idx when already at end of buffer" (fn _ => + let + (* arrange *) + val str = "hello \nworld \ntime to go\n" + val len = String.size str - 2 + val buffer = LineGap.fromString str + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, len) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"j") + + (* assert *) + (* String.size str - 1 is a valid char position + * but we are counting String.size str - 2 as the end + * because, in Vim, saved files always end with \n + * but the last char, \n, is not visible *) + val isAtEnd = cursorIdx = len + in + Expect.isTrue isAtEnd + end) + ] + + val kMove = describe "move motion 'k'" + [ test "moves cursur up one column in contiguous string when column = 0" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "0__\n4___\n9___\n14_" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 14) + + (* act *) + val app1 = AppUpdate.update (app, CHAR_EVENT #"k") + val app2 = AppUpdate.update (app1, CHAR_EVENT #"k") + val app3 = AppUpdate.update (app2, CHAR_EVENT #"k") + + (* assert *) + val c1 = getChr app1 = #"9" + val c2 = getChr app2 = #"4" + val c3 = getChr app3 = #"0" + in + Expect.isTrue (c1 andalso c2 andalso c3) + end) + , test "moves cursur up one column in split string when column = 0" (fn _ => + let + (* arrange *) + val buffer = fromList ["0__", "\n4__", "_\n9_", "__\n14_"] + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 14) + + (* act *) + val app1 = AppUpdate.update (app, CHAR_EVENT #"k") + val app2 = AppUpdate.update (app1, CHAR_EVENT #"k") + val app3 = AppUpdate.update (app2, CHAR_EVENT #"k") + + (* assert *) + val c1 = getChr app1 = #"9" + val c2 = getChr app2 = #"4" + val c3 = getChr app3 = #"0" + in + Expect.isTrue (c1 andalso c2 andalso c3) + end) + , test "moves cursur up one column in contiguous string when column = 1" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "_w_\n_5__\n_10_\n_15" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 15) + + (* act *) + val app1 = AppUpdate.update (app, CHAR_EVENT #"k") + val app2 = AppUpdate.update (app1, CHAR_EVENT #"k") + val app3 = AppUpdate.update (app2, CHAR_EVENT #"k") + + (* assert *) + val c1 = getChr app1 = #"1" + val c2 = getChr app2 = #"5" + val c3 = getChr app3 = #"w" + in + Expect.isTrue (c1 andalso c2 andalso c3) + end) + , test "moves cursur up one column in split string when column = 1" (fn _ => + let + (* arrange *) + val buffer = fromList ["_w_\n", "_5__", "\n_10_\n", "_15"] + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 15) + + (* act *) + val app1 = AppUpdate.update (app, CHAR_EVENT #"k") + val app2 = AppUpdate.update (app1, CHAR_EVENT #"k") + val app3 = AppUpdate.update (app2, CHAR_EVENT #"k") + + (* assert *) + val c1 = getChr app1 = #"1" + val c2 = getChr app2 = #"5" + val c3 = getChr app3 = #"w" + in + Expect.isTrue (c1 andalso c2 andalso c3) + end) + , test "moves cursur up one column in contiguous string when column = 2" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "__2\n__6\n__10\n__15\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 15) + + (* act *) + val app1 = AppUpdate.update (app, CHAR_EVENT #"k") + val app2 = AppUpdate.update (app1, CHAR_EVENT #"k") + val app3 = AppUpdate.update (app2, CHAR_EVENT #"k") + + (* assert *) + val c1 = getChr app1 = #"1" + val c2 = getChr app2 = #"6" + val c3 = getChr app3 = #"2" + in + Expect.isTrue (c1 andalso c2 andalso c3) + end) + , test "moves cursur up one column in split string when column = 2" (fn _ => + let + (* arrange *) + val buffer = fromList ["__", "2\n", "__6", "\n__10", "\n__1", "5\n"] + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 15) + + (* act *) + val app1 = AppUpdate.update (app, CHAR_EVENT #"k") + val app2 = AppUpdate.update (app1, CHAR_EVENT #"k") + val app3 = AppUpdate.update (app2, CHAR_EVENT #"k") + + (* assert *) + val c1 = getChr app1 = #"1" + val c2 = getChr app2 = #"6" + val c3 = getChr app3 = #"2" + in + Expect.isTrue (c1 andalso c2 andalso c3) + end) + , test + "skips '\\n' when cursor is on '\\n',\ + \prev-char is '\\n' and prev-prev char is not '\\n'" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello\n\n world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 6) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"k") + + (* assert *) + val isSkipped = cursorIdx = 0 + in + Expect.isTrue isSkipped + end) + , test "moves to 0 of buffer when on first line" (fn _ => + let + (* arrange *) + val str = "hello \nworld \ntime to go\n" + val buffer = LineGap.fromString str + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 5) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"k") + + (* assert *) + val isAtStart = cursorIdx = 0 + in + Expect.isTrue isAtStart + end) + , test "leaves cursor at same idx when already at start of buffer" (fn _ => + let + (* arrange *) + val str = "hello \nworld \ntime to go\n" + val buffer = LineGap.fromString str + val app = AppType.init (buffer, 0, 0) + (* line below does nothing; just for explicitness *) + val app = withIdx (app, 0) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"k") + + (* assert *) + (* String.size str - 1 is a valid char position + * but we are counting String.size str - 2 as the end + * because, in Vim, saved files always end with \n + * but the last char, \n, is not visible *) + val isAtStart = cursorIdx = 0 + in + Expect.isTrue isAtStart + end) + ] + + val wMove = describe "move motion 'w'" + [ test "moves cursor to start of next word in contiguous string" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"w") + + (* assert *) + val chr = String.sub ("hello world", cursorIdx) + in + Expect.isTrue (chr = #"w") + end) + , test "moves cursor to start of next word in split string" (fn _ => + let + (* arrange *) + val buffer = fromList ["hello ", "world"] + val app = AppType.init (buffer, 0, 0) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"w") + + (* assert *) + val chr = String.sub ("hello world", cursorIdx) + in + Expect.isTrue (chr = #"w") + end) + , test "moves cursor past newline when next word is after newline" (fn _ => + (* This behaviour makes behaviour different from vi, + * where "w" when a newline is in between causes cursor + * to go to newline and not next word. + * I don't personally like this behaviour from vi + * since one can just press "j" to go to the newline instead + * and it is more intuitive for the cursor to go the next word + * as usual with "w". *) + let + (* arrange *) + val buffer = LineGap.fromString "hello \n\n\n world" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"w") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #"w") + end) + , test "does not break on undescore when cursor is on alphanumeric char" + (fn _ => + (* This behaviour makes behaviour different from vi, + * where "w" when a newline is in between causes cursor + * to go to newline and not next word. + * I don't personally like this behaviour from vi + * since one can just press "j" to go to the newline instead + * and it is more intuitive for the cursor to go the next word + * as usual with "w". *) + let + (* arrange *) + val buffer = LineGap.fromString "hello_world goodbye_world" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"w") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #"g") + end) + , test "breaks on punctuation when cursor is on alphanumeric char" (fn _ => + (* vi's definition of 'word' instead of 'WORD' *) + let + (* arrange *) + val buffer = LineGap.fromString "hello, world" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"w") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #",") + end) + , test "breaks on alphanumeric char when cursor is on punctuation" (fn _ => + (* vi's definition of 'word' instead of 'WORD' *) + let + (* arrange *) + val buffer = LineGap.fromString "!#%^()hello\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"w") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #"h") + end) + , test "breaks on non-blank char when on blank char" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "0123 \t \n \t 789\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 4) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"w") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #"7") + end) + , test "moves cursor to first alphanumeric char when on punctuation" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "!!! hello\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app1 = AppUpdate.update (app, CHAR_EVENT #"w") + + (* assert *) + val startsAtExc = getChr app = #"!" + val movedToH = getChr app1 = #"h" + in + Expect.isTrue (startsAtExc andalso movedToH) + end) + , test "moves cursor to last char when cursor is on last word" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 6) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"w") + + (* assert *) + val chrIsEnd = getChr app = #"d" + in + Expect.isTrue chrIsEnd + end) + ] + + val WMove = describe "move motion 'W'" + [ test "moves cursor to start of next WORD in contiguous string" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"W") + + (* assert *) + val chr = String.sub ("hello world", cursorIdx) + in + Expect.isTrue (chr = #"w") + end) + , test "moves cursor to start of next WORD in split string" (fn _ => + let + (* arrange *) + val buffer = fromList ["hello ", "world"] + val app = AppType.init (buffer, 0, 0) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"W") + + (* assert *) + val chr = String.sub ("hello world", cursorIdx) + in + Expect.isTrue (chr = #"w") + end) + , test "moves cursor past newline when next WORD is after newline" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello \n\n\n world" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"W") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #"w") + end) + , test "does not break on punctuation when cursor is on alphanumeric char" + (fn _ => + (* vi's definition of 'WORD' instead of 'word' *) + let + (* arrange *) + val buffer = LineGap.fromString "hello, world" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"W") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #"w") + end) + , test "does not break on alphanumeric char when cursor is on punctuation" + (fn _ => + (* vi's definition of 'WORD' instead of 'word' *) + let + (* arrange *) + val buffer = LineGap.fromString "#!hello!!! world!!!\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"W") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #"w") + end) + , test "moves cursor to first non-blank when cursor is on blank" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "0123 \t \n \t 789\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 4) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"w") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #"7") + end) + , test "moves cursor to last char when cursor is on last word" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 6) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"w") + + (* assert *) + val chrIsEnd = getChr app = #"d" + in + Expect.isTrue chrIsEnd + end) + ] + + val eMove = describe "move motion 'e'" + [ test + "moves cursor to last alphanumeric char in contiguous string\ + \when in alphanumeric word and there is at least one\ + \alphanumeric char after cursor" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"e") + in + (* assert *) + Expect.isTrue (getChr app = #"o") + end) + , test + "moves cursor to last alphanumeric char in split string\ + \when in alphanumeric word and there is at least one\ + \alphanumeric char after cursor" + (fn _ => + let + (* arrange *) + val buffer = fromList ["hello ", "world", "\n"] + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"e") + in + (* assert *) + Expect.isTrue (getChr app = #"o") + end) + , test + "moves cursor to last punctuation char in contiguous string\ + \when in punctuation word and there is at least one\ + \punctuation char after cursor" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "#$%!^ world\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"e") + in + (* assert *) + Expect.isTrue (getChr app = #"^") + end) + , test + "moves cursor to last punctuation char in split string\ + \when in punctuation word and there is at least one\ + \punctuation char after cursor" + (fn _ => + let + (* arrange *) + val buffer = fromList ["#$", "%!^ ", "world", "\n"] + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"e") + in + (* assert *) + Expect.isTrue (getChr app = #"^") + end) + , test + "moves cursor to last char of next word,\ + \when cursor is on last char of current word" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 4) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"e") + in + (* assert *) + Expect.isTrue (getChr app = #"d") + end) + , test "does not break on undescore when cursor is on alphanumeric char" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello_world\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"e") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #"d") + end) + , test "breaks on undescore when cursor is on punctuation char" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "#!^*(_#!@*(\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"e") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #"(") + end) + , test "breaks on punctuation when cursor is on alphanumeric char" (fn _ => + (* vi's definition of 'word' instead of 'WORD' *) + let + (* arrange *) + val buffer = LineGap.fromString "hello, world" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"e") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #"o") + end) + , test "breaks on alphanumeric char when cursor is on punctuation" (fn _ => + (* vi's definition of 'word' instead of 'WORD' *) + let + (* arrange *) + val buffer = LineGap.fromString "!#%^()hello\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"e") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #")") + end) + , test "skips 'space' chars: '\\n', '\\t', ' '" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "0123 \t \n \t 789\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 4) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"e") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #"9") + end) + , test + "moves cursor to last char in punctuation string \ + \when cursor is on punctuation" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "!!! hello\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"e") + val newIdx = #cursorIdx app + in + (* assert *) + Expect.isTrue (newIdx = 2) + end) + , test "moves cursor to last char when cursor is on last word" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 7) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"e") + + (* assert *) + val chrIsEnd = getChr app = #"d" + in + Expect.isTrue chrIsEnd + end) + ] + + val EMove = describe "move motion 'E'" + [ test "moves cursor to last char in WORD when in contiguous string" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hel!!!lo world\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"E") + in + (* assert *) + Expect.isTrue (getChr app = #"o") + end) + , test "moves cursor to last char in WORD when in split string" (fn _ => + let + (* arrange *) + val buffer = fromList ["hel", "!!!", "lo ", "world", "\n"] + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"E") + in + (* assert *) + Expect.isTrue (getChr app = #"o") + end) + , test + "moves cursor to last char of next WORD,\ + \when cursor is on last char of current WORD" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "#ELL) !@*(ORL$\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 4) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"E") + in + (* assert *) + Expect.isTrue (getChr app = #"$") + end) + , test "does not break on punctuation when in alphanumeric char" (fn _ => + (* vi's definition of 'word' instead of 'WORD' *) + let + (* arrange *) + val buffer = LineGap.fromString "hello, world" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"E") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #",") + end) + , test "does not break on alphanumeric char when in punctuation" (fn _ => + (* vi's definition of 'word' instead of 'WORD' *) + let + (* arrange *) + val buffer = LineGap.fromString "!#%^()hello world\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"E") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #"o") + end) + , test "skips 'space' chars: '\\n', '\\t', ' '" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "0123 \t \n \t 789\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 4) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"E") + + (* assert *) + val cursorChr = getChr app + in + Expect.isTrue (cursorChr = #"9") + end) + , test "moves cursor to last char when cursor is on last word" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world!\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 7) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"E") + in + (* assert *) + Expect.isTrue (getChr app = #"!") + end) + ] + + val bMove = describe "move motion 'b'" + [ test "leaves cursor at 0 when cursor is already at 0" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"b") + in + (* assert *) + Expect.isTrue (cursorIdx = 0) + end) + , test "moves cursor to 0 when cursor > 0 and cursor is on first word" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 3) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"b") + in + (* assert *) + Expect.isTrue (cursorIdx = 0) + end) + , test + "moves cursor to first alphanumeric char after whitespace \ + \when in alphanumeric word" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString " hello world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 7) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"b") + in + (* assert *) + Expect.isTrue (getChr app = #"h") + end) + , test + "moves cursor to first alphanumeric char after punctuation \ + \when in alphanumeric word" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "!*#hello world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 7) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"b") + in + (* assert *) + Expect.isTrue (getChr app = #"h") + end) + , test + "moves cursor to first punctuation char after whitespace \ + \when in punctuation word" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString " !@#$%^&*()" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 7) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"b") + in + (* assert *) + Expect.isTrue (getChr app = #"!") + end) + , test + "moves cursor to first punctuation char after \ + \alphanumeric char when in punctuation word" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "abc!@#$%^&*()" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 7) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"b") + in + (* assert *) + Expect.isTrue (getChr app = #"!") + end) + ] + + val BMove = describe "move motion 'B'" + [ test "leaves cursor at 0 when cursor is already at 0" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"B") + in + (* assert *) + Expect.isTrue (cursorIdx = 0) + end) + , test "moves cursor to 0 when cursor > 0 and cursor is on first WORD" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 3) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"B") + in + (* assert *) + Expect.isTrue (cursorIdx = 0) + end) + , test + "moves cursor to first non-space char after whitespace \ + \when in WORD" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString " hello world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 7) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"B") + in + (* assert *) + Expect.isTrue (getChr app = #"h") + end) + , test + "moves cursor to 0 when cursor is on first letter of first WORD \ + \and there are leadinng spaces before first letter" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString " hello world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 3) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"B") + in + (* assert *) + Expect.isTrue (cursorIdx = 0) + end) + , test + "moves cursor to first char in WORD \ + \when in alphanumeric word preceded by punctuation" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "!*#hello world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 7) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"B") + in + (* assert *) + Expect.isTrue (getChr app = #"!") + end) + , test "moves cursor to first char after whitespace when in WORD" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString " !qwerty@#$%^&*()\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 17) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"B") + in + (* assert *) + Expect.isTrue (getChr app = #"!") + end) + , test + "moves cursor to first char in WORD \ + \when in punctuation word preceded by alphanumeric" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "abc!@#$%^&*()" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 11) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"B") + in + (* assert *) + Expect.isTrue (getChr app = #"a") + end) + ] + + + val zeroMove = describe "move motion '0'" + [ test "moves cursor to 0 in contiguous string when on first line" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello w7rld\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 7) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"0") + in + (* assert *) + Expect.isTrue (cursorIdx = 0) + end) + , test "moves cursor to 0 in split string when on first line" (fn _ => + let + (* arrange *) + val buffer = fromList ["hel", "lo ", "w7r", "ld\n"] + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 7) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"0") + in + (* assert *) + Expect.isTrue (cursorIdx = 0) + end) + , test "leaves cursor on 0 when cursor is already on 0" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"0") + in + (* assert *) + Expect.isTrue (cursorIdx = 0) + end) + , test "leaves cursor at same idx when cursor is on '\\n'" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world\n hello again\n" + val app = AppType.init (buffer, 0, 0) + + val app = withIdx (app, 11) + val {cursorIdx = oldIdx, ...} = app + + (* act *) + val {cursorIdx = newIdx, ...} = + AppUpdate.update (app, CHAR_EVENT #"0") + in + (* assert *) + Expect.isTrue (oldIdx = newIdx) + end) + , test + "moves cursor to first char after '\\n' in contiguous string\ + \when cursor is after first line" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello world\n#ello again\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 21) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"0") + + (* assert *) + val chr = getChr app + in + (* assert *) + Expect.isTrue (chr = #"#") + end) + , test + "moves cursor to first char after '\\n' in split string\ + \when cursor is after first line" + (fn _ => + let + (* arrange *) + val buffer = fromList + ["hel", "lo ", "wor", "ld\n", "#el", "lo ", "aga", "in\n"] + val buffer = LineGap.fromString "hello world\n#ello again\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 21) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"0") + + (* assert *) + val chr = getChr app + in + (* assert *) + Expect.isTrue (chr = #"#") + end) + ] + + val dlrMove = describe "move motion '$'" + [ test "moves cursor to char before '\\n' in contiguous string" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello wor9\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"$") + in + (* assert *) + Expect.isTrue (getChr app = #"9") + end) + , test "moves cursor to char before '\\n' in split string" (fn _ => + let + (* arrange *) + val buffer = fromList ["hel", "lo ", " wor9\n"] + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"$") + in + (* assert *) + Expect.isTrue (getChr app = #"9") + end) + , test + "leaves cursor at same idx in contiguous string\ + \when char after cursor is '\\n'" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello\n world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 11) + val oldIdx = #cursorIdx app + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"$") + val newIdx = #cursorIdx app + + val nchr = getChr app + val nchr = Char.toString nchr ^ "\n" + in + (* assert *) + Expect.isTrue (oldIdx = newIdx) + end) + , test + "leaves cursor at same idx in split string\ + \when char after cursor is '\\n'" + (fn _ => + let + (* arrange *) + val buffer = fromList ["hel", "lo\n", " wo", "rld", "\n"] + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 11) + val oldIdx = #cursorIdx app + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"$") + val newIdx = #cursorIdx app + + val nchr = getChr app + val nchr = Char.toString nchr ^ "\n" + in + (* assert *) + Expect.isTrue (oldIdx = newIdx) + end) + ] + + val caretMove = describe "move motion '^'" + [ test + "moves cursor to first non-space char in first line\ + \when first line starts with spaces\ + \and cursor is on first space" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString " 3ello world\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"^") + in + (* assert *) + Expect.isTrue (getChr app = #"3") + end) + , test + "moves cursor to first non-space char in first line\ + \when first line starts with space\ + \and cursor is after first non-space char" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString " 3ell7 world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 7) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"^") + in + (* assert *) + Expect.isTrue (getChr app = #"3") + end) + , test + "moves cursor to first non-space char\ + \when cursor is after first line\ + \and before first non-space char" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello\n world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 7) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"^") + in + (* assert *) + Expect.isTrue (getChr app = #"w") + end) + , test + "moves cursor to first non-space char\ + \when cursor is after first line\ + \and after first non-space char" + (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello\n world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 11) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"^") + in + (* assert *) + Expect.isTrue (getChr app = #"w") + end) + , test "leaves cursor in same position when on '\\n'" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hel\nlo\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 3) + val oldIdx = #cursorIdx app + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"^") + val newIdx = #cursorIdx app + in + (* assert *) + Expect.isTrue (newIdx = oldIdx) + end) + ] + + val GMove = describe "move motion 'G'" + [test "moves cursor to last char in buffer" (fn _ => + (* Note: We assume unix-style line endings: + * End of file always has \n at the end. + * We don't want cursor to ever reach this last \n + * so we say last char is the char before \n + * *) + let + (* arrange *) + val buffer = LineGap.fromString "01234\n56789\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"G") + in + (* assert *) + Expect.isTrue (getChr app = #"9") + end)] + + val percentMove = describe "move motion '%'" + [ test "moves to next ) when cursor is on (" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "(hello)\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"%") + in + (* assert *) + Expect.isTrue (getChr app = #")") + end) + , test "moves to preceding ( when cursur is on )" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "(hello)\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 6) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"%") + in + (* assert *) + Expect.isTrue (getChr app = #"(") + end) + (* testing that cursor goes to correct level of nesting *) + , test "moves to outermost ) when cursor is on outermost (" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "(((hello)))\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"%") + val newIdx = #cursorIdx app + in + (* assert *) + Expect.isTrue (newIdx = 10) + end) + , test "moves to outermost ( when cursor is on outermost )" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "(((hello)))\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 10) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"%") + val newIdx = #cursorIdx app + in + (* assert *) + Expect.isTrue (newIdx = 0) + end) + , test "moves to middle ) when cursor is on middle (" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "(((hello)))\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 1) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"%") + val newIdx = #cursorIdx app + in + (* assert *) + Expect.isTrue (newIdx = 9) + end) + , test "moves to middle ( when cursor is on middle )" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "(((hello)))\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 9) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"%") + val newIdx = #cursorIdx app + in + (* assert *) + Expect.isTrue (newIdx = 1) + end) + , test "moves to innermost ) when cursor is on innermost (" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "(((hello)))\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 2) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"%") + val newIdx = #cursorIdx app + in + (* assert *) + Expect.isTrue (newIdx = 8) + end) + , test "moves to innermost ( when cursor is on innermost )" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "(((hello)))\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 8) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"%") + val newIdx = #cursorIdx app + in + (* assert *) + Expect.isTrue (newIdx = 2) + end) + (* testing different pair combinations *) + , test "moves to next ] when cursor is on [" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "[hello]\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"%") + in + (* assert *) + Expect.isTrue (getChr app = #"]") + end) + , test "moves to preceding [ when cursur is on ]" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "[hello]\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 6) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"%") + in + (* assert *) + Expect.isTrue (getChr app = #"[") + end) + , test "moves to next } when cursor is on {" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "{hello}\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"%") + in + (* assert *) + Expect.isTrue (getChr app = #"}") + end) + , test "moves to preceding { when cursur is on }" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "{hello}\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 6) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"%") + in + (* assert *) + Expect.isTrue (getChr app = #"{") + end) + , test "moves to next > when cursor is on <" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "\n" + val app = AppType.init (buffer, 0, 0) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"%") + in + (* assert *) + Expect.isTrue (getChr app = #">") + end) + , test "moves to preceding < when cursur is on >" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 6) + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"%") + in + (* assert *) + Expect.isTrue (getChr app = #"<") + end) + (* testing that % on a non-pair character is a no-op *) + , test "does not move when cursor is on a non-pair-character" (fn _ => + let + (* arrange *) + val buffer = LineGap.fromString "hello, world\n" + val app = AppType.init (buffer, 0, 0) + val app = withIdx (app, 5) + val oldIdx = #cursorIdx app + + (* act *) + val app = AppUpdate.update (app, CHAR_EVENT #"%") + val newIdx = #cursorIdx app + in + (* assert *) + Expect.isTrue (newIdx = oldIdx) + end) + ] + + val tests = concat + [ hMove + , lMove + , jMove + , kMove + , wMove + , WMove + , eMove + , EMove + , bMove + , BMove + , zeroMove + , dlrMove + , caretMove + , GMove + , percentMove + ] +end diff --git a/test/test.sml b/test/test.sml index 81cfff6..eed821c 100644 --- a/test/test.sml +++ b/test/test.sml @@ -1,1812 +1,11 @@ -open Railroad -open Railroad.Test -open InputMsg - -local - fun helpCountLineBreaks (pos, acc, str) = - if pos < 0 then - Vector.fromList acc - else - let - val chr = String.sub (str, pos) - in - if chr = #"\n" then - (* Is this a \r\n pair? Then the position of \r should be consed. *) - if pos = 0 then - Vector.fromList (0 :: acc) - else - let - val prevChar = String.sub (str, pos - 1) - in - if prevChar = #"\r" then - helpCountLineBreaks (pos - 2, (pos - 1) :: acc, str) - else - helpCountLineBreaks (pos - 1, pos :: acc, str) - end - else if chr = #"\r" then - helpCountLineBreaks (pos - 1, pos :: acc, str) - else - helpCountLineBreaks (pos - 1, acc, str) - end - - fun countLineBreaks str = - helpCountLineBreaks (String.size str - 1, [], str) -in - (* creates a LineGap.t with valid metadata from a list of strings *) - fun fromList lst = - { idx = 0 - , line = 0 - , leftStrings = [] - , leftLines = [] - , rightStrings = lst - , rightLines = List.map countLineBreaks lst - } -end - -fun withIdx (app: AppType.app_type, idx) = +fun main () = let - val - { startLine - , buffer - , searchList - , searchString - , mode - , windowWidth - , windowHeight - , msgs - , cursorIdx = _ - } = app + open Railroad + open Railroad.Test + + val tests = NormalMove.tests in - { startLine = startLine - , buffer = buffer - , searchList = searchList - , searchString = searchString - , mode = mode - , windowWidth = windowWidth - , windowHeight = windowHeight - , msgs = msgs - , cursorIdx = idx - } + runWithConfig [Configuration.PrintPassed false] tests end -fun getChr (app: AppType.app_type) = - let - val {cursorIdx, buffer, ...} = app - val c = LineGap.substring (cursorIdx, 1, buffer) - in - String.sub (c, 0) - end - -val hMove = describe "move motion 'h'" - [ test "moves cursor left by one in contiguous string when cursorIdx > 0" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 1) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"h") - in - (* assert *) - Expect.isTrue (cursorIdx = 0) - end) - , test "moves cursor left by one in split string when cursorIdx > 0" (fn _ => - let - (* arrange *) - val buffer = fromList ["hello", " world"] - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 5) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"h") - in - (* assert *) - Expect.isTrue (cursorIdx = 4) - end) - , test "does not move cursor when cursorIdx = 0" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world" - val app = AppType.init (buffer, 0, 0) - val {cursorIdx = oldCursorIdx, ...} = app - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"h") - in - (* assert *) - Expect.isTrue (oldCursorIdx = 0 andalso cursorIdx = 0) - end) - , test "moves cursor left by two in contiguous string when prev chr is \\n" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello\nworld" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 6) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"h") - in - (* assert *) - Expect.isTrue (cursorIdx = 4) - end) - , test "moves cursor left by two in split string when prev chr is \\n" - (fn _ => - let - (* arrange *) - val buffer = fromList ["hello\n", " world"] - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 6) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"h") - in - (* assert *) - Expect.isTrue (cursorIdx = 4) - end) - ] - -val lMove = describe "move motion 'l'" - [ test - "moves cursor right by one in contiguous string when cursorIdx < length" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world" - val app = AppType.init (buffer, 0, 0) - val {cursorIdx = oldCursorIdx, ...} = app - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"l") - in - (* assert *) - Expect.isTrue (oldCursorIdx = 0 andalso cursorIdx = 1) - end) - , test "moves cursor right by one in split string when cursorIdx < length" - (fn _ => - let - (* arrange *) - val buffer = fromList ["hello ", "world"] - val app = AppType.init (buffer, 0, 0) - val {cursorIdx = oldCursorIdx, ...} = app - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"l") - in - (* assert *) - Expect.isTrue (oldCursorIdx = 0 andalso cursorIdx = 1) - end) - , test "does not move cursor when cursorIdx = length" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 10) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"l") - in - (* assert *) - Expect.isTrue (cursorIdx = 10) - end) - , test "moves right by two in contiguous string when char is followed by \\n" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello\nworld\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 4) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"l") - in - (* assert *) - Expect.isTrue (cursorIdx = 6) - end) - , test "moves right by two in split string when char is followed by \\n" - (fn _ => - let - (* arrange *) - val buffer = fromList ["hello\n", "world"] - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 4) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"l") - in - (* assert *) - Expect.isTrue (cursorIdx = 6) - end) - ] - -val jMove = describe "move motion 'j'" - [ test "moves cursur down one column in contiguous string when column = 0" - (fn _ => - let - (* arrange *) - (* "world" at end of string is intentionally misspelled as "qorld" - * since "world" appears twice and it is useful to differentiate them - * *) - val buffer = LineGap.fromString "hello \nworld \ngoodbye \nqorld \n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app1 = AppUpdate.update (app, CHAR_EVENT #"j") - val app2 = AppUpdate.update (app1, CHAR_EVENT #"j") - val app3 = AppUpdate.update (app2, CHAR_EVENT #"j") - - (* assert *) - val c1 = getChr app1 = #"w" - val c2 = getChr app2 = #"g" - val c3 = getChr app3 = #"q" - in - Expect.isTrue (c1 andalso c2 andalso c3) - end) - , test "moves cursur down one column in split string when column = 0" (fn _ => - let - (* arrange *) - val buffer = fromList ["hello \n", "world \n", "goodbye \n", "qorld"] - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app1 = AppUpdate.update (app, CHAR_EVENT #"j") - val app2 = AppUpdate.update (app1, CHAR_EVENT #"j") - val app3 = AppUpdate.update (app2, CHAR_EVENT #"j") - - (* assert *) - val c1 = getChr app1 = #"w" - val c2 = getChr app2 = #"g" - val c3 = getChr app3 = #"q" - in - Expect.isTrue (c1 andalso c2 andalso c3) - end) - , test "moves cursur down one column in contiguous string when column = 1" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello \nworld \nbye \nfriends \n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 1) - - (* act *) - val app1 = AppUpdate.update (app, CHAR_EVENT #"j") - val app2 = AppUpdate.update (app1, CHAR_EVENT #"j") - val app3 = AppUpdate.update (app2, CHAR_EVENT #"j") - - (* assert *) - val c1 = getChr app1 = #"o" - val c2 = getChr app2 = #"y" - val c3 = getChr app3 = #"r" - in - Expect.isTrue (c1 andalso c2 andalso c3) - end) - , test "moves cursur down one column in split string when column = 1" (fn _ => - let - (* arrange *) - val buffer = fromList ["hello \n", "world ", "\nb", "ye \nfriends \n"] - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 1) - - (* act *) - val app1 = AppUpdate.update (app, CHAR_EVENT #"j") - val app2 = AppUpdate.update (app1, CHAR_EVENT #"j") - val app3 = AppUpdate.update (app2, CHAR_EVENT #"j") - - (* assert *) - val c1 = getChr app1 = #"o" - val c2 = getChr app2 = #"y" - val c3 = getChr app3 = #"r" - in - Expect.isTrue (c1 andalso c2 andalso c3) - end) - , test "moves cursur down one column in contiguous string when column = 2" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello \nworld \nbye \nfriends \n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 2) - - (* act *) - val app1 = AppUpdate.update (app, CHAR_EVENT #"j") - val app2 = AppUpdate.update (app1, CHAR_EVENT #"j") - val app3 = AppUpdate.update (app2, CHAR_EVENT #"j") - - (* assert *) - val c1 = getChr app1 = #"r" - val c2 = getChr app2 = #"e" - val c3 = getChr app3 = #"i" - in - Expect.isTrue (c1 andalso c2 andalso c3) - end) - , test "moves cursur down one column in split string when column = 2" (fn _ => - let - (* arrange *) - val buffer = fromList ["hello \n", "world ", "\nb", "ye \nfriends \n"] - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 2) - - (* act *) - val app1 = AppUpdate.update (app, CHAR_EVENT #"j") - val app2 = AppUpdate.update (app1, CHAR_EVENT #"j") - val app3 = AppUpdate.update (app2, CHAR_EVENT #"j") - - (* assert *) - val c1 = getChr app1 = #"r" - val c2 = getChr app2 = #"e" - val c3 = getChr app3 = #"i" - in - Expect.isTrue (c1 andalso c2 andalso c3) - end) - , test "skips '\\n' when cursor is on non-\\n and is followed by two '\\n's" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello\n\n nworld\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"j") - - (* assert *) - val isSkipped = cursorIdx = 6 - in - Expect.isTrue isSkipped - end) - , test "moves to end of buffer when on last line" (fn _ => - let - (* arrange *) - val str = "hello \nworld \ntime to go\n" - val buffer = LineGap.fromString str - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 15) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"j") - - (* assert *) - (* String.size str - 1 is a valid char position - * but we are counting String.size str - 2 as the end - * because, in Vim, saved files always end with \n - * but the last char, \n, is not visible *) - val isAtEnd = cursorIdx = String.size str - 2 - in - Expect.isTrue isAtEnd - end) - , test "leaves cursor at same idx when already at end of buffer" (fn _ => - let - (* arrange *) - val str = "hello \nworld \ntime to go\n" - val len = String.size str - 2 - val buffer = LineGap.fromString str - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, len) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"j") - - (* assert *) - (* String.size str - 1 is a valid char position - * but we are counting String.size str - 2 as the end - * because, in Vim, saved files always end with \n - * but the last char, \n, is not visible *) - val isAtEnd = cursorIdx = len - in - Expect.isTrue isAtEnd - end) - ] - -val kMove = describe "move motion 'k'" - [ test "moves cursur up one column in contiguous string when column = 0" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "0__\n4___\n9___\n14_" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 14) - - (* act *) - val app1 = AppUpdate.update (app, CHAR_EVENT #"k") - val app2 = AppUpdate.update (app1, CHAR_EVENT #"k") - val app3 = AppUpdate.update (app2, CHAR_EVENT #"k") - - (* assert *) - val c1 = getChr app1 = #"9" - val c2 = getChr app2 = #"4" - val c3 = getChr app3 = #"0" - in - Expect.isTrue (c1 andalso c2 andalso c3) - end) - , test "moves cursur up one column in split string when column = 0" (fn _ => - let - (* arrange *) - val buffer = fromList ["0__", "\n4__", "_\n9_", "__\n14_"] - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 14) - - (* act *) - val app1 = AppUpdate.update (app, CHAR_EVENT #"k") - val app2 = AppUpdate.update (app1, CHAR_EVENT #"k") - val app3 = AppUpdate.update (app2, CHAR_EVENT #"k") - - (* assert *) - val c1 = getChr app1 = #"9" - val c2 = getChr app2 = #"4" - val c3 = getChr app3 = #"0" - in - Expect.isTrue (c1 andalso c2 andalso c3) - end) - , test "moves cursur up one column in contiguous string when column = 1" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "_w_\n_5__\n_10_\n_15" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 15) - - (* act *) - val app1 = AppUpdate.update (app, CHAR_EVENT #"k") - val app2 = AppUpdate.update (app1, CHAR_EVENT #"k") - val app3 = AppUpdate.update (app2, CHAR_EVENT #"k") - - (* assert *) - val c1 = getChr app1 = #"1" - val c2 = getChr app2 = #"5" - val c3 = getChr app3 = #"w" - in - Expect.isTrue (c1 andalso c2 andalso c3) - end) - , test "moves cursur up one column in split string when column = 1" (fn _ => - let - (* arrange *) - val buffer = fromList ["_w_\n", "_5__", "\n_10_\n", "_15"] - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 15) - - (* act *) - val app1 = AppUpdate.update (app, CHAR_EVENT #"k") - val app2 = AppUpdate.update (app1, CHAR_EVENT #"k") - val app3 = AppUpdate.update (app2, CHAR_EVENT #"k") - - (* assert *) - val c1 = getChr app1 = #"1" - val c2 = getChr app2 = #"5" - val c3 = getChr app3 = #"w" - in - Expect.isTrue (c1 andalso c2 andalso c3) - end) - , test "moves cursur up one column in contiguous string when column = 2" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "__2\n__6\n__10\n__15\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 15) - - (* act *) - val app1 = AppUpdate.update (app, CHAR_EVENT #"k") - val app2 = AppUpdate.update (app1, CHAR_EVENT #"k") - val app3 = AppUpdate.update (app2, CHAR_EVENT #"k") - - (* assert *) - val c1 = getChr app1 = #"1" - val c2 = getChr app2 = #"6" - val c3 = getChr app3 = #"2" - in - Expect.isTrue (c1 andalso c2 andalso c3) - end) - , test "moves cursur up one column in split string when column = 2" (fn _ => - let - (* arrange *) - val buffer = fromList ["__", "2\n", "__6", "\n__10", "\n__1", "5\n"] - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 15) - - (* act *) - val app1 = AppUpdate.update (app, CHAR_EVENT #"k") - val app2 = AppUpdate.update (app1, CHAR_EVENT #"k") - val app3 = AppUpdate.update (app2, CHAR_EVENT #"k") - - (* assert *) - val c1 = getChr app1 = #"1" - val c2 = getChr app2 = #"6" - val c3 = getChr app3 = #"2" - in - Expect.isTrue (c1 andalso c2 andalso c3) - end) - , test - "skips '\\n' when cursor is on '\\n',\ - \prev-char is '\\n' and prev-prev char is not '\\n'" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello\n\n world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 6) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"k") - - (* assert *) - val isSkipped = cursorIdx = 0 - in - Expect.isTrue isSkipped - end) - , test "moves to 0 of buffer when on first line" (fn _ => - let - (* arrange *) - val str = "hello \nworld \ntime to go\n" - val buffer = LineGap.fromString str - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 5) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"k") - - (* assert *) - val isAtStart = cursorIdx = 0 - in - Expect.isTrue isAtStart - end) - , test "leaves cursor at same idx when already at start of buffer" (fn _ => - let - (* arrange *) - val str = "hello \nworld \ntime to go\n" - val buffer = LineGap.fromString str - val app = AppType.init (buffer, 0, 0) - (* line below does nothing; just for explicitness *) - val app = withIdx (app, 0) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"k") - - (* assert *) - (* String.size str - 1 is a valid char position - * but we are counting String.size str - 2 as the end - * because, in Vim, saved files always end with \n - * but the last char, \n, is not visible *) - val isAtStart = cursorIdx = 0 - in - Expect.isTrue isAtStart - end) - ] - -val wMove = describe "move motion 'w'" - [ test "moves cursor to start of next word in contiguous string" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"w") - - (* assert *) - val chr = String.sub ("hello world", cursorIdx) - in - Expect.isTrue (chr = #"w") - end) - , test "moves cursor to start of next word in split string" (fn _ => - let - (* arrange *) - val buffer = fromList ["hello ", "world"] - val app = AppType.init (buffer, 0, 0) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"w") - - (* assert *) - val chr = String.sub ("hello world", cursorIdx) - in - Expect.isTrue (chr = #"w") - end) - , test "moves cursor past newline when next word is after newline" (fn _ => - (* This behaviour makes behaviour different from vi, - * where "w" when a newline is in between causes cursor - * to go to newline and not next word. - * I don't personally like this behaviour from vi - * since one can just press "j" to go to the newline instead - * and it is more intuitive for the cursor to go the next word - * as usual with "w". *) - let - (* arrange *) - val buffer = LineGap.fromString "hello \n\n\n world" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"w") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #"w") - end) - , test "does not break on undescore when cursor is on alphanumeric char" - (fn _ => - (* This behaviour makes behaviour different from vi, - * where "w" when a newline is in between causes cursor - * to go to newline and not next word. - * I don't personally like this behaviour from vi - * since one can just press "j" to go to the newline instead - * and it is more intuitive for the cursor to go the next word - * as usual with "w". *) - let - (* arrange *) - val buffer = LineGap.fromString "hello_world goodbye_world" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"w") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #"g") - end) - , test "breaks on punctuation when cursor is on alphanumeric char" (fn _ => - (* vi's definition of 'word' instead of 'WORD' *) - let - (* arrange *) - val buffer = LineGap.fromString "hello, world" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"w") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #",") - end) - , test "breaks on alphanumeric char when cursor is on punctuation" (fn _ => - (* vi's definition of 'word' instead of 'WORD' *) - let - (* arrange *) - val buffer = LineGap.fromString "!#%^()hello\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"w") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #"h") - end) - , test "breaks on non-blank char when on blank char" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "0123 \t \n \t 789\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 4) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"w") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #"7") - end) - , test "moves cursor to first alphanumeric char when on punctuation" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "!!! hello\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app1 = AppUpdate.update (app, CHAR_EVENT #"w") - - (* assert *) - val startsAtExc = getChr app = #"!" - val movedToH = getChr app1 = #"h" - in - Expect.isTrue (startsAtExc andalso movedToH) - end) - , test "moves cursor to last char when cursor is on last word" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 6) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"w") - - (* assert *) - val chrIsEnd = getChr app = #"d" - in - Expect.isTrue chrIsEnd - end) - ] - -val WMove = describe "move motion 'W'" - [ test "moves cursor to start of next WORD in contiguous string" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"W") - - (* assert *) - val chr = String.sub ("hello world", cursorIdx) - in - Expect.isTrue (chr = #"w") - end) - , test "moves cursor to start of next WORD in split string" (fn _ => - let - (* arrange *) - val buffer = fromList ["hello ", "world"] - val app = AppType.init (buffer, 0, 0) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"W") - - (* assert *) - val chr = String.sub ("hello world", cursorIdx) - in - Expect.isTrue (chr = #"w") - end) - , test "moves cursor past newline when next WORD is after newline" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello \n\n\n world" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"W") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #"w") - end) - , test "does not break on punctuation when cursor is on alphanumeric char" - (fn _ => - (* vi's definition of 'WORD' instead of 'word' *) - let - (* arrange *) - val buffer = LineGap.fromString "hello, world" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"W") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #"w") - end) - , test "does not break on alphanumeric char when cursor is on punctuation" - (fn _ => - (* vi's definition of 'WORD' instead of 'word' *) - let - (* arrange *) - val buffer = LineGap.fromString "#!hello!!! world!!!\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"W") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #"w") - end) - , test "moves cursor to first non-blank when cursor is on blank" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "0123 \t \n \t 789\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 4) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"w") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #"7") - end) - , test "moves cursor to last char when cursor is on last word" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 6) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"w") - - (* assert *) - val chrIsEnd = getChr app = #"d" - in - Expect.isTrue chrIsEnd - end) - ] - -val eMove = describe "move motion 'e'" - [ test - "moves cursor to last alphanumeric char in contiguous string\ - \when in alphanumeric word and there is at least one\ - \alphanumeric char after cursor" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"e") - in - (* assert *) - Expect.isTrue (getChr app = #"o") - end) - , test - "moves cursor to last alphanumeric char in split string\ - \when in alphanumeric word and there is at least one\ - \alphanumeric char after cursor" - (fn _ => - let - (* arrange *) - val buffer = fromList ["hello ", "world", "\n"] - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"e") - in - (* assert *) - Expect.isTrue (getChr app = #"o") - end) - , test - "moves cursor to last punctuation char in contiguous string\ - \when in punctuation word and there is at least one\ - \punctuation char after cursor" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "#$%!^ world\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"e") - in - (* assert *) - Expect.isTrue (getChr app = #"^") - end) - , test - "moves cursor to last punctuation char in split string\ - \when in punctuation word and there is at least one\ - \punctuation char after cursor" - (fn _ => - let - (* arrange *) - val buffer = fromList ["#$", "%!^ ", "world", "\n"] - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"e") - in - (* assert *) - Expect.isTrue (getChr app = #"^") - end) - , test - "moves cursor to last char of next word,\ - \when cursor is on last char of current word" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 4) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"e") - in - (* assert *) - Expect.isTrue (getChr app = #"d") - end) - , test "does not break on undescore when cursor is on alphanumeric char" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello_world\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"e") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #"d") - end) - , test "breaks on undescore when cursor is on punctuation char" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "#!^*(_#!@*(\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"e") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #"(") - end) - , test "breaks on punctuation when cursor is on alphanumeric char" (fn _ => - (* vi's definition of 'word' instead of 'WORD' *) - let - (* arrange *) - val buffer = LineGap.fromString "hello, world" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"e") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #"o") - end) - , test "breaks on alphanumeric char when cursor is on punctuation" (fn _ => - (* vi's definition of 'word' instead of 'WORD' *) - let - (* arrange *) - val buffer = LineGap.fromString "!#%^()hello\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"e") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #")") - end) - , test "skips 'space' chars: '\\n', '\\t', ' '" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "0123 \t \n \t 789\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 4) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"e") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #"9") - end) - , test - "moves cursor to last char in punctuation string \ - \when cursor is on punctuation" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "!!! hello\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"e") - val newIdx = #cursorIdx app - in - (* assert *) - Expect.isTrue (newIdx = 2) - end) - , test "moves cursor to last char when cursor is on last word" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 7) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"e") - - (* assert *) - val chrIsEnd = getChr app = #"d" - in - Expect.isTrue chrIsEnd - end) - ] - -val EMove = describe "move motion 'E'" - [ test "moves cursor to last char in WORD when in contiguous string" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hel!!!lo world\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"E") - in - (* assert *) - Expect.isTrue (getChr app = #"o") - end) - , test "moves cursor to last char in WORD when in split string" (fn _ => - let - (* arrange *) - val buffer = fromList ["hel", "!!!", "lo ", "world", "\n"] - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"E") - in - (* assert *) - Expect.isTrue (getChr app = #"o") - end) - , test - "moves cursor to last char of next WORD,\ - \when cursor is on last char of current WORD" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "#ELL) !@*(ORL$\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 4) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"E") - in - (* assert *) - Expect.isTrue (getChr app = #"$") - end) - , test "does not break on punctuation when in alphanumeric char" (fn _ => - (* vi's definition of 'word' instead of 'WORD' *) - let - (* arrange *) - val buffer = LineGap.fromString "hello, world" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"E") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #",") - end) - , test "does not break on alphanumeric char when in punctuation" (fn _ => - (* vi's definition of 'word' instead of 'WORD' *) - let - (* arrange *) - val buffer = LineGap.fromString "!#%^()hello world\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"E") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #"o") - end) - , test "skips 'space' chars: '\\n', '\\t', ' '" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "0123 \t \n \t 789\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 4) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"E") - - (* assert *) - val cursorChr = getChr app - in - Expect.isTrue (cursorChr = #"9") - end) - , test "moves cursor to last char when cursor is on last word" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world!\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 7) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"E") - in - (* assert *) - Expect.isTrue (getChr app = #"!") - end) - ] - -val bMove = describe "move motion 'b'" - [ test "leaves cursor at 0 when cursor is already at 0" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"b") - in - (* assert *) - Expect.isTrue (cursorIdx = 0) - end) - , test "moves cursor to 0 when cursor > 0 and cursor is on first word" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 3) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"b") - in - (* assert *) - Expect.isTrue (cursorIdx = 0) - end) - , test - "moves cursor to first alphanumeric char after whitespace \ - \when in alphanumeric word" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString " hello world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 7) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"b") - in - (* assert *) - Expect.isTrue (getChr app = #"h") - end) - , test - "moves cursor to first alphanumeric char after punctuation \ - \when in alphanumeric word" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "!*#hello world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 7) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"b") - in - (* assert *) - Expect.isTrue (getChr app = #"h") - end) - , test - "moves cursor to first punctuation char after whitespace \ - \when in punctuation word" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString " !@#$%^&*()" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 7) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"b") - in - (* assert *) - Expect.isTrue (getChr app = #"!") - end) - , test - "moves cursor to first punctuation char after \ - \alphanumeric char when in punctuation word" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "abc!@#$%^&*()" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 7) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"b") - in - (* assert *) - Expect.isTrue (getChr app = #"!") - end) - ] - -val BMove = describe "move motion 'B'" - [ test "leaves cursor at 0 when cursor is already at 0" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"B") - in - (* assert *) - Expect.isTrue (cursorIdx = 0) - end) - , test "moves cursor to 0 when cursor > 0 and cursor is on first WORD" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 3) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"B") - in - (* assert *) - Expect.isTrue (cursorIdx = 0) - end) - , test - "moves cursor to first non-space char after whitespace \ - \when in WORD" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString " hello world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 7) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"B") - in - (* assert *) - Expect.isTrue (getChr app = #"h") - end) - , test - "moves cursor to 0 when cursor is on first letter of first WORD \ - \and there are leadinng spaces before first letter" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString " hello world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 3) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"B") - in - (* assert *) - Expect.isTrue (cursorIdx = 0) - end) - , test - "moves cursor to first char in WORD \ - \when in alphanumeric word preceded by punctuation" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "!*#hello world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 7) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"B") - in - (* assert *) - Expect.isTrue (getChr app = #"!") - end) - , test "moves cursor to first char after whitespace when in WORD" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString " !qwerty@#$%^&*()\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 17) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"B") - in - (* assert *) - Expect.isTrue (getChr app = #"!") - end) - , test - "moves cursor to first char in WORD \ - \when in punctuation word preceded by alphanumeric" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "abc!@#$%^&*()" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 11) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"B") - in - (* assert *) - Expect.isTrue (getChr app = #"a") - end) - ] - - -val zeroMove = describe "move motion '0'" - [ test "moves cursor to 0 in contiguous string when on first line" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello w7rld\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 7) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"0") - in - (* assert *) - Expect.isTrue (cursorIdx = 0) - end) - , test "moves cursor to 0 in split string when on first line" (fn _ => - let - (* arrange *) - val buffer = fromList ["hel", "lo ", "w7r", "ld\n"] - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 7) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"0") - in - (* assert *) - Expect.isTrue (cursorIdx = 0) - end) - , test "leaves cursor on 0 when cursor is already on 0" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val {cursorIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"0") - in - (* assert *) - Expect.isTrue (cursorIdx = 0) - end) - , test "leaves cursor at same idx when cursor is on '\\n'" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world\n hello again\n" - val app = AppType.init (buffer, 0, 0) - - val app = withIdx (app, 11) - val {cursorIdx = oldIdx, ...} = app - - (* act *) - val {cursorIdx = newIdx, ...} = AppUpdate.update (app, CHAR_EVENT #"0") - in - (* assert *) - Expect.isTrue (oldIdx = newIdx) - end) - , test - "moves cursor to first char after '\\n' in contiguous string\ - \when cursor is after first line" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello world\n#ello again\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 21) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"0") - - (* assert *) - val chr = getChr app - in - (* assert *) - Expect.isTrue (chr = #"#") - end) - , test - "moves cursor to first char after '\\n' in split string\ - \when cursor is after first line" - (fn _ => - let - (* arrange *) - val buffer = fromList - ["hel", "lo ", "wor", "ld\n", "#el", "lo ", "aga", "in\n"] - val buffer = LineGap.fromString "hello world\n#ello again\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 21) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"0") - - (* assert *) - val chr = getChr app - in - (* assert *) - Expect.isTrue (chr = #"#") - end) - ] - -val dlrMove = describe "move motion '$'" - [ test "moves cursor to char before '\\n' in contiguous string" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello wor9\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"$") - in - (* assert *) - Expect.isTrue (getChr app = #"9") - end) - , test "moves cursor to char before '\\n' in split string" (fn _ => - let - (* arrange *) - val buffer = fromList ["hel", "lo ", " wor9\n"] - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"$") - in - (* assert *) - Expect.isTrue (getChr app = #"9") - end) - , test - "leaves cursor at same idx in contiguous string\ - \when char after cursor is '\\n'" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello\n world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 11) - val oldIdx = #cursorIdx app - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"$") - val newIdx = #cursorIdx app - - val nchr = getChr app - val nchr = Char.toString nchr ^ "\n" - in - (* assert *) - Expect.isTrue (oldIdx = newIdx) - end) - , test - "leaves cursor at same idx in split string\ - \when char after cursor is '\\n'" - (fn _ => - let - (* arrange *) - val buffer = fromList ["hel", "lo\n", " wo", "rld", "\n"] - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 11) - val oldIdx = #cursorIdx app - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"$") - val newIdx = #cursorIdx app - - val nchr = getChr app - val nchr = Char.toString nchr ^ "\n" - in - (* assert *) - Expect.isTrue (oldIdx = newIdx) - end) - ] - -val caretMove = describe "move motion '^'" - [ test - "moves cursor to first non-space char in first line\ - \when first line starts with spaces\ - \and cursor is on first space" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString " 3ello world\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"^") - in - (* assert *) - Expect.isTrue (getChr app = #"3") - end) - , test - "moves cursor to first non-space char in first line\ - \when first line starts with space\ - \and cursor is after first non-space char" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString " 3ell7 world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 7) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"^") - in - (* assert *) - Expect.isTrue (getChr app = #"3") - end) - , test - "moves cursor to first non-space char\ - \when cursor is after first line\ - \and before first non-space char" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello\n world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 7) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"^") - in - (* assert *) - Expect.isTrue (getChr app = #"w") - end) - , test - "moves cursor to first non-space char\ - \when cursor is after first line\ - \and after first non-space char" - (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello\n world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 11) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"^") - in - (* assert *) - Expect.isTrue (getChr app = #"w") - end) - , test "leaves cursor in same position when on '\\n'" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hel\nlo\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 3) - val oldIdx = #cursorIdx app - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"^") - val newIdx = #cursorIdx app - in - (* assert *) - Expect.isTrue (newIdx = oldIdx) - end) - ] - -val GMove = describe "move motion 'G'" - [test "moves cursor to last char in buffer" (fn _ => - (* Note: We assume unix-style line endings: - * End of file always has \n at the end. - * We don't want cursor to ever reach this last \n - * so we say last char is the char before \n - * *) - let - (* arrange *) - val buffer = LineGap.fromString "01234\n56789\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"G") - in - (* assert *) - Expect.isTrue (getChr app = #"9") - end)] - -val percentMove = describe "move motion '%'" - [ test "moves to next ) when cursor is on (" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "(hello)\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"%") - in - (* assert *) - Expect.isTrue (getChr app = #")") - end) - , test "moves to preceding ( when cursur is on )" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "(hello)\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 6) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"%") - in - (* assert *) - Expect.isTrue (getChr app = #"(") - end) - (* testing that cursor goes to correct level of nesting *) - , test "moves to outermost ) when cursor is on outermost (" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "(((hello)))\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"%") - val newIdx = #cursorIdx app - in - (* assert *) - Expect.isTrue (newIdx = 10) - end) - , test "moves to outermost ( when cursor is on outermost )" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "(((hello)))\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 10) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"%") - val newIdx = #cursorIdx app - in - (* assert *) - Expect.isTrue (newIdx = 0) - end) - , test "moves to middle ) when cursor is on middle (" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "(((hello)))\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 1) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"%") - val newIdx = #cursorIdx app - in - (* assert *) - Expect.isTrue (newIdx = 9) - end) - , test "moves to middle ( when cursor is on middle )" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "(((hello)))\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 9) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"%") - val newIdx = #cursorIdx app - in - (* assert *) - Expect.isTrue (newIdx = 1) - end) - , test "moves to innermost ) when cursor is on innermost (" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "(((hello)))\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 2) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"%") - val newIdx = #cursorIdx app - in - (* assert *) - Expect.isTrue (newIdx = 8) - end) - , test "moves to innermost ( when cursor is on innermost )" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "(((hello)))\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 8) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"%") - val newIdx = #cursorIdx app - in - (* assert *) - Expect.isTrue (newIdx = 2) - end) - (* testing different pair combinations *) - , test "moves to next ] when cursor is on [" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "[hello]\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"%") - in - (* assert *) - Expect.isTrue (getChr app = #"]") - end) - , test "moves to preceding [ when cursur is on ]" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "[hello]\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 6) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"%") - in - (* assert *) - Expect.isTrue (getChr app = #"[") - end) - , test "moves to next } when cursor is on {" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "{hello}\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"%") - in - (* assert *) - Expect.isTrue (getChr app = #"}") - end) - , test "moves to preceding { when cursur is on }" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "{hello}\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 6) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"%") - in - (* assert *) - Expect.isTrue (getChr app = #"{") - end) - , test "moves to next > when cursor is on <" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "\n" - val app = AppType.init (buffer, 0, 0) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"%") - in - (* assert *) - Expect.isTrue (getChr app = #">") - end) - , test "moves to preceding < when cursur is on >" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 6) - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"%") - in - (* assert *) - Expect.isTrue (getChr app = #"<") - end) - (* testing that % on a non-pair character is a no-op *) - , test "does not move when cursor is on a non-pair-character" (fn _ => - let - (* arrange *) - val buffer = LineGap.fromString "hello, world\n" - val app = AppType.init (buffer, 0, 0) - val app = withIdx (app, 5) - val oldIdx = #cursorIdx app - - (* act *) - val app = AppUpdate.update (app, CHAR_EVENT #"%") - val newIdx = #cursorIdx app - in - (* assert *) - Expect.isTrue (newIdx = oldIdx) - end) - ] - -val tests = concat - [ hMove - , lMove - , jMove - , kMove - , wMove - , WMove - , eMove - , EMove - , bMove - , BMove - , zeroMove - , dlrMove - , caretMove - , GMove - , percentMove - ] - -val _ = runWithConfig [Configuration.PrintPassed false] tests +val () = main ()