From f63e416e8ff7c26f10f6ed6280e5f40a08cbba58 Mon Sep 17 00:00:00 2001 From: Humza Shahid Date: Thu, 7 Aug 2025 18:02:47 +0100 Subject: [PATCH] move normal-mode deletion functions to their own file --- fcore/app-update.sml | 422 ++-------------------------- fcore/normal-mode/normal-delete.sml | 378 +++++++++++++++++++++++++ shf-tests.mlb | 3 + shf.mlb | 3 + 4 files changed, 408 insertions(+), 398 deletions(-) create mode 100644 fcore/normal-mode/normal-delete.sml diff --git a/fcore/app-update.sml b/fcore/app-update.sml index dac6cdf..fd270f8 100644 --- a/fcore/app-update.sml +++ b/fcore/app-update.sml @@ -6,380 +6,6 @@ struct open DrawMsg open InputMsg - (* text-delete functions *) - (** equivalent of vi's 'x' command **) - fun helpRemoveChr (app: app_type, buffer, cursorIdx, count) = - if count = 0 then - let - val searchString = #searchString app - val buffer = LineGap.goToEnd buffer - val initialMsg = [SEARCH (buffer, searchString)] - - val buffer = LineGap.goToIdx (cursorIdx + 777, buffer) - val searchList = - SearchList.buildRange (buffer, searchString, cursorIdx - 777) - in - Finish.buildTextAndClear - (app, buffer, cursorIdx, searchList, initialMsg) - end - else - let - val buffer = LineGap.goToIdx (cursorIdx, buffer) - - (* Explanation of how Vi's 'x' command behaves: - * If the cursor is at the end of the file, - * then it is decremented by 1. - * If the character after the cursor is a line break, - * then it is also decremented by 1. - * If the character before the cursor is a linee break, the cursor stays - * where it is. - * If the chracter AT the cursor is a line break and the characater - * AFTER the cursor is also a line break, then nothing is deleted. - * Otherwise, the same cursor is returned. - * All decrement cases do not decrement when the cursor is 0. *) - val cursorIsStart = Cursor.isCursorAtStartOfLine (buffer, cursorIdx) - val nextIsEnd = Cursor.isNextChrEndOfLine (buffer, cursorIdx) - in - if nextIsEnd andalso cursorIsStart then - (* vi simply doesn't do anything on 'x' command - * when cursor is at start of line, and next chr is line break - * so skip to end of loop by passing count of 0 *) - helpRemoveChr (app, buffer, cursorIdx, 0) - else if cursorIsStart then - helpRemoveChr (app, buffer, cursorIdx, 0) - else if nextIsEnd then - let - (* delete char at cursor and then decrement cursorIdx by 1 - * if cursorIdx is not 0 *) - val searchString = #searchString app - val buffer = LineGap.delete (cursorIdx, 1, buffer) - - val cursorIdx = - if - Cursor.isPrevChrStartOfLine (buffer, cursorIdx) - orelse cursorIdx = 0 - then cursorIdx - else cursorIdx - 1 - in - helpRemoveChr (app, buffer, cursorIdx, count - 1) - end - else - let - val searchString = #searchString app - val buffer = LineGap.delete (cursorIdx, 1, buffer) - in - helpRemoveChr (app, buffer, cursorIdx, count - 1) - end - end - - fun removeChr (app: app_type, count) = - helpRemoveChr (app, #buffer app, #cursorIdx app, count) - - fun helpDelete (app: app_type, buffer, cursorIdx, otherIdx, count, fMove) = - (* As a small optimisation to reduce allocations, - * we accumulate otherIdx by calling fMove with it and the buffer - * on each loop. - * Then, at the end of the loop, we perform the actual deletion. - * This is faster than performing the actual deletion on every loop - * because we only delete once, and avoid allocating intermediary buffers. - * The behaviour between the two is equivalent. *) - if count = 0 then - let - val low = Int.min (cursorIdx, otherIdx) - val high = Int.max (cursorIdx, otherIdx) - val high = Cursor.clipIdx (buffer, high) - val length = high - low - - val buffer = LineGap.delete (low, length, buffer) - - val buffer = LineGap.goToEnd buffer - val searchString = #searchString app - val initialMsg = [SEARCH (buffer, searchString)] - - val buffer = LineGap.goToIdx (cursorIdx + 777, buffer) - val searchList = - SearchList.buildRange (buffer, searchString, cursorIdx - 777) - - (* If we have deleted from the buffer so that cursorIdx - * is no longer a valid idx, - * clip cursorIdx to the end. *) - val buffer = LineGap.goToIdx (low, buffer) - val cursorIdx = Cursor.clipIdx (buffer, low) - in - Finish.buildTextAndClear - (app, buffer, cursorIdx, searchList, initialMsg) - end - else - let - (* get otherIdx, where cursor will want to go after motion. *) - val buffer = LineGap.goToIdx (otherIdx, buffer) - val newOtherIdx = fMove (buffer, otherIdx) - val newCount = if newOtherIdx = otherIdx then 0 else count - 1 - in - helpDelete (app, buffer, cursorIdx, newOtherIdx, newCount, fMove) - end - - fun delete (app: app_type, count, fMove) = - helpDelete (app, #buffer app, #cursorIdx app, #cursorIdx app, count, fMove) - - fun deleteByDfa (app: app_type, count, fMove) = - let - val {buffer, cursorIdx, searchString, ...} = app - - val buffer = LineGap.goToIdx (cursorIdx, buffer) - val otherIdx = fMove (buffer, cursorIdx, count) - - val low = Int.min (cursorIdx, otherIdx) - val high = Int.max (cursorIdx, otherIdx) - val length = high - low - - val buffer = LineGap.delete (low, length, buffer) - - val buffer = LineGap.goToEnd buffer - val initialMsg = [SEARCH (buffer, searchString)] - - val buffer = LineGap.goToIdx (cursorIdx + 777, buffer) - val searchList = - SearchList.buildRange (buffer, searchString, cursorIdx - 777) - - val buffer = LineGap.goToIdx (low, buffer) - in - Finish.buildTextAndClear (app, buffer, low, searchList, initialMsg) - end - - fun deleteToEndOfLine (app: app_type) = - let - val {buffer, cursorIdx, ...} = app - in - if Cursor.isCursorAtStartOfLine (buffer, cursorIdx) then - (* if we are on \n, we don't want to delete or do anything - * so reset the mode *) - Finish.clearMode app - else - let - (* viDlr takes us to the last chr in the line - * but does not delete that last chr - * so we call helpRemoveChr to delete that last chr. - * We also rely on helpRemoveChr to handle backwards-movement logic: - * If cursorIdx is at \n after deletion, then stop. - * Else, move back one chr. *) - val lastChr = Cursor.viDlr (buffer, cursorIdx, 1) - val length = lastChr - cursorIdx - val buffer = LineGap.delete (cursorIdx, length, buffer) - in - helpRemoveChr (app, buffer, cursorIdx, 1) - end - end - - fun deleteLine (app: app_type, count) = - let - val {buffer, cursorIdx, searchString, ...} = app - val buffer = LineGap.goToIdx (cursorIdx, buffer) - - val startIdx = Cursor.vi0 (buffer, cursorIdx) - val finishIdx = Cursor.viDlrForDelete (buffer, cursorIdx, count) - - val length = finishIdx - startIdx - val buffer = LineGap.delete (startIdx, length, buffer) - - val buffer = LineGap.goToEnd buffer - val initialMsg = [SEARCH (buffer, searchString)] - - val buffer = LineGap.goToIdx (cursorIdx + 777, buffer) - val searchList = - SearchList.buildRange (buffer, searchString, cursorIdx - 777) - - val buffer = LineGap.goToIdx (startIdx, buffer) - in - Finish.buildTextAndClear (app, buffer, startIdx, searchList, initialMsg) - end - - fun helpDeleteLineBack (app, buffer, low, high, count) = - if count = 0 then - let - val low = Int.max (low, 0) - val length = high - low - val buffer = LineGap.delete (low, length, buffer) - - val buffer = LineGap.goToEnd buffer - val searchString = #searchString app - val initialMsg = [SEARCH (buffer, searchString)] - - val buffer = LineGap.goToIdx (low + 777, buffer) - val searchList = SearchList.buildRange (buffer, searchString, low - 777) - - val buffer = LineGap.goToIdx (low, buffer) - in - Finish.buildTextAndClear (app, buffer, low, searchList, initialMsg) - end - else - let - val buffer = LineGap.goToIdx (low, buffer) - val low = Cursor.viH (buffer, low) - val buffer = LineGap.goToIdx (low, buffer) - val low = Cursor.vi0 (buffer, low) - val newCount = if low = 0 then 0 else count - 1 - in - helpDeleteLineBack (app, buffer, low, high, newCount) - end - - fun deleteLineBack (app: app_type, count) = - let - val {buffer, cursorIdx, ...} = app - val low = Cursor.vi0 (buffer, cursorIdx) - val high = Cursor.viDlr (buffer, cursorIdx, 1) + 1 - in - helpDeleteLineBack (app, buffer, low, high, count) - end - - fun deleteToFirstNonSpaceChr (app: app_type) = - let - val - { buffer - , cursorIdx - , windowWidth - , windowHeight - , startLine - , searchString - , ... - } = app - - (* move LineGap and buffer to start of line *) - val buffer = LineGap.goToIdx (cursorIdx, buffer) - val otherIdx = Cursor.vi0 (buffer, cursorIdx) - - (* move cursorIdx to first character on line *) - val buffer = LineGap.goToIdx (cursorIdx, buffer) - val otherIdx = Cursor.firstNonSpaceChr (buffer, otherIdx) - - val low = Int.min (cursorIdx, otherIdx) - val high = Int.max (cursorIdx, otherIdx) - val length = high - low - - val buffer = LineGap.delete (low, length, buffer) - - val buffer = LineGap.goToEnd buffer - val initialMsg = [SEARCH (buffer, searchString)] - - val buffer = LineGap.goToIdx (cursorIdx + 777, buffer) - val searchList = - SearchList.buildRange (buffer, searchString, cursorIdx - 777) - in - Finish.buildTextAndClear (app, buffer, low, searchList, initialMsg) - end - - fun helpDeleteToChr - (app: app_type, buffer, cursorIdx, otherIdx, count, fMove, fInc, chr) = - if count = 0 then - let - val low = Int.min (cursorIdx, otherIdx) - val high = Int.max (cursorIdx, otherIdx) - val length = high - low - val buffer = LineGap.delete (low, length, buffer) - - val buffer = LineGap.goToEnd buffer - val searchString = #searchString app - val initialMsg = [SEARCH (buffer, searchString)] - - val buffer = LineGap.goToIdx (cursorIdx + 777, buffer) - val searchList = - SearchList.buildRange (buffer, searchString, cursorIdx - 777) - in - Finish.buildTextAndClearAfterChr (app, buffer, low, searchList, initialMsg) - end - else - let - val buffer = LineGap.goToIdx (otherIdx, buffer) - val newOtherIdx = fMove (buffer, otherIdx, chr) - val newCount = if newOtherIdx = otherIdx then 0 else count - 1 - val newOtherIdx = fInc (newOtherIdx, 1) - in - helpDeleteToChr - (app, buffer, cursorIdx, newOtherIdx, newCount, fMove, fInc, chr) - end - - fun deleteToChr (app: app_type, count, fMove, fInc, chr) = - helpDeleteToChr - ( app - , #buffer app - , #cursorIdx app - , #cursorIdx app - , count - , fMove - , fInc - , chr - ) - - fun deleteToStart (app: app_type) = - let - val {cursorIdx, buffer, windowWidth, windowHeight, searchString, ...} = - app - - val buffer = LineGap.delete (0, cursorIdx, buffer) - - val buffer = LineGap.goToEnd buffer - val initialMsg = [SEARCH (buffer, #searchString app)] - - val buffer = LineGap.goToIdx (cursorIdx + 777, buffer) - val searchList = - SearchList.buildRange (buffer, searchString, cursorIdx - 777) - - val cursorIdx = 0 - val startLine = 0 - val buffer = LineGap.goToIdx (cursorIdx, buffer) - - val drawMsg = TextBuilder.build - ( startLine - , cursorIdx - , buffer - , windowWidth - , windowHeight - , searchList - , searchString - , initialMsg - ) - - val mode = NORMAL_MODE "" - in - AppWith.bufferAndCursorIdx - (app, buffer, cursorIdx, mode, startLine, searchList, drawMsg) - end - - fun helpDeleteToMatch (app: app_type, low, high) = - let - val {buffer, searchString, ...} = app - val length = high - low - val buffer = LineGap.delete (low, length, buffer) - - val buffer = LineGap.goToEnd buffer - val initialMsg = [SEARCH (buffer, searchString)] - - val buffer = LineGap.goToIdx (low + 777, buffer) - val searchList = SearchList.buildRange (buffer, searchString, low - 777) - - val buffer = LineGap.goToIdx (low, buffer) - in - Finish.buildTextAndClear (app, buffer, low, searchList, initialMsg) - end - - fun deleteToNextMatch (app: app_type, count) = - let - val {cursorIdx, searchList, ...} = app - val newCursorIdx = SearchList.nextMatch (cursorIdx, searchList, count) - in - if newCursorIdx = ~1 orelse newCursorIdx <= cursorIdx then Finish.clearMode app - else helpDeleteToMatch (app, cursorIdx, newCursorIdx) - end - - fun deleteToPrevMatch (app: app_type, count) = - let - val {cursorIdx, searchList, ...} = app - val newCursorIdx = SearchList.prevMatch (cursorIdx, searchList, count) - in - if newCursorIdx = ~1 orelse newCursorIdx >= cursorIdx then Finish.clearMode app - else helpDeleteToMatch (app, newCursorIdx, cursorIdx) - end - (* command-parsing functions *) (** number of characters which are integers *) fun getNumLength (pos, str) = @@ -467,7 +93,7 @@ struct if String.size str = 0 then NormalMove.moveToEnd app else NormalMove.moveToLine (app, count - 1) | #"%" => NormalMove.moveToMatchingPair app - | #"x" => removeChr (app, count) + | #"x" => NormalDelete.removeChr (app, count) (* multi-char commands which can be appended *) | #"t" => appendChr (app, chr, str) | #"T" => appendChr (app, chr, str) @@ -495,26 +121,26 @@ struct CHAR_EVENT chr => (case chr of (* terminal commands: require no input after *) - #"h" => delete (app, count, Cursor.viH) - | #"l" => delete (app, count, Cursor.viL) + #"h" => NormalDelete.delete (app, count, Cursor.viH) + | #"l" => NormalDelete.delete (app, count, Cursor.viL) (* vi's 'j' and 'k' commands move up or down a column * but 'dj' or 'dk' delete whole lines * so their implementation differs from * other cursor motions *) - | #"j" => deleteLine (app, count + 1) - | #"k" => deleteLineBack (app, count) - | #"w" => deleteByDfa (app, count, Cursor.nextWord) - | #"W" => deleteByDfa (app, count, Cursor.nextWORD) - | #"b" => deleteByDfa (app, count, Cursor.prevWord) - | #"B" => deleteByDfa (app, count, Cursor.prevWORD) - | #"e" => deleteByDfa (app, count, Cursor.endOfWordForDelete) - | #"E" => deleteByDfa (app, count, Cursor.endOfWORDForDelete) - | #"0" => delete (app, 1, Cursor.vi0) - | #"$" => deleteToEndOfLine app - | #"^" => deleteToFirstNonSpaceChr app - | #"d" => deleteLine (app, count) - | #"n" => deleteToNextMatch (app, count) - | #"N" => deleteToPrevMatch (app, count) + | #"j" => NormalDelete.deleteLine (app, count + 1) + | #"k" => NormalDelete.deleteLineBack (app, count) + | #"w" => NormalDelete.deleteByDfa (app, count, Cursor.nextWord) + | #"W" => NormalDelete.deleteByDfa (app, count, Cursor.nextWORD) + | #"b" => NormalDelete.deleteByDfa (app, count, Cursor.prevWord) + | #"B" => NormalDelete.deleteByDfa (app, count, Cursor.prevWORD) + | #"e" => NormalDelete.deleteByDfa (app, count, Cursor.endOfWordForDelete) + | #"E" => NormalDelete.deleteByDfa (app, count, Cursor.endOfWORDForDelete) + | #"0" => NormalDelete.delete (app, 1, Cursor.vi0) + | #"$" => NormalDelete.deleteToEndOfLine app + | #"^" => NormalDelete.deleteToFirstNonSpaceChr app + | #"d" => NormalDelete.deleteLine (app, count) + | #"n" => NormalDelete.deleteToNextMatch (app, count) + | #"N" => NormalDelete.deleteToPrevMatch (app, count) (* non-terminal commands which require appending chr *) | #"t" => appendChr (app, chr, str) | #"T" => appendChr (app, chr, str) @@ -533,7 +159,7 @@ struct (* delete till chr, forwards *) (case newCmd of CHAR_EVENT chr => - deleteToChr (app, 1, Cursor.tillNextChr, op+, chr) + NormalDelete.deleteToChr (app, 1, Cursor.tillNextChr, op+, chr) | KEY_ESC => Finish.clearMode app | RESIZE_EVENT (width, height) => Finish.resizeText (app, width, height) | WITH_SEARCH_LIST searchList => Finish.withSearchList (app, searchList)) @@ -541,14 +167,14 @@ struct (* delete till chr, backwards *) (case newCmd of CHAR_EVENT chr => - deleteToChr (app, 1, Cursor.tillPrevChr, op-, chr) + NormalDelete.deleteToChr (app, 1, Cursor.tillPrevChr, op-, chr) | KEY_ESC => Finish.clearMode app | RESIZE_EVENT (width, height) => Finish.resizeText (app, width, height) | WITH_SEARCH_LIST searchList => Finish.withSearchList (app, searchList)) | #"f" => (case newCmd of CHAR_EVENT chr => - deleteToChr (app, count, Cursor.toNextChr, op+, chr) + NormalDelete.deleteToChr (app, count, Cursor.toNextChr, op+, chr) | KEY_ESC => Finish.clearMode app | RESIZE_EVENT (width, height) => Finish.resizeText (app, width, height) | WITH_SEARCH_LIST searchList => Finish.withSearchList (app, searchList)) @@ -556,7 +182,7 @@ struct (* delete to chr, backwards *) (case newCmd of CHAR_EVENT chr => - deleteToChr (app, count, Cursor.toPrevChr, op-, chr) + NormalDelete.deleteToChr (app, count, Cursor.toPrevChr, op-, chr) | KEY_ESC => Finish.clearMode app | RESIZE_EVENT (width, height) => Finish.resizeText (app, width, height) | WITH_SEARCH_LIST searchList => Finish.withSearchList (app, searchList)) @@ -565,9 +191,9 @@ struct (case newCmd of CHAR_EVENT chr => (case chr of - #"e" => deleteByDfa (app, count, Cursor.endOfPrevWord) - | #"E" => deleteByDfa (app, count, Cursor.endOfPrevWORD) - | #"g" => deleteToStart app + #"e" => NormalDelete.deleteByDfa (app, count, Cursor.endOfPrevWord) + | #"E" => NormalDelete.deleteByDfa (app, count, Cursor.endOfPrevWORD) + | #"g" => NormalDelete.deleteToStart app | _ => Finish.clearMode app) | KEY_ESC => Finish.clearMode app | RESIZE_EVENT (width, height) => Finish.resizeText (app, width, height) diff --git a/fcore/normal-mode/normal-delete.sml b/fcore/normal-mode/normal-delete.sml new file mode 100644 index 0000000..70e2947 --- /dev/null +++ b/fcore/normal-mode/normal-delete.sml @@ -0,0 +1,378 @@ +structure NormalDelete = +struct + open AppType + open MailboxType + + (* equivalent of vi's 'x' command **) + fun helpRemoveChr (app: app_type, buffer, cursorIdx, count) = + if count = 0 then + let + val searchString = #searchString app + val buffer = LineGap.goToEnd buffer + val initialMsg = [SEARCH (buffer, searchString)] + + val buffer = LineGap.goToIdx (cursorIdx + 777, buffer) + val searchList = + SearchList.buildRange (buffer, searchString, cursorIdx - 777) + in + Finish.buildTextAndClear + (app, buffer, cursorIdx, searchList, initialMsg) + end + else + let + val buffer = LineGap.goToIdx (cursorIdx, buffer) + + (* Explanation of how Vi's 'x' command behaves: + * If the cursor is at the end of the file, + * then it is decremented by 1. + * If the character after the cursor is a line break, + * then it is also decremented by 1. + * If the character before the cursor is a linee break, the cursor stays + * where it is. + * If the chracter AT the cursor is a line break and the characater + * AFTER the cursor is also a line break, then nothing is deleted. + * Otherwise, the same cursor is returned. + * All decrement cases do not decrement when the cursor is 0. *) + val cursorIsStart = Cursor.isCursorAtStartOfLine (buffer, cursorIdx) + val nextIsEnd = Cursor.isNextChrEndOfLine (buffer, cursorIdx) + in + if nextIsEnd andalso cursorIsStart then + (* vi simply doesn't do anything on 'x' command + * when cursor is at start of line, and next chr is line break + * so skip to end of loop by passing count of 0 *) + helpRemoveChr (app, buffer, cursorIdx, 0) + else if cursorIsStart then + helpRemoveChr (app, buffer, cursorIdx, 0) + else if nextIsEnd then + let + (* delete char at cursor and then decrement cursorIdx by 1 + * if cursorIdx is not 0 *) + val searchString = #searchString app + val buffer = LineGap.delete (cursorIdx, 1, buffer) + + val cursorIdx = + if + Cursor.isPrevChrStartOfLine (buffer, cursorIdx) + orelse cursorIdx = 0 + then cursorIdx + else cursorIdx - 1 + in + helpRemoveChr (app, buffer, cursorIdx, count - 1) + end + else + let + val searchString = #searchString app + val buffer = LineGap.delete (cursorIdx, 1, buffer) + in + helpRemoveChr (app, buffer, cursorIdx, count - 1) + end + end + + fun removeChr (app: app_type, count) = + helpRemoveChr (app, #buffer app, #cursorIdx app, count) + + fun helpDelete (app: app_type, buffer, cursorIdx, otherIdx, count, fMove) = + (* As a small optimisation to reduce allocations, + * we accumulate otherIdx by calling fMove with it and the buffer + * on each loop. + * Then, at the end of the loop, we perform the actual deletion. + * This is faster than performing the actual deletion on every loop + * because we only delete once, and avoid allocating intermediary buffers. + * The behaviour between the two is equivalent. *) + if count = 0 then + let + val low = Int.min (cursorIdx, otherIdx) + val high = Int.max (cursorIdx, otherIdx) + val high = Cursor.clipIdx (buffer, high) + val length = high - low + + val buffer = LineGap.delete (low, length, buffer) + + val buffer = LineGap.goToEnd buffer + val searchString = #searchString app + val initialMsg = [SEARCH (buffer, searchString)] + + val buffer = LineGap.goToIdx (cursorIdx + 777, buffer) + val searchList = + SearchList.buildRange (buffer, searchString, cursorIdx - 777) + + (* If we have deleted from the buffer so that cursorIdx + * is no longer a valid idx, + * clip cursorIdx to the end. *) + val buffer = LineGap.goToIdx (low, buffer) + val cursorIdx = Cursor.clipIdx (buffer, low) + in + Finish.buildTextAndClear + (app, buffer, cursorIdx, searchList, initialMsg) + end + else + let + (* get otherIdx, where cursor will want to go after motion. *) + val buffer = LineGap.goToIdx (otherIdx, buffer) + val newOtherIdx = fMove (buffer, otherIdx) + val newCount = if newOtherIdx = otherIdx then 0 else count - 1 + in + helpDelete (app, buffer, cursorIdx, newOtherIdx, newCount, fMove) + end + + fun delete (app: app_type, count, fMove) = + helpDelete (app, #buffer app, #cursorIdx app, #cursorIdx app, count, fMove) + + fun deleteByDfa (app: app_type, count, fMove) = + let + val {buffer, cursorIdx, searchString, ...} = app + + val buffer = LineGap.goToIdx (cursorIdx, buffer) + val otherIdx = fMove (buffer, cursorIdx, count) + + val low = Int.min (cursorIdx, otherIdx) + val high = Int.max (cursorIdx, otherIdx) + val length = high - low + + val buffer = LineGap.delete (low, length, buffer) + + val buffer = LineGap.goToEnd buffer + val initialMsg = [SEARCH (buffer, searchString)] + + val buffer = LineGap.goToIdx (cursorIdx + 777, buffer) + val searchList = + SearchList.buildRange (buffer, searchString, cursorIdx - 777) + + val buffer = LineGap.goToIdx (low, buffer) + in + Finish.buildTextAndClear (app, buffer, low, searchList, initialMsg) + end + + fun deleteToEndOfLine (app: app_type) = + let + val {buffer, cursorIdx, ...} = app + in + if Cursor.isCursorAtStartOfLine (buffer, cursorIdx) then + (* if we are on \n, we don't want to delete or do anything + * so reset the mode *) + Finish.clearMode app + else + let + (* viDlr takes us to the last chr in the line + * but does not delete that last chr + * so we call helpRemoveChr to delete that last chr. + * We also rely on helpRemoveChr to handle backwards-movement logic: + * If cursorIdx is at \n after deletion, then stop. + * Else, move back one chr. *) + val lastChr = Cursor.viDlr (buffer, cursorIdx, 1) + val length = lastChr - cursorIdx + val buffer = LineGap.delete (cursorIdx, length, buffer) + in + helpRemoveChr (app, buffer, cursorIdx, 1) + end + end + + fun deleteLine (app: app_type, count) = + let + val {buffer, cursorIdx, searchString, ...} = app + val buffer = LineGap.goToIdx (cursorIdx, buffer) + + val startIdx = Cursor.vi0 (buffer, cursorIdx) + val finishIdx = Cursor.viDlrForDelete (buffer, cursorIdx, count) + + val length = finishIdx - startIdx + val buffer = LineGap.delete (startIdx, length, buffer) + + val buffer = LineGap.goToEnd buffer + val initialMsg = [SEARCH (buffer, searchString)] + + val buffer = LineGap.goToIdx (cursorIdx + 777, buffer) + val searchList = + SearchList.buildRange (buffer, searchString, cursorIdx - 777) + + val buffer = LineGap.goToIdx (startIdx, buffer) + in + Finish.buildTextAndClear (app, buffer, startIdx, searchList, initialMsg) + end + + fun helpDeleteLineBack (app, buffer, low, high, count) = + if count = 0 then + let + val low = Int.max (low, 0) + val length = high - low + val buffer = LineGap.delete (low, length, buffer) + + val buffer = LineGap.goToEnd buffer + val searchString = #searchString app + val initialMsg = [SEARCH (buffer, searchString)] + + val buffer = LineGap.goToIdx (low + 777, buffer) + val searchList = SearchList.buildRange (buffer, searchString, low - 777) + + val buffer = LineGap.goToIdx (low, buffer) + in + Finish.buildTextAndClear (app, buffer, low, searchList, initialMsg) + end + else + let + val buffer = LineGap.goToIdx (low, buffer) + val low = Cursor.viH (buffer, low) + val buffer = LineGap.goToIdx (low, buffer) + val low = Cursor.vi0 (buffer, low) + val newCount = if low = 0 then 0 else count - 1 + in + helpDeleteLineBack (app, buffer, low, high, newCount) + end + + fun deleteLineBack (app: app_type, count) = + let + val {buffer, cursorIdx, ...} = app + val low = Cursor.vi0 (buffer, cursorIdx) + val high = Cursor.viDlr (buffer, cursorIdx, 1) + 1 + in + helpDeleteLineBack (app, buffer, low, high, count) + end + + fun deleteToFirstNonSpaceChr (app: app_type) = + let + val + { buffer + , cursorIdx + , windowWidth + , windowHeight + , startLine + , searchString + , ... + } = app + + (* move LineGap and buffer to start of line *) + val buffer = LineGap.goToIdx (cursorIdx, buffer) + val otherIdx = Cursor.vi0 (buffer, cursorIdx) + + (* move cursorIdx to first character on line *) + val buffer = LineGap.goToIdx (cursorIdx, buffer) + val otherIdx = Cursor.firstNonSpaceChr (buffer, otherIdx) + + val low = Int.min (cursorIdx, otherIdx) + val high = Int.max (cursorIdx, otherIdx) + val length = high - low + + val buffer = LineGap.delete (low, length, buffer) + + val buffer = LineGap.goToEnd buffer + val initialMsg = [SEARCH (buffer, searchString)] + + val buffer = LineGap.goToIdx (cursorIdx + 777, buffer) + val searchList = + SearchList.buildRange (buffer, searchString, cursorIdx - 777) + in + Finish.buildTextAndClear (app, buffer, low, searchList, initialMsg) + end + + fun helpDeleteToChr + (app: app_type, buffer, cursorIdx, otherIdx, count, fMove, fInc, chr) = + if count = 0 then + let + val low = Int.min (cursorIdx, otherIdx) + val high = Int.max (cursorIdx, otherIdx) + val length = high - low + val buffer = LineGap.delete (low, length, buffer) + + val buffer = LineGap.goToEnd buffer + val searchString = #searchString app + val initialMsg = [SEARCH (buffer, searchString)] + + val buffer = LineGap.goToIdx (cursorIdx + 777, buffer) + val searchList = + SearchList.buildRange (buffer, searchString, cursorIdx - 777) + in + Finish.buildTextAndClearAfterChr (app, buffer, low, searchList, initialMsg) + end + else + let + val buffer = LineGap.goToIdx (otherIdx, buffer) + val newOtherIdx = fMove (buffer, otherIdx, chr) + val newCount = if newOtherIdx = otherIdx then 0 else count - 1 + val newOtherIdx = fInc (newOtherIdx, 1) + in + helpDeleteToChr + (app, buffer, cursorIdx, newOtherIdx, newCount, fMove, fInc, chr) + end + + fun deleteToChr (app: app_type, count, fMove, fInc, chr) = + helpDeleteToChr + ( app + , #buffer app + , #cursorIdx app + , #cursorIdx app + , count + , fMove + , fInc + , chr + ) + + fun deleteToStart (app: app_type) = + let + val {cursorIdx, buffer, windowWidth, windowHeight, searchString, ...} = + app + + val buffer = LineGap.delete (0, cursorIdx, buffer) + + val buffer = LineGap.goToEnd buffer + val initialMsg = [SEARCH (buffer, #searchString app)] + + val buffer = LineGap.goToIdx (cursorIdx + 777, buffer) + val searchList = + SearchList.buildRange (buffer, searchString, cursorIdx - 777) + + val cursorIdx = 0 + val startLine = 0 + val buffer = LineGap.goToIdx (cursorIdx, buffer) + + val drawMsg = TextBuilder.build + ( startLine + , cursorIdx + , buffer + , windowWidth + , windowHeight + , searchList + , searchString + , initialMsg + ) + + val mode = NORMAL_MODE "" + in + AppWith.bufferAndCursorIdx + (app, buffer, cursorIdx, mode, startLine, searchList, drawMsg) + end + + fun helpDeleteToMatch (app: app_type, low, high) = + let + val {buffer, searchString, ...} = app + val length = high - low + val buffer = LineGap.delete (low, length, buffer) + + val buffer = LineGap.goToEnd buffer + val initialMsg = [SEARCH (buffer, searchString)] + + val buffer = LineGap.goToIdx (low + 777, buffer) + val searchList = SearchList.buildRange (buffer, searchString, low - 777) + + val buffer = LineGap.goToIdx (low, buffer) + in + Finish.buildTextAndClear (app, buffer, low, searchList, initialMsg) + end + + fun deleteToNextMatch (app: app_type, count) = + let + val {cursorIdx, searchList, ...} = app + val newCursorIdx = SearchList.nextMatch (cursorIdx, searchList, count) + in + if newCursorIdx = ~1 orelse newCursorIdx <= cursorIdx then Finish.clearMode app + else helpDeleteToMatch (app, cursorIdx, newCursorIdx) + end + + fun deleteToPrevMatch (app: app_type, count) = + let + val {cursorIdx, searchList, ...} = app + val newCursorIdx = SearchList.prevMatch (cursorIdx, searchList, count) + in + if newCursorIdx = ~1 orelse newCursorIdx >= cursorIdx then Finish.clearMode app + else helpDeleteToMatch (app, newCursorIdx, cursorIdx) + end +end diff --git a/shf-tests.mlb b/shf-tests.mlb index 3202fea..c91ef0f 100644 --- a/shf-tests.mlb +++ b/shf-tests.mlb @@ -32,7 +32,10 @@ fcore/text-window.sml fcore/finish.sml fcore/move.sml + fcore/normal-mode/normal-move.sml +fcore/normal-mode/normal-delete.sml + fcore/app-update.sml diff --git a/shf.mlb b/shf.mlb index 128e5aa..9844036 100644 --- a/shf.mlb +++ b/shf.mlb @@ -32,7 +32,10 @@ fcore/text-window.sml fcore/finish.sml fcore/move.sml + fcore/normal-mode/normal-move.sml +fcore/normal-mode/normal-delete.sml + fcore/app-update.sml (* IMPERATIVE SHELL *)