Implemented the replace (r and R) actions and replace mode #5

Merged
azpect merged 3 commits from feature/replace into master 2026-04-06 18:30:37 -07:00
Owner

PR Type

Enhancement, Bug fix, Tests, Documentation


Description

  • Implemented r (replace character) and R (replace mode) actions.

  • Added comprehensive integration tests for replace functionalities.

  • Introduced ReplaceMode with dedicated key bindings and cursor styling.

  • Corrected cursor positioning for end-of-line motions.


Diagram Walkthrough

flowchart LR
  NormalMode -- "Press 'r' + char" --> ReplaceCharAction["Replace Single Character"]
  NormalMode -- "Press 'R'" --> ReplaceMode
  ReplaceMode -- "Type character" --> ReplaceModeChar["Overwrite Character"]
  ReplaceMode -- "Press 'Enter'" --> ReplaceNewline["Split Line"]
  ReplaceMode -- "Press 'Tab'" --> ReplaceTab["Insert Tab Spaces"]
  ReplaceMode -- "Press 'Backspace' / 'Delete' / 'Ctrl+W'" --> ReplaceModeEditActions["Edit Current Line"]
  ReplaceMode -- "Press 'Esc'" --> NormalMode

File Walkthrough

Relevant files
Enhancement
5 files
replace.go
Introduced actions for single character replacement and replace mode
+129/-0 
mode.go
Added `ReplaceMode` to the editor's mode enumeration         
+5/-1     
handler.go
Implemented `handleReplaceKey` to process input in replace mode
+49/-3   
keymap.go
Defined key bindings for `ReplaceMode` actions and motions
+23/-0   
style.go
Added distinct cursor styling for `ReplaceMode` and `WaitingMode`
+14/-0   
Tests
2 files
helpers_test.go
Expanded `sendKeys` helper with navigation and tab keys for testing
+10/-0   
integration_replace_test.go
Added extensive integration tests for 'r' and 'R' replace
functionalities
+1040/-0
Bug fix
3 files
integration_motion_jump_test.go
Fixed cursor position expectation for '$' motion in tests
+3/-3     
integration_visual_test.go
Adjusted cursor position expectation for 'v$' in visual mode tests
+2/-3     
jump.go
Corrected `MoveToLineEnd` to position cursor on the last character
+1/-1     
Cleanup
1 files
model_builder.go
Removed commented-out list of unused Chroma styles             
+1/-70   
Documentation
2 files
FEATURES.md
Updated feature list to mark 'r' and 'R' actions as implemented
+2/-2     
README.md
Added a section explaining the design choices for replace mode
+8/-0     

### **PR Type** Enhancement, Bug fix, Tests, Documentation ___ ### **Description** - Implemented `r` (replace character) and `R` (replace mode) actions. - Added comprehensive integration tests for replace functionalities. - Introduced `ReplaceMode` with dedicated key bindings and cursor styling. - Corrected cursor positioning for end-of-line motions. ___ ### Diagram Walkthrough ```mermaid flowchart LR NormalMode -- "Press 'r' + char" --> ReplaceCharAction["Replace Single Character"] NormalMode -- "Press 'R'" --> ReplaceMode ReplaceMode -- "Type character" --> ReplaceModeChar["Overwrite Character"] ReplaceMode -- "Press 'Enter'" --> ReplaceNewline["Split Line"] ReplaceMode -- "Press 'Tab'" --> ReplaceTab["Insert Tab Spaces"] ReplaceMode -- "Press 'Backspace' / 'Delete' / 'Ctrl+W'" --> ReplaceModeEditActions["Edit Current Line"] ReplaceMode -- "Press 'Esc'" --> NormalMode ``` <details> <summary><h3> File Walkthrough</h3></summary> <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><details><summary>5 files</summary><table> <tr> <td><strong>replace.go</strong><dd><code>Introduced actions for single character replacement and replace mode</code></dd></td> <td><a href="https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/action/replace.go">+129/-0</a>&nbsp; </td> </tr> <tr> <td><strong>mode.go</strong><dd><code>Added `ReplaceMode` to the editor's mode enumeration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/core/mode.go">+5/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>handler.go</strong><dd><code>Implemented `handleReplaceKey` to process input in replace mode</code></dd></td> <td><a href="https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/input/handler.go">+49/-3</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>keymap.go</strong><dd><code>Defined key bindings for `ReplaceMode` actions and motions</code></dd></td> <td><a href="https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/input/keymap.go">+23/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>style.go</strong><dd><code>Added distinct cursor styling for `ReplaceMode` and `WaitingMode`</code></dd></td> <td><a href="https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/style/style.go">+14/-0</a>&nbsp; &nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Tests</strong></td><td><details><summary>2 files</summary><table> <tr> <td><strong>helpers_test.go</strong><dd><code>Expanded `sendKeys` helper with navigation and tab keys for testing</code></dd></td> <td><a href="https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/editor/helpers_test.go">+10/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>integration_replace_test.go</strong><dd><code>Added extensive integration tests for 'r' and 'R' replace </code><br><code>functionalities</code></dd></td> <td><a href="https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/editor/integration_replace_test.go">+1040/-0</a></td> </tr> </table></details></td></tr><tr><td><strong>Bug fix</strong></td><td><details><summary>3 files</summary><table> <tr> <td><strong>integration_motion_jump_test.go</strong><dd><code>Fixed cursor position expectation for '$' motion in tests</code></dd></td> <td><a href="https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/editor/integration_motion_jump_test.go">+3/-3</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>integration_visual_test.go</strong><dd><code>Adjusted cursor position expectation for 'v$' in visual mode tests</code></dd></td> <td><a href="https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/editor/integration_visual_test.go">+2/-3</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>jump.go</strong><dd><code>Corrected `MoveToLineEnd` to position cursor on the last character</code></dd></td> <td><a href="https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/motion/jump.go">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Cleanup</strong></td><td><details><summary>1 files</summary><table> <tr> <td><strong>model_builder.go</strong><dd><code>Removed commented-out list of unused Chroma styles</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td> <td><a href="https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/editor/model_builder.go">+1/-70</a>&nbsp; &nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Documentation</strong></td><td><details><summary>2 files</summary><table> <tr> <td><strong>FEATURES.md</strong><dd><code>Updated feature list to mark 'r' and 'R' actions as implemented</code></dd></td> <td><a href="https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/FEATURES.md">+2/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong><dd><code>Added a section explaining the design choices for replace mode</code></dd></td> <td><a href="https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/README.md">+8/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> </table></details></td></tr></tr></tbody></table> </details> ___
azpect added 2 commits 2026-04-05 23:59:56 -07:00
feat: implement the r action, tested
All checks were successful
Run Test Suite / test (push) Successful in 17s
6033e58d0e
Began work on replace mode, but not complete.
feat: implemented replace mode, tested!
All checks were successful
Run Test Suite / test (push) Successful in 15s
Run Test Suite / test (pull_request) Successful in 15s
43b3992522
Looking great, maybe I will actually use this lol
Author
Owner

/review

/review
Collaborator

PR Reviewer Guide 🔍

(Review updated until commit 066b817200)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵
🧪 PR contains tests
🔒 No security concerns identified
 Recommended focus areas for review

Undo Behavior

The handleReplaceKey function incorrectly calls buf.UndoStack.BeginBlock(win.Cursor) for every character typed in ReplaceMode if an undo block is not already recording. This means each character typed will be a separate undoable action, rather than the entire ReplaceMode session being a single undoable block. This deviates from the expected behavior where a single R command (and subsequent typing) should be undoable as one unit. The BeginBlock should ideally be called once when entering ReplaceMode (e.g., by the EnterReplace action), and EndBlock when exiting ReplaceMode (e.g., when 'Esc' is pressed).

if buf.UndoStack != nil && !buf.UndoStack.Recording() {
	buf.UndoStack.BeginBlock(win.Cursor)
}
## PR Reviewer Guide 🔍 #### (Review updated until commit https://git.gophernest.net/azpect/Gim/commit/066b817200f6ab30f3225f66eef1df7d6de66546) Here are some key observations to aid the review process: <table> <tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 3 🔵🔵🔵⚪⚪</td></tr> <tr><td>🧪&nbsp;<strong>PR contains tests</strong></td></tr> <tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr> <tr><td>⚡&nbsp;<strong>Recommended focus areas for review</strong><br><br> <details><summary><a href='https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/input/handler.go#L582-L584'><strong>Undo Behavior</strong></a> The `handleReplaceKey` function incorrectly calls `buf.UndoStack.BeginBlock(win.Cursor)` for every character typed in `ReplaceMode` if an undo block is not already recording. This means each character typed will be a separate undoable action, rather than the entire `ReplaceMode` session being a single undoable block. This deviates from the expected behavior where a single `R` command (and subsequent typing) should be undoable as one unit. The `BeginBlock` should ideally be called once when entering `ReplaceMode` (e.g., by the `EnterReplace` action), and `EndBlock` when exiting `ReplaceMode` (e.g., when 'Esc' is pressed). </summary> ```go if buf.UndoStack != nil && !buf.UndoStack.Recording() { buf.UndoStack.BeginBlock(win.Cursor) } ``` </details> </td></tr> </table>
Collaborator

Persistent review updated to latest commit 066b817200

**[Persistent review](https://git.gophernest.net/azpect/Gim/pulls/5#issuecomment-891)** updated to latest commit https://git.gophernest.net/azpect/Gim/commit/066b817200f6ab30f3225f66eef1df7d6de66546
Author
Owner

/improve

/improve
Collaborator

PR Code Suggestions

Latest suggestions up to 066b817

CategorySuggestion                                                                                                                                    Impact
Possible issue
Use rune-aware string manipulation

Go strings are UTF-8 encoded, so direct byte-based slicing (line[:pos],
line[pos+1:]) and len(line) can corrupt multi-byte characters. Convert the line to
[]rune for character-accurate manipulation, then convert back to string. This
ensures correct behavior with Unicode characters across ReplaceChar,
ReplaceModeChar, ReplaceNewline, and ReplaceTab actions.

internal/action/replace.go [38-44]

 	pos := win.Cursor.Col
-	line := buf.Line(win.Cursor.Line)
-	for i := 0; i < a.Count && pos < len(line); i++ {
-		line = line[:pos] + a.Char + line[pos+1:]
-		buf.SetLine(win.Cursor.Line, line)
+	runes := []rune(buf.Line(win.Cursor.Line))
+	charRunes := []rune(a.Char)
+	if len(charRunes) == 0 {
+		return nil // Handle empty char replacement gracefully
+	}
+	for i := 0; i < a.Count && pos < len(runes); i++ {
+		runes[pos] = charRunes[0] // Replace with the first rune of a.Char
+		buf.SetLine(win.Cursor.Line, string(runes))
 		pos++
 	}
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical issue with byte-based string manipulation in Go when dealing with Unicode characters. Converting to []rune ensures character-accurate replacement, which is essential for a text editor.

High
Ensure cursor calculations are rune-aware

Cursor column positions and calculations, especially when using len(string) for
character counts, should be based on rune counts, not byte lengths. This ensures
correct visual positioning and movement when multi-byte Unicode characters are
present. This also applies to ReplaceTab.Execute and MoveToLineEnd.Execute.

internal/action/replace.go [87]

-	win.SetCursorCol(x + len(a.Char))
+	win.SetCursorCol(x + len([]rune(a.Char)))
Suggestion importance[1-10]: 9

__

Why: Using len(string) for cursor column calculations can lead to incorrect positioning with multi-byte Unicode characters. The suggestion correctly proposes using len([]rune(a.Char)) for accurate character counting, which is crucial for correct visual feedback.

High
Correct tab key behavior

The ReplaceTab action currently replaces a single character with multiple spaces.
Given the TODO in keymap.go and common editor behavior, a tab in replace mode should
typically insert spaces, shifting subsequent characters, rather than overwriting
just one. Adjust the logic to perform an insertion.

internal/action/replace.go [121-127]

 	tabs := strings.Repeat(" ", m.Settings().TabStop)
-	if x < len(l) {
-		buf.SetLine(y, l[:x]+tabs+l[x+1:])
-	} else {
-		buf.SetLine(y, l+tabs)
-	}
-	win.SetCursorCol(x + len(tabs))
+	runes := []rune(buf.Line(y))
+	newRunes := make([]rune, 0, len(runes)+len([]rune(tabs)))
+	newRunes = append(newRunes, runes[:x]...)
+	newRunes = append(newRunes, []rune(tabs)...)
+	newRunes = append(newRunes, runes[x:]...) // Insert remaining part of the line
+	buf.SetLine(y, string(newRunes))
+	win.SetCursorCol(x + len([]rune(tabs)))
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly points out that the ReplaceTab action should insert spaces rather than overwrite a single character, aligning with standard editor behavior and the TODO comment in keymap.go. The improved_code also correctly uses rune-aware operations.

Medium

Previous suggestions

Suggestions up to commit 066b817
CategorySuggestion                                                                                                                                    Impact
Possible issue
Optimize character replacement and fix cursor position

The current implementation of ReplaceChar.Execute uses inefficient string
concatenations and repeated buf.SetLine calls within a loop, which can be slow for
large counts or long lines. Additionally, win.SetCursorCol(pos - 1) can result in a
negative column if no characters were replaced (e.g., on an empty line or when
a.Count is 0). Optimize the string manipulation by converting the line to a []rune
slice, modifying it once, and ensure the cursor is only moved if replacements
occurred, landing on the last replaced character.

internal/action/replace.go [38-46]

-	pos := win.Cursor.Col
-	line := buf.Line(win.Cursor.Line)
-	for i := 0; i < a.Count && pos < len(line); i++ {
-		line = line[:pos] + a.Char + line[pos+1:]
-		buf.SetLine(win.Cursor.Line, line)
-		pos++
+	initialCol := win.Cursor.Col
+	lineRunes := []rune(buf.Line(win.Cursor.Line))
+	charRunes := []rune(a.Char)
+	if len(charRunes) == 0 {
+		// If replacement char is empty, do nothing.
+		m.SetMode(core.NormalMode)
+		return nil
+	}
+	replaceRune := charRunes[0]
+
+	replacedCount := 0
+	// Iterate up to a.Count or line end, whichever comes first
+	for i := 0; i < a.Count && initialCol+i < len(lineRunes); i++ {
+		lineRunes[initialCol+i] = replaceRune
+		replacedCount++
 	}
 
-	win.SetCursorCol(pos - 1)
+	if replacedCount > 0 {
+		buf.SetLine(win.Cursor.Line, string(lineRunes))
+		win.SetCursorCol(initialCol + replacedCount - 1)
+	}
+	// If replacedCount is 0, cursor stays at initialCol, which is correct.
 
+	m.SetMode(core.NormalMode)
+
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies performance issues with string concatenations and repeated buf.SetLine calls. It also fixes a potential bug where win.SetCursorCol(pos - 1) could result in a negative column or incorrect position in edge cases.

High
Fix repeat command behavior in replace mode

The handleReplaceKey function currently records individual keys into InsertKeys. For
replace mode (R), the . command should repeat the entire sequence of characters
typed during the R session, not just the last character. Using InsertKeys for this
purpose will lead to incorrect repeat behavior. A dedicated mechanism is needed to
store and replay the full replace mode input for the . command.

internal/input/handler.go [588-590]

-	// Record the key for count replay (e.g. 5i...)
-	m.SetInsertKeys(append(m.InsertKeys(), key))
-	m.SetLastChangeKeys(append(m.LastChangeKeys(), key))
+	// TODO: Implement a dedicated mechanism for recording replace mode input for '.' command.
+	// The current use of m.SetInsertKeys will lead to incorrect repeat behavior for 'R' mode.
+	// m.SetLastChangeKeys(append(m.LastChangeKeys(), key)) // Keep if LastChangeKeys is a generic last change tracker.
Suggestion importance[1-10]: 8

__

Why: The suggestion accurately points out that using m.SetInsertKeys for replace mode will not correctly implement the Vim-like . command, which should repeat the entire sequence of characters typed in R mode. This is a crucial behavioral detail for a Vim-inspired editor.

Medium
## PR Code Suggestions ✨ <!-- 066b817 --> Latest suggestions up to 066b817 <table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=3>Possible issue</td> <td> <details><summary>Use rune-aware string manipulation</summary> ___ **Go strings are UTF-8 encoded, so direct byte-based slicing (<code>line[:pos]</code>, <br><code>line[pos+1:]</code>) and <code>len(line)</code> can corrupt multi-byte characters. Convert the line to <br><code>[]rune</code> for character-accurate manipulation, then convert back to <code>string</code>. This <br>ensures correct behavior with Unicode characters across <code>ReplaceChar</code>, <br><code>ReplaceModeChar</code>, <code>ReplaceNewline</code>, and <code>ReplaceTab</code> actions.** [internal/action/replace.go [38-44]](https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/action/replace.go#L38-L44) ```diff pos := win.Cursor.Col - line := buf.Line(win.Cursor.Line) - for i := 0; i < a.Count && pos < len(line); i++ { - line = line[:pos] + a.Char + line[pos+1:] - buf.SetLine(win.Cursor.Line, line) + runes := []rune(buf.Line(win.Cursor.Line)) + charRunes := []rune(a.Char) + if len(charRunes) == 0 { + return nil // Handle empty char replacement gracefully + } + for i := 0; i < a.Count && pos < len(runes); i++ { + runes[pos] = charRunes[0] // Replace with the first rune of a.Char + buf.SetLine(win.Cursor.Line, string(runes)) pos++ } ``` <details><summary>Suggestion importance[1-10]: 9</summary> __ Why: The suggestion correctly identifies a critical issue with byte-based string manipulation in Go when dealing with Unicode characters. Converting to `[]rune` ensures character-accurate replacement, which is essential for a text editor. </details></details></td><td align=center>High </td></tr><tr><td> <details><summary>Ensure cursor calculations are rune-aware</summary> ___ **Cursor column positions and calculations, especially when using <code>len(string)</code> for <br>character counts, should be based on rune counts, not byte lengths. This ensures <br>correct visual positioning and movement when multi-byte Unicode characters are <br>present. This also applies to <code>ReplaceTab.Execute</code> and <code>MoveToLineEnd.Execute</code>.** [internal/action/replace.go [87]](https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/action/replace.go#L87-L87) ```diff - win.SetCursorCol(x + len(a.Char)) + win.SetCursorCol(x + len([]rune(a.Char))) ``` <details><summary>Suggestion importance[1-10]: 9</summary> __ Why: Using `len(string)` for cursor column calculations can lead to incorrect positioning with multi-byte Unicode characters. The suggestion correctly proposes using `len([]rune(a.Char))` for accurate character counting, which is crucial for correct visual feedback. </details></details></td><td align=center>High </td></tr><tr><td> <details><summary>Correct tab key behavior</summary> ___ **The <code>ReplaceTab</code> action currently replaces a single character with multiple spaces. <br>Given the <code>TODO</code> in <code>keymap.go</code> and common editor behavior, a tab in replace mode should <br>typically *insert* spaces, shifting subsequent characters, rather than overwriting <br>just one. Adjust the logic to perform an insertion.** [internal/action/replace.go [121-127]](https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/action/replace.go#L121-L127) ```diff tabs := strings.Repeat(" ", m.Settings().TabStop) - if x < len(l) { - buf.SetLine(y, l[:x]+tabs+l[x+1:]) - } else { - buf.SetLine(y, l+tabs) - } - win.SetCursorCol(x + len(tabs)) + runes := []rune(buf.Line(y)) + newRunes := make([]rune, 0, len(runes)+len([]rune(tabs))) + newRunes = append(newRunes, runes[:x]...) + newRunes = append(newRunes, []rune(tabs)...) + newRunes = append(newRunes, runes[x:]...) // Insert remaining part of the line + buf.SetLine(y, string(newRunes)) + win.SetCursorCol(x + len([]rune(tabs))) ``` <details><summary>Suggestion importance[1-10]: 8</summary> __ Why: This suggestion correctly points out that the `ReplaceTab` action should insert spaces rather than overwrite a single character, aligning with standard editor behavior and the `TODO` comment in `keymap.go`. The `improved_code` also correctly uses rune-aware operations. </details></details></td><td align=center>Medium </td></tr></tr></tbody></table> ___ #### Previous suggestions <details><summary>Suggestions up to commit 066b817</summary> <br><table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=2>Possible issue</td> <td> <details><summary>Optimize character replacement and fix cursor position</summary> ___ **The current implementation of <code>ReplaceChar.Execute</code> uses inefficient string <br>concatenations and repeated <code>buf.SetLine</code> calls within a loop, which can be slow for <br>large counts or long lines. Additionally, <code>win.SetCursorCol(pos - 1)</code> can result in a <br>negative column if no characters were replaced (e.g., on an empty line or when <br><code>a.Count</code> is 0). Optimize the string manipulation by converting the line to a <code>[]rune</code> <br>slice, modifying it once, and ensure the cursor is only moved if replacements <br>occurred, landing on the last replaced character.** [internal/action/replace.go [38-46]](https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/action/replace.go#L38-L46) ```diff - pos := win.Cursor.Col - line := buf.Line(win.Cursor.Line) - for i := 0; i < a.Count && pos < len(line); i++ { - line = line[:pos] + a.Char + line[pos+1:] - buf.SetLine(win.Cursor.Line, line) - pos++ + initialCol := win.Cursor.Col + lineRunes := []rune(buf.Line(win.Cursor.Line)) + charRunes := []rune(a.Char) + if len(charRunes) == 0 { + // If replacement char is empty, do nothing. + m.SetMode(core.NormalMode) + return nil + } + replaceRune := charRunes[0] + + replacedCount := 0 + // Iterate up to a.Count or line end, whichever comes first + for i := 0; i < a.Count && initialCol+i < len(lineRunes); i++ { + lineRunes[initialCol+i] = replaceRune + replacedCount++ } - win.SetCursorCol(pos - 1) + if replacedCount > 0 { + buf.SetLine(win.Cursor.Line, string(lineRunes)) + win.SetCursorCol(initialCol + replacedCount - 1) + } + // If replacedCount is 0, cursor stays at initialCol, which is correct. + m.SetMode(core.NormalMode) + ``` <details><summary>Suggestion importance[1-10]: 9</summary> __ Why: The suggestion correctly identifies performance issues with string concatenations and repeated `buf.SetLine` calls. It also fixes a potential bug where `win.SetCursorCol(pos - 1)` could result in a negative column or incorrect position in edge cases. </details></details></td><td align=center>High </td></tr><tr><td> <details><summary>Fix repeat command behavior in replace mode</summary> ___ **The <code>handleReplaceKey</code> function currently records individual keys into <code>InsertKeys</code>. For <br>replace mode (<code>R</code>), the <code>.</code> command should repeat the *entire sequence* of characters <br>typed during the <code>R</code> session, not just the last character. Using <code>InsertKeys</code> for this <br>purpose will lead to incorrect repeat behavior. A dedicated mechanism is needed to <br>store and replay the full replace mode input for the <code>.</code> command.** [internal/input/handler.go [588-590]](https://git.gophernest.net/azpect/Gim/src/branch/feature/replace/internal/input/handler.go#L588-L590) ```diff - // Record the key for count replay (e.g. 5i...) - m.SetInsertKeys(append(m.InsertKeys(), key)) - m.SetLastChangeKeys(append(m.LastChangeKeys(), key)) + // TODO: Implement a dedicated mechanism for recording replace mode input for '.' command. + // The current use of m.SetInsertKeys will lead to incorrect repeat behavior for 'R' mode. + // m.SetLastChangeKeys(append(m.LastChangeKeys(), key)) // Keep if LastChangeKeys is a generic last change tracker. ``` <details><summary>Suggestion importance[1-10]: 8</summary> __ Why: The suggestion accurately points out that using `m.SetInsertKeys` for replace mode will not correctly implement the Vim-like `.` command, which should repeat the entire sequence of characters typed in `R` mode. This is a crucial behavioral detail for a Vim-inspired editor. </details></details></td><td align=center>Medium </td></tr></tr></tbody></table> </details>
azpect added 1 commit 2026-04-06 18:29:54 -07:00
doc: documented trade off regarding last insert tracking
All checks were successful
Run Test Suite / test (push) Successful in 17s
Run Test Suite / test (pull_request) Successful in 15s
881a4d6d78
azpect merged commit d6323be62b into master 2026-04-06 18:30:37 -07:00
azpect deleted branch feature/replace 2026-04-06 18:30:37 -07:00
Sign in to join this conversation.
No description provided.