2024-10-06 09:32:56 +01:00
|
|
|
structure AppUpdate =
|
|
|
|
|
struct
|
|
|
|
|
open AppType
|
|
|
|
|
|
|
|
|
|
open MailboxType
|
|
|
|
|
open DrawMsg
|
|
|
|
|
open InputMsg
|
|
|
|
|
|
|
|
|
|
fun resizeText (app: app_type, newWidth, newHeight) =
|
|
|
|
|
let
|
2024-10-18 06:44:45 +01:00
|
|
|
val {buffer, windowWidth, windowHeight, startLine, cursorIdx, ...} = app
|
2024-10-06 09:32:56 +01:00
|
|
|
|
2024-10-08 08:53:43 +01:00
|
|
|
val newBuffer = LineGap.goToLine (startLine, buffer)
|
2024-10-09 11:36:05 +01:00
|
|
|
val drawMsg = TextBuilder.build
|
2024-10-09 12:43:42 +01:00
|
|
|
(startLine, cursorIdx, newBuffer, newWidth, newHeight)
|
2024-10-08 08:53:43 +01:00
|
|
|
|
|
|
|
|
val newApp = AppWith.bufferAndSize (app, newBuffer, newWidth, newHeight)
|
2024-10-06 09:32:56 +01:00
|
|
|
in
|
2024-10-09 11:36:05 +01:00
|
|
|
(newApp, drawMsg)
|
2024-10-06 09:32:56 +01:00
|
|
|
end
|
|
|
|
|
|
2024-10-26 06:01:48 +01:00
|
|
|
fun helpMoveBackward (app: app_type, buffer, cursorIdx, count, fMove) =
|
|
|
|
|
if count = 0 then
|
|
|
|
|
let
|
|
|
|
|
val {windowWidth, windowHeight, startLine, ...} = app
|
|
|
|
|
(* todo: get new startLine if cursor has moved out of screen *)
|
|
|
|
|
|
|
|
|
|
(* move LineGap to first line displayed on screen, and build new text *)
|
|
|
|
|
val buffer = LineGap.goToLine (startLine, buffer)
|
|
|
|
|
val drawMsg = TextBuilder.build
|
|
|
|
|
(startLine, cursorIdx, buffer, windowWidth, windowHeight)
|
|
|
|
|
|
|
|
|
|
val mode = NORMAL_MODE ""
|
|
|
|
|
val newApp = AppWith.bufferAndCursorIdx (app, buffer, cursorIdx, mode)
|
|
|
|
|
in
|
|
|
|
|
(newApp, drawMsg)
|
|
|
|
|
end
|
|
|
|
|
else
|
2024-10-17 02:14:09 +01:00
|
|
|
(* move LineGap to cursorIdx, which is necessary for finding newCursorIdx *)
|
2024-10-26 06:01:48 +01:00
|
|
|
let
|
|
|
|
|
val buffer = LineGap.goToIdx (cursorIdx, buffer)
|
|
|
|
|
val cursorIdx = fMove (buffer, cursorIdx)
|
|
|
|
|
in
|
|
|
|
|
helpMoveBackward (app, buffer, cursorIdx, count - 1, fMove)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
fun moveBackward (app: app_type, count, fMove) =
|
|
|
|
|
let val {cursorIdx, buffer, ...} = app
|
|
|
|
|
in helpMoveBackward (app, buffer, cursorIdx, count, fMove)
|
2024-10-25 21:01:48 +01:00
|
|
|
end
|
|
|
|
|
|
2024-10-26 06:01:48 +01:00
|
|
|
fun helpMoveForwards (app: app_type, buffer, cursorIdx, count, fMove) =
|
|
|
|
|
if count = 0 then
|
|
|
|
|
let
|
|
|
|
|
val {windowWidth, windowHeight, startLine, ...} = app
|
|
|
|
|
(* todo: get new startLine if cursor has moved out of screen *)
|
|
|
|
|
|
|
|
|
|
(* move LineGap to first line displayed on screen, and build new text *)
|
|
|
|
|
val buffer = LineGap.goToLine (startLine, buffer)
|
|
|
|
|
val drawMsg = TextBuilder.build
|
|
|
|
|
(startLine, cursorIdx, buffer, windowWidth, windowHeight)
|
|
|
|
|
|
|
|
|
|
val mode = NORMAL_MODE ""
|
|
|
|
|
val newApp = AppWith.bufferAndCursorIdx (app, buffer, cursorIdx, mode)
|
|
|
|
|
in
|
|
|
|
|
(newApp, drawMsg)
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
let
|
|
|
|
|
(* move LineGap to cursorIdx, which is necessary for finding newCursorIdx *)
|
|
|
|
|
val buffer = LineGap.goToIdx (cursorIdx, buffer)
|
|
|
|
|
val cursorIdx = fMove (buffer, cursorIdx)
|
|
|
|
|
in
|
|
|
|
|
helpMoveForwards (app, buffer, cursorIdx, count - 1, fMove)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
fun moveForwards (app: app_type, count, fMove) =
|
|
|
|
|
let val {cursorIdx, buffer, ...} = app
|
|
|
|
|
in helpMoveForwards (app, buffer, cursorIdx, count, fMove)
|
2024-10-25 21:01:48 +01:00
|
|
|
end
|
|
|
|
|
|
2024-10-25 21:44:46 +01:00
|
|
|
fun firstNonSpaceChr (app: app_type) =
|
|
|
|
|
let
|
2024-10-26 06:01:48 +01:00
|
|
|
val {buffer, cursorIdx, windowWidth, windowHeight, startLine, ...} = app
|
2024-10-25 21:44:46 +01:00
|
|
|
|
|
|
|
|
(* move LineGap and buffer to start of line *)
|
|
|
|
|
val buffer = LineGap.goToIdx (cursorIdx, buffer)
|
|
|
|
|
val cursorIdx = Cursor.vi0 (buffer, cursorIdx)
|
|
|
|
|
|
|
|
|
|
(* move cursorIdx to first character on line *)
|
|
|
|
|
val buffer = LineGap.goToIdx (cursorIdx, buffer)
|
|
|
|
|
val cursorIdx = Cursor.firstNonSpaceChr (buffer, cursorIdx)
|
|
|
|
|
|
|
|
|
|
(* todo: get new startLine if cursor has moved out of screen *)
|
|
|
|
|
|
|
|
|
|
(* move LineGap to first line displayed on screen, and build new text *)
|
|
|
|
|
val buffer = LineGap.goToLine (startLine, buffer)
|
|
|
|
|
val drawMsg = TextBuilder.build
|
|
|
|
|
(startLine, cursorIdx, buffer, windowWidth, windowHeight)
|
|
|
|
|
|
2024-10-26 06:01:48 +01:00
|
|
|
val mode = NORMAL_MODE ""
|
|
|
|
|
val newApp = AppWith.bufferAndCursorIdx (app, buffer, cursorIdx, mode)
|
2024-10-25 21:44:46 +01:00
|
|
|
in
|
|
|
|
|
(newApp, drawMsg)
|
|
|
|
|
end
|
|
|
|
|
|
2024-10-26 06:01:48 +01:00
|
|
|
(* number of characters which are integers *)
|
|
|
|
|
fun getNumLength (pos, str) =
|
|
|
|
|
if pos = String.size str then
|
|
|
|
|
pos
|
|
|
|
|
else
|
|
|
|
|
let
|
|
|
|
|
val chr = String.sub (str, pos)
|
|
|
|
|
in
|
2024-10-26 10:47:45 +01:00
|
|
|
if Char.isDigit chr then getNumLength (pos + 1, str)
|
2024-10-26 06:01:48 +01:00
|
|
|
else pos
|
|
|
|
|
end
|
|
|
|
|
|
2024-10-26 10:47:45 +01:00
|
|
|
fun appendChr (app: app_type, chr, str) =
|
|
|
|
|
let
|
|
|
|
|
val str = str ^ Char.toString chr
|
|
|
|
|
val mode = NORMAL_MODE str
|
|
|
|
|
val newApp = AppWith.mode (app, mode)
|
|
|
|
|
in
|
|
|
|
|
(newApp, [])
|
|
|
|
|
end
|
|
|
|
|
|
2024-10-26 06:01:48 +01:00
|
|
|
fun handleChr (app: app_type, count, chr, str) =
|
2024-10-17 02:14:09 +01:00
|
|
|
case chr of
|
2024-10-26 06:01:48 +01:00
|
|
|
#"h" => moveBackward (app, count, Cursor.viH)
|
|
|
|
|
| #"j" => moveForwards (app, count, Cursor.viJ)
|
|
|
|
|
| #"k" => moveBackward (app, count, Cursor.viK)
|
|
|
|
|
| #"l" => moveForwards (app, count, Cursor.viL)
|
|
|
|
|
| #"w" => moveForwards (app, count, Cursor.nextWord)
|
|
|
|
|
| #"W" => moveForwards (app, count, Cursor.nextWORD)
|
|
|
|
|
| #"b" => moveBackward (app, count, Cursor.prevWord)
|
|
|
|
|
| #"B" => moveBackward (app, count, Cursor.prevWORD)
|
|
|
|
|
| #"e" => moveForwards (app, count, Cursor.endOfWord)
|
|
|
|
|
| #"E" => moveForwards (app, count, Cursor.endOfWORD)
|
2024-10-26 11:04:04 +01:00
|
|
|
(* can only move to start or end of line once
|
|
|
|
|
* so hardcode count as 1 *)
|
|
|
|
|
| #"0" =>
|
|
|
|
|
(* 0 is a bit of a special case.
|
|
|
|
|
* If 0 is pressed without any preceding characters,
|
|
|
|
|
* then it should move cursor to the start of the line.
|
|
|
|
|
* However, if a number was pressed previously before 0 was,
|
|
|
|
|
* then this means user is entering a count.
|
|
|
|
|
* In that case, we append 0 to the string. *)
|
|
|
|
|
if String.size str > 0 then
|
|
|
|
|
let
|
|
|
|
|
val lastChr = String.sub (str, String.size str - 1)
|
|
|
|
|
in
|
|
|
|
|
if Char.isDigit lastChr then
|
|
|
|
|
let
|
|
|
|
|
val chr = Char.toString chr
|
|
|
|
|
val str = str ^ chr
|
|
|
|
|
val mode = NORMAL_MODE str
|
|
|
|
|
val newApp = AppWith.mode (app, mode)
|
|
|
|
|
in
|
|
|
|
|
(newApp, [])
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
moveBackward (app, 1, Cursor.vi0)
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
moveBackward (app, 1, Cursor.vi0)
|
2024-10-26 06:54:15 +01:00
|
|
|
| #"$" => moveForwards (app, 1, Cursor.viDlr)
|
2024-10-25 21:44:46 +01:00
|
|
|
| #"^" => firstNonSpaceChr app
|
2024-10-26 10:47:45 +01:00
|
|
|
(* multi-char commands which can be appended *)
|
|
|
|
|
| #"t" => appendChr (app, chr, str)
|
|
|
|
|
| #"T" => appendChr (app, chr, str)
|
|
|
|
|
| #"y" => appendChr (app, chr, str)
|
|
|
|
|
| #"d" => appendChr (app, chr, str)
|
|
|
|
|
| #"f" => appendChr (app, chr, str)
|
|
|
|
|
| #"F" => appendChr (app, chr, str)
|
|
|
|
|
| #"g" => appendChr (app, chr, str)
|
|
|
|
|
| #"c" => appendChr (app, chr, str)
|
2024-10-26 06:01:48 +01:00
|
|
|
| _ =>
|
|
|
|
|
(* user may be entering a cmd with more than one chr
|
|
|
|
|
* such as "2dw" to delete two word
|
|
|
|
|
* so add current chr to mode, and save it in the app state *)
|
|
|
|
|
let
|
|
|
|
|
val str =
|
2024-10-26 10:47:45 +01:00
|
|
|
if Char.isDigit chr
|
|
|
|
|
then str ^ Char.toString chr
|
2024-10-26 06:01:48 +01:00
|
|
|
else ""
|
|
|
|
|
val mode = NORMAL_MODE str
|
|
|
|
|
val newApp = AppWith.mode (app, mode)
|
|
|
|
|
in
|
|
|
|
|
(newApp, [])
|
|
|
|
|
end
|
|
|
|
|
|
2024-10-26 10:47:45 +01:00
|
|
|
fun helpMoveToChrNext (app: app_type, buffer, cursorIdx, count, fMove, chr) =
|
|
|
|
|
if count = 0 then
|
|
|
|
|
let
|
|
|
|
|
val {windowWidth, windowHeight, startLine, ...} = app
|
|
|
|
|
(* todo: get new startLine if cursor has moved out of screen *)
|
|
|
|
|
|
|
|
|
|
(* move LineGap to first line displayed on screen, and build new text *)
|
|
|
|
|
val buffer = LineGap.goToLine (startLine, buffer)
|
|
|
|
|
val drawMsg = TextBuilder.build
|
|
|
|
|
(startLine, cursorIdx, buffer, windowWidth, windowHeight)
|
|
|
|
|
|
|
|
|
|
val mode = NORMAL_MODE ""
|
|
|
|
|
val newApp = AppWith.bufferAndCursorIdx (app, buffer, cursorIdx, mode)
|
|
|
|
|
in
|
|
|
|
|
(newApp, drawMsg)
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
let
|
|
|
|
|
(* move LineGap to cursorIdx, which is necessary for finding newCursorIdx *)
|
|
|
|
|
val buffer = LineGap.goToIdx (cursorIdx, buffer)
|
|
|
|
|
val cursorIdx = fMove (buffer, cursorIdx, chr)
|
|
|
|
|
in
|
|
|
|
|
helpMoveToChrNext (app, buffer, cursorIdx, count - 1, fMove, chr)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
fun moveToChrNext (app: app_type, count, fMove, chr) =
|
|
|
|
|
let val {cursorIdx, buffer, ...} = app
|
|
|
|
|
in helpMoveToChrNext (app, buffer, cursorIdx, count, fMove, chr)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
(* temp placeholder function *)
|
|
|
|
|
fun clearMode app =
|
|
|
|
|
let
|
|
|
|
|
val mode = NORMAL_MODE ""
|
|
|
|
|
val newApp = AppWith.mode (app, mode)
|
|
|
|
|
in
|
|
|
|
|
(newApp, [])
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
fun handleNextChr (count, app, fMove, newCmd) =
|
|
|
|
|
case newCmd of
|
|
|
|
|
CHAR_EVENT chr => moveToChrNext (app, count, fMove, chr)
|
|
|
|
|
| RESIZE_EVENT (width, height) => resizeText (app, width, height)
|
|
|
|
|
|
2024-10-26 06:54:15 +01:00
|
|
|
(* useful reference as list of non-terminal commands *)
|
2024-10-26 10:47:45 +01:00
|
|
|
(* todo: actually parse, checking if there are further strings or input *)
|
2024-10-26 06:54:15 +01:00
|
|
|
fun parseAfterCount (strPos, str, count, app, newCmd) =
|
|
|
|
|
(* we are trying to parse multi-char but non-terminal strings here.
|
|
|
|
|
* For example, we don't want to parse 3w which is a terminal commmand
|
|
|
|
|
* to go 3 words forwards
|
|
|
|
|
* but we do want to parse 3d which is a non-terminal command
|
|
|
|
|
* which can be made terminal by adding "w" or "e" at the end.
|
|
|
|
|
* *)
|
2024-10-26 10:47:45 +01:00
|
|
|
case String.sub (str, strPos) of
|
2024-10-26 06:54:15 +01:00
|
|
|
#"t" =>
|
2024-10-26 10:47:45 +01:00
|
|
|
(* to just before char, forward
|
|
|
|
|
* tillNextChr with count of 1 has same effect
|
|
|
|
|
* as tillNextChr with any count above 1
|
|
|
|
|
* so just hardcode 1 *)
|
|
|
|
|
handleNextChr (1, app, Cursor.tillNextChr, newCmd)
|
|
|
|
|
| #"T" =>
|
|
|
|
|
(* to just before chr, backward *)
|
|
|
|
|
clearMode app
|
|
|
|
|
| #"y" =>
|
|
|
|
|
(* yank *)
|
|
|
|
|
clearMode app
|
|
|
|
|
| #"d" =>
|
|
|
|
|
(* delete *)
|
|
|
|
|
clearMode app
|
2024-10-26 06:54:15 +01:00
|
|
|
| #"f" =>
|
|
|
|
|
(* to chr, forward *)
|
2024-10-26 10:47:45 +01:00
|
|
|
handleNextChr (count, app, Cursor.toNextChr, newCmd)
|
|
|
|
|
| #"F" =>
|
|
|
|
|
(* to chr, backward *)
|
2024-10-26 21:21:33 +01:00
|
|
|
handleNextChr (count, app, Cursor.toPrevChr, newCmd)
|
2024-10-26 10:47:45 +01:00
|
|
|
| #"g" =>
|
|
|
|
|
(* go *)
|
|
|
|
|
clearMode app
|
|
|
|
|
| #"c" =>
|
|
|
|
|
(* change *)
|
|
|
|
|
clearMode app
|
2024-10-26 06:54:15 +01:00
|
|
|
| _ =>
|
2024-10-26 10:47:45 +01:00
|
|
|
(* isn't a non-terminal cmd
|
|
|
|
|
* this case should never happen*)
|
|
|
|
|
clearMode app
|
2024-10-26 06:54:15 +01:00
|
|
|
|
2024-10-26 06:01:48 +01:00
|
|
|
fun parseNormalModeCommand (app, str, newCmd) =
|
|
|
|
|
if String.size str = 0 then
|
|
|
|
|
case newCmd of
|
|
|
|
|
RESIZE_EVENT (width, height) => resizeText (app, width, height)
|
|
|
|
|
| CHAR_EVENT chr => handleChr (app, 1, chr, str)
|
2024-10-26 10:47:45 +01:00
|
|
|
else if String.size str = 1 then
|
|
|
|
|
case newCmd of
|
|
|
|
|
RESIZE_EVENT (width, height) => resizeText (app, width, height)
|
|
|
|
|
| CHAR_EVENT chr =>
|
|
|
|
|
(case Int.fromString str of
|
|
|
|
|
SOME count => handleChr (app, count, chr, str)
|
|
|
|
|
| NONE => parseAfterCount (0, str, 1, app, newCmd))
|
2024-10-26 06:01:48 +01:00
|
|
|
else
|
|
|
|
|
let
|
|
|
|
|
val numLength = getNumLength (0, str)
|
|
|
|
|
val count = String.substring (str, 0, numLength)
|
|
|
|
|
val count =
|
|
|
|
|
case Int.fromString count of
|
|
|
|
|
SOME x => x
|
|
|
|
|
| NONE => 1
|
|
|
|
|
in
|
|
|
|
|
if numLength = String.size str then
|
|
|
|
|
(* reached end of str; str only contained numbers *)
|
|
|
|
|
case newCmd of
|
|
|
|
|
RESIZE_EVENT (width, height) => resizeText (app, width, height)
|
|
|
|
|
| CHAR_EVENT chr => handleChr (app, count, chr, str)
|
2024-10-26 10:47:45 +01:00
|
|
|
else if numLength + 1 < String.size str then
|
|
|
|
|
(* continue parsing. *)
|
|
|
|
|
parseAfterCount (numLength + 1, str, count, app, newCmd)
|
2024-10-26 06:01:48 +01:00
|
|
|
else
|
2024-10-26 10:47:45 +01:00
|
|
|
(* continue parsing. *)
|
|
|
|
|
parseAfterCount (numLength, str, count, app, newCmd)
|
2024-10-26 06:01:48 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
fun updateNormalMode (app, str, msg) = parseNormalModeCommand (app, str, msg)
|
2024-10-17 02:14:09 +01:00
|
|
|
|
2024-10-06 09:32:56 +01:00
|
|
|
fun update (app, msg) =
|
2024-10-26 06:01:48 +01:00
|
|
|
case #mode app of NORMAL_MODE str => updateNormalMode (app, str, msg)
|
2024-10-06 09:32:56 +01:00
|
|
|
end
|