Repository: alacritty/alacritty (language: rust)

Issue:
Semantic characters in Vi Mode and moving left
### System

OS: Linux 6.11.9 (arch)
Version: alacritty 0.14.0 (22a44757)
Sway 1:1.10-1  

```
[0.000000942s] [INFO ] [alacritty] Welcome to Alacritty
[0.000051958s] [INFO ] [alacritty] Version 0.14.0 (22a44757)
[0.000061095s] [INFO ] [alacritty] Running on Wayland
[0.000525696s] [INFO ] [alacritty] Configuration files loaded from:
                                     ".config/alacritty/alacritty.toml"
                                     ".config/alacritty/themes/themes/tokyo-night.toml"
[0.032020983s] [INFO ] [alacritty] Using EGL 1.5
[0.032067079s] [DEBUG] [alacritty] Picked GL Config:
                                     buffer_type: Some(Rgb { r_size: 8, g_size: 8, b_size: 8 })
                                     alpha_size: 8
                                     num_samples: 0
                                     hardware_accelerated: true
                                     supports_transparency: Some(true)
                                     config_api: Api(OPENGL | GLES1 | GLES2 | GLES3)
                                     srgb_capable: true
[0.035075269s] [INFO ] [alacritty] Window scale factor: 1
[0.036997955s] [DEBUG] [alacritty] Loading "JetBrains Mono" font
[0.048522765s] [DEBUG] [crossfont] Loaded Face Face { ft_face: Font Face: Regular, load_flags: LoadFlag(FORCE_AUTOHINT | TARGET_LIGHT | TARGET_MONO), render_mode: "Lcd", lcd_filter: 1 }
[0.052494282s] [DEBUG] [crossfont] Loaded Face Face { ft_face: Font Face: Italic, load_flags: LoadFlag(FORCE_AUTOHINT | TARGET_LIGHT | TARGET_MONO), render_mode: "Lcd", lcd_filter: 1 }
[0.056145538s] [DEBUG] [crossfont] Loaded Face Face { ft_face: Font Face: Bold Italic, load_flags: LoadFlag(FORCE_AUTOHINT | TARGET_LIGHT | TARGET_MONO), render_mode: "Lcd", lcd_filter: 1 }
[0.061237837s] [INFO ] [alacritty] Running on AMD Radeon Graphics (radeonsi, renoir, LLVM 18.1.8, DRM 3.59, 6.11.9-zen1-1-zen)
[0.061260339s] [INFO ] [alacritty] OpenGL version 4.6 (Core Profile) Mesa 24.2.7-arch1.1, shader_version 4.60
[0.061267753s] [INFO ] [alacritty] Using OpenGL 3.3 renderer
[0.068799656s] [DEBUG] [alacritty] Enabled debug logging for OpenGL
[0.070534168s] [DEBUG] [alacritty] Filling glyph cache with common glyphs
[0.080691745s] [INFO ] [alacritty] Cell size: 10 x 25
[0.080720799s] [INFO ] [alacritty] Padding: 0 x 0
[0.080725207s] [INFO ] [alacritty] Width: 800, Height: 600
[0.080761626s] [INFO ] [alacritty] PTY dimensions: 24 x 80
[0.084459059s] [INFO ] [alacritty] Initialisation complete
[0.105657768s] [INFO ] [alacritty] Font size changed to 18.666666 px
[0.105695439s] [INFO ] [alacritty] Cell size: 10 x 25
[0.105708323s] [DEBUG] [alacritty_terminal] New num_cols is 191 and num_lines is 40
[0.110029896s] [INFO ] [alacritty] Padding: 0 x 0
[0.110058470s] [INFO ] [alacritty] Width: 1916, Height: 1020
```
When in Vi Mode [Shift]+[Ctrl]+[Space], moving to the left with [B] "SemanticLeft" does not work for all semantic characters:

`this is a test－STAHP．STAHP—yes this is a test-move test•lets go`

In the above, the characters to the left of "STAHP" causes the cursor to... stop. Moving forward with [E] is fine. The only way to move past those characters is to use the cursor keys.

Please be so kind as to look into this if you have the time at hand to spare.

With humble thanks.


Discussion / hints:
I'd assume that you have those wide chars in semantic chars specified, since wide chars are not there by default.
Yes.

`[selection]

semantic_escape_chars = ",│`|:\"' ()[]{}<>\t•-—－．"
`
It seems like `b` isn't the only binding affected. The `w` binding also does not work correctly.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bbe0d2f4f0f..c9faa35c90b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ Notable changes to the `alacritty_terminal` crate are documented in its
 
 - Mouse/Vi cursor hint highlighting broken on the terminal cursor line
 - Hint launcher opening arbitrary text, when terminal content changed while opening
+- `SemanticRight`/`SemanticLeft` vi motions breaking with wide semantic escape characters
 
 ## 0.14.0
 
diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs
index 1f1e52c1cfb..c2d77ec764c 100644
--- a/alacritty_terminal/src/term/mod.rs
+++ b/alacritty_terminal/src/term/mod.rs
@@ -930,6 +930,11 @@ impl<T> Term<T> {
         &self.config.semantic_escape_chars
     }
 
+    #[cfg(test)]
+    pub(crate) fn set_semantic_escape_chars(&mut self, semantic_escape_chars: &str) {
+        self.config.semantic_escape_chars = semantic_escape_chars.into();
+    }
+
     /// Active terminal cursor style.
     ///
     /// While vi mode is active, this will automatically return the vi mode cursor style.
diff --git a/alacritty_terminal/src/vi_mode.rs b/alacritty_terminal/src/vi_mode.rs
index 7065544ea72..e23e9b80d96 100644
--- a/alacritty_terminal/src/vi_mode.rs
+++ b/alacritty_terminal/src/vi_mode.rs
@@ -265,14 +265,14 @@ fn semantic<T: EventListener>(
         }
     };
 
-    // Make sure we jump above wide chars.
-    point = term.expand_wide(point, direction);
-
     // Move to word boundary.
     if direction != side && !is_boundary(term, point, direction) {
         point = expand_semantic(point);
     }
 
+    // Make sure we jump above wide chars.
+    point = term.expand_wide(point, direction);
+
     // Skip whitespace.
     let mut next_point = advance(term, point, direction);
     while !is_boundary(term, point, direction) && is_space(term, next_point) {
@@ -283,6 +283,11 @@ fn semantic<T: EventListener>(
     // Assure minimum movement of one cell.
     if !is_boundary(term, point, direction) {
         point = advance(term, point, direction);
+
+        // Skip over wide cell spacers.
+        if direction == Direction::Left {
+            point = term.expand_wide(point, direction);
+        }
     }
 
     // Move to word boundary.
@@ -820,4 +825,42 @@ mod tests {
         cursor = cursor.scroll(&term, -20);
         assert_eq!(cursor.point, Point::new(Line(19), Column(0)));
     }
+
+    #[test]
+    fn wide_semantic_char() {
+        let mut term = term();
+        term.set_semantic_escape_chars("－");
+        term.grid_mut()[Line(0)][Column(0)].c = 'x';
+        term.grid_mut()[Line(0)][Column(1)].c = 'x';
+        term.grid_mut()[Line(0)][Column(2)].c = '－';
+        term.grid_mut()[Line(0)][Column(2)].flags.insert(Flags::WIDE_CHAR);
+        term.grid_mut()[Line(0)][Column(3)].c = ' ';
+        term.grid_mut()[Line(0)][Column(3)].flags.insert(Flags::WIDE_CHAR_SPACER);
+        term.grid_mut()[Line(0)][Column(4)].c = 'x';
+        term.grid_mut()[Line(0)][Column(5)].c = 'x';
+
+        // Test motion to the right.
+
+        let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
+        cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
+        assert_eq!(cursor.point, Point::new(Line(0), Column(2)));
+
+        let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(2)));
+        cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
+        assert_eq!(cursor.point, Point::new(Line(0), Column(4)));
+
+        // Test motion to the left.
+
+        let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(5)));
+        cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
+        assert_eq!(cursor.point, Point::new(Line(0), Column(4)));
+
+        let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(4)));
+        cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
+        assert_eq!(cursor.point, Point::new(Line(0), Column(2)));
+
+        let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(2)));
+        cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
+        assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+    }
 }


Test patch: