Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 +271 to +275
Copy link

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