-
Notifications
You must be signed in to change notification settings - Fork 807
Fix VIM-2656: Incorrect shift motion behavior with tabs #1416
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
Conversation
When using tabs for indentation, the `>>` command was sometimes inserting partial indents (mixed tabs and spaces) instead of full tab-based indents. This occurred when lines had existing misaligned whitespace. The issue was in the indentRange method, which calculated new indent as `currentColumn + indentSize`. This preserved any misalignment from existing indentation, leading to positions that weren't multiples of tabSize, resulting in mixed tabs/spaces from createIndentBySize. The fix changes the logic to: 1. Calculate current indent depth: `currentDepth = col / indentSize` 2. Calculate new depth: `newDepth = currentDepth + shiftCount` 3. Create indent string: `createIndentByDepth(newDepth)` This ensures indents always align to proper indent boundaries, so when using tabs, `>>` always adds full tab indents rather than creating "half-indented" lines with mixed tabs and spaces. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
| } | ||
| } else { | ||
| // Shift non-blockwise selection | ||
| val indentSize = indentConfig.getIndentSize(1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good practice: Moving the indentSize calculation outside the loop is a nice optimization. This value is constant for all lines being shifted, so computing it once improves performance.
| val limit = max(0.0, (col + dir * indentConfig.getIndentSize(count)).toDouble()) | ||
| .toInt() | ||
| // Calculate the current indent depth (floor division) | ||
| val currentDepth = col / indentSize |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: Integer division here will always floor the result, which is correct for the fix. However, could you add a brief comment explaining why we use integer division (floor) rather than rounding? This would help future maintainers understand that misaligned indents should round down to the previous indent level before shifting.
Example: With indentSize=4 and col=2, we get currentDepth=0 (not 0.5 rounded), which is the intended behavior.
| @TestWithoutNeovim(SkipNeovimReason.NOT_VIM_TESTING) | ||
| @Test | ||
| fun `test shift right with existing misaligned spaces should use full tabs`() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent test: This is the key test case that demonstrates the bug fix. Starting with 2 spaces of misaligned indent and expecting it to round to 1 full tab (not 1 tab + 2 spaces) is exactly what VIM-2656 was about.
Suggestion for additional edge case: Consider adding a test where TAB_SIZE != INDENT_SIZE (e.g., TAB_SIZE=8, INDENT_SIZE=4). This is a valid configuration and would verify the fix works correctly when indent depth and tab rendering differ.
| javaSettings.TAB_SIZE = 4 | ||
| javaSettings.INDENT_SIZE = 4 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test coverage note: All tests use TAB_SIZE = 4 and INDENT_SIZE = 4. While this covers the common case, consider adding at least one test with different values (e.g., TAB_SIZE=8, INDENT_SIZE=4) to ensure the fix works correctly when these values differ.
This configuration is valid and used in some codebases where you want 4-space indent levels but tabs render as 8 spaces.
PR Review SummaryThis is a well-implemented fix for VIM-2656. The approach is sound, and the code changes are minimal and focused. Here's my detailed assessment: ✅ Strengths
🔍 Areas for Improvement1. Missing Test Edge CaseAll tests use 2. Code Comment for Integer DivisionAt vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroupBase.kt:2008, the integer division behavior (floor) is critical to the fix. A brief comment would help future maintainers understand why misaligned indents round down before shifting. 3. Shift Left CoverageThe PR only adds tests for shift right ( 🛡️ Quality Assessment
📋 RecommendationAPPROVE with minor suggestions. The core fix is correct and well-tested. The suggested improvements are nice-to-haves that would make the PR even stronger, but they don't block merging. The fix correctly implements Vim's behavior of normalizing misaligned indents to proper boundaries when shifting, which is exactly what VIM-2656 requested. |
Summary
Fixes VIM-2656:
>>command now correctly adds full tab indents instead of creating mixed tabs/spaces when using tab-based indentation.Problem
When projects used tabs for indentation, the
>>command would sometimes insert partial indents (mixed tabs and spaces) instead of full tab-based indents. This occurred when lines had existing misaligned whitespace.For example, with 2 spaces of existing indent,
>>would produce 1 tab + 2 spaces instead of rounding to 1 full tab.Root Cause
The
indentRangemethod inVimChangeGroupBase.ktcalculated new indent ascurrentColumn + indentSize, which preserved any misalignment from existing indentation. This resulted in column positions that weren't multiples oftabSize, causingcreateIndentBySizeto generate mixed tabs and spaces.Solution
Changed the indentation logic to work with indent depth (levels) instead of raw column positions:
currentDepth = col / indentSize(floor division)newDepth = currentDepth + shiftCountcreateIndentByDepth(newDepth)This ensures indents always align to proper indent boundaries, so when using tabs,
>>always adds full tab indents rather than creating "half-indented" lines.Testing
ShiftRightTabsTest.ktwith tests for tab-based indentationTest plan
🤖 Generated with Claude Code