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 , cursorIdx = _ } = app in { startLine = startLine , buffer = buffer , searchList = searchList , searchString = searchString , mode = mode , windowWidth = windowWidth , windowHeight = windowHeight , 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 movementTests = describe "movement operations" [ test "'h' 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 "'h' 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 "'h' 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 "'h' 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 "'h' 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) , test "'l' 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 "'l' 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 "'l' does not move cursor right by one 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 "'l' 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 "'l' 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) , test "'j' 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 "'j' 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 "'j' 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 "'j' 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 "'j' 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 "'j' 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 "'j' 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 "'j' 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 "'w' 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 "'w' 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) ] val tests = concat [movementTests] val _ = run tests