diff --git a/.gitignore b/.gitignore index 2a6e8ee..edde333 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .env __pycache__ .keystroke + +**/.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..73e1125 --- /dev/null +++ b/CLAUDE.md @@ -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`. \ No newline at end of file diff --git a/easymotion.py b/easymotion.py index c6a44c9..5c06961 100755 --- a/easymotion.py +++ b/easymotion.py @@ -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) + """ + 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 @@ -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 diff --git a/test_easymotion.py b/test_easymotion.py index adc530b..ac98c7c 100644 --- a/test_easymotion.py +++ b/test_easymotion.py @@ -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(): @@ -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(): @@ -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():