Skip to content

fix: handle tab on tmux next3.6 #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
.env
__pycache__
.keystroke

**/.claude/settings.local.json
57 changes: 57 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

tmux-easymotion is a tmux plugin inspired by vim-easymotion that provides a quick way to navigate and jump between positions in tmux panes. The key feature is the ability to jump between panes, not just within a single pane.

## Code Architecture

- **easymotion.tmux**: Main shell script that sets up the tmux key bindings and configuration options
- **easymotion.py**: Python implementation of the easymotion functionality
- Uses two display methods: ANSI sequences or curses
- Implements a hints system to quickly navigate to characters
- Handles smart matching features like case sensitivity and smartsign

## Key Concepts

1. **Hint Generation**: Creates single or double character hints for navigation
2. **Smart Matching**: Supports case-insensitive matching and "smartsign" (matching symbol pairs)
3. **Pane Navigation**: Can jump between panes, not just within one pane
4. **Visual Width Handling**: Properly handles wide characters (CJK, etc.)

## Running Tests

To run the tests:

```bash
pytest test_easymotion.py -v --cache-clear
```

## Configuration Options

The plugin supports several configuration options set in tmux.conf:

- Hint characters
- Border style
- Display method (ANSI or curses)
- Case sensitivity
- Smartsign feature
- Debug and performance logging

## Common Development Tasks

When working on this plugin, you may need to:

1. Debug the easymotion behavior by enabling debug logging:
```
set -g @easymotion-debug 'true'
```

2. Measure performance using the perf logging option:
```
set -g @easymotion-perf 'true'
```

Both debug and perf logs are written to `~/easymotion.log`.
40 changes: 30 additions & 10 deletions easymotion.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,24 +214,38 @@ def wrapper(*args, **kwargs):
return decorator


def calculate_tab_width(position: int, tab_size: int = 8) -> int:
"""Calculate the visual width of a tab based on its position"""
return tab_size - (position % tab_size)

@functools.lru_cache(maxsize=1024)
def get_char_width(char: str) -> int:
"""Get visual width of a single character with caching"""
def get_char_width(char: str, position: int = 0) -> int:
"""Get visual width of a single character with caching

Args:
char: The character to measure
position: The visual position of the character (needed for tabs)
Comment on lines +223 to +227
Copy link
Preview

Copilot AI May 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] It may be beneficial to update the docstring of get_char_width to explicitly mention how the position parameter affects tab width calculation and caching behavior.

Suggested change
"""Get visual width of a single character with caching
Args:
char: The character to measure
position: The visual position of the character (needed for tabs)
"""Get visual width of a single character with caching.
This function calculates the visual width of a character, taking into account
special cases like tab characters and wide East Asian characters. The result
is cached to improve performance for repeated calls with the same arguments.
Args:
char: The character to measure.
position: The visual position of the character in the line. This is used
to calculate the width of tab characters (`\t`) based on their position,
as tab width depends on alignment. Different `position` values will
result in different cache keys, which can affect performance and memory
usage.
Returns:
The visual width of the character.

Copilot uses AI. Check for mistakes.

"""
if char == '\t':
return calculate_tab_width(position)
return 2 if unicodedata.east_asian_width(char) in 'WF' else 1


@functools.lru_cache(maxsize=1024)
def get_string_width(s: str) -> int:
"""Calculate visual width of string, accounting for double-width characters"""
return sum(map(get_char_width, s))
"""Calculate visual width of string, accounting for double-width characters and tabs"""
visual_pos = 0
for char in s:
visual_pos += get_char_width(char, visual_pos)
return visual_pos


def get_true_position(line, target_col):
"""Calculate true position accounting for wide characters"""
"""Calculate true position accounting for wide characters and tabs"""
visual_pos = 0
true_pos = 0
while true_pos < len(line) and visual_pos < target_col:
char_width = get_char_width(line[true_pos])
char_width = get_char_width(line[true_pos], visual_pos)
visual_pos += char_width
true_pos += 1
return true_pos
Expand Down Expand Up @@ -547,12 +561,18 @@ def find_matches(panes, search_ch):
for ch in search_chars:
if CASE_SENSITIVE:
if pos < len(line) and line[pos] == ch:
visual_col = sum(get_char_width(c) for c in line[:pos])
matches.append((pane, line_num, visual_col))
# Calculate visual position accounting for tab width based on position
visual_pos = 0
for i in range(pos):
visual_pos += get_char_width(line[i], visual_pos)
matches.append((pane, line_num, visual_pos))
else:
if pos < len(line) and line[pos].lower() == ch.lower():
visual_col = sum(get_char_width(c) for c in line[:pos])
matches.append((pane, line_num, visual_col))
# Calculate visual position accounting for tab width based on position
visual_pos = 0
for i in range(pos):
visual_pos += get_char_width(line[i], visual_pos)
matches.append((pane, line_num, visual_pos))

return matches

Expand Down
16 changes: 15 additions & 1 deletion test_easymotion.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from easymotion import (generate_hints, get_char_width, get_string_width,
get_true_position)
get_true_position, calculate_tab_width)


def test_get_char_width():
Expand All @@ -9,6 +9,9 @@ def test_get_char_width():
assert get_char_width('한') == 2 # Korean character (wide)
assert get_char_width(' ') == 1 # Space
assert get_char_width('\n') == 1 # Newline
assert get_char_width('\t', 0) == 8 # Tab at position 0
assert get_char_width('\t', 1) == 7 # Tab at position 1
assert get_char_width('\t', 7) == 1 # Tab at position 7


def test_get_string_width():
Expand All @@ -17,12 +20,23 @@ def test_get_string_width():
assert get_string_width('hello こんにちは') == 16
assert get_string_width('') == 0

# Need to manually calculate tab width examples to match our implementation
assert get_string_width('\t') == 8 # Tab at position 0 = 8 spaces
assert get_string_width('a\t') == 8 # 'a' (1) + Tab at position 1 (7) = 8
assert get_string_width('1234567\t') == 8 # 7 chars + Tab at position 7 (1) = 8
assert get_string_width('a\tb\t') == 16 # 'a' (1) + Tab at position 1 (7) + 'b' (1) + Tab at position 9=1 (7) = 16


def test_get_true_position():
assert get_true_position('hello', 3) == 3
assert get_true_position('あいうえお', 4) == 2
assert get_true_position('hello あいうえお', 7) == 7
assert get_true_position('', 5) == 0
assert get_true_position('\t', 4) == 1 # Halfway through tab
assert get_true_position('\t', 8) == 1 # Full tab width
assert get_true_position('a\tb', 1) == 1 # 'a'
assert get_true_position('a\tb', 5) == 2 # After 'a', halfway through tab
assert get_true_position('a\tb', 9) == 3 # 'b'


def test_generate_hints():
Expand Down