Skip to content

feat: add g_ motion (move to last non-blank character)#262

Open
legalcodes wants to merge 1 commit intoreplit:masterfrom
legalcodes:legalcodes/add-g-underscore-motion
Open

feat: add g_ motion (move to last non-blank character)#262
legalcodes wants to merge 1 commit intoreplit:masterfrom
legalcodes:legalcodes/add-g-underscore-motion

Conversation

@legalcodes
Copy link
Copy Markdown

@legalcodes legalcodes commented Mar 29, 2026

Manual Review

I tested using this motion in the replit link -- it worked.

Adds the g_ motion, which moves the cursor to the last non-whitespace character on the line.

Behavior

  • g_ moves to the last non-blank character of the current line
  • 2g_ moves to the last non-blank of the next line (count - 1 lines down)
  • Works with operators: dg_ deletes to the last non-blank, preserving trailing whitespace
  • Inclusive motion, matching vim's native behavior

This is the counterpart to ^ (first non-blank), which is already implemented. Currently g_ is unmapped and does nothing.

Implementation

  • New helper findLastNonWhiteSpaceCharacter() mirrors the existing findFirstNonWhiteSpaceCharacter()
  • New motion moveToLastNonWhiteSpaceCharacter registered with inclusive: true
  • Tests cover basic movement, no-trailing-whitespace lines, count support, and operator interaction

Test plan

  • Added automated tests in vim_test.js
  • Manual verification via web demo

Open web-demo @ d6eeff49181ef1b2a41ff8f40aba9e6ee427cc96

Implements the g_ motion from vim, which moves to the last
non-whitespace character on the line. Supports counts (e.g. 2g_
moves to the last non-blank of the next line) and works correctly
with operators (dg_ deletes to last non-blank, preserving trailing
whitespace).
Copilot AI review requested due to automatic review settings March 29, 2026 04:04
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds Vim’s g_ motion to move the cursor to the last non-blank character on a line (inclusive), including count support and operator compatibility, aligning behavior with existing whitespace-aware motions.

Changes:

  • Register g_ as an inclusive motion mapped to moveToLastNonWhiteSpaceCharacter.
  • Implement moveToLastNonWhiteSpaceCharacter and findLastNonWhiteSpaceCharacter() helper.
  • Add automated tests covering basic movement, count usage, and dg_ behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/vim.js Adds the g_ keybinding, implements the motion, and introduces a helper to find the last non-whitespace character.
test/vim_test.js Adds a new test case for g_ movement, count behavior, and delete-operator interaction.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2543 to +2545
var lineText = cm.getLine(line);
var lastNonWS = findLastNonWhiteSpaceCharacter(lineText);
return new Pos(line, Math.max(0, lastNonWS));
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

moveToLastNonWhiteSpaceCharacter computes line = head.line + motionArgs.repeat - 1 and immediately calls cm.getLine(line) without clipping the line number to the document bounds. With a large count (e.g. 100g_ near EOF) this can produce an out-of-range line and return a Pos with an invalid line, which then flows into operator selections (dg_, etc.) without being clipped and can break selection logic. Clamp the target line to [cm.firstLine(), cm.lastLine()] (or use cm.clipPos(new Pos(line, 0)).line) before calling getLine and before returning the Pos.

Suggested change
var lineText = cm.getLine(line);
var lastNonWS = findLastNonWhiteSpaceCharacter(lineText);
return new Pos(line, Math.max(0, lastNonWS));
var clippedLine = cm.clipPos(new Pos(line, 0)).line;
var lineText = cm.getLine(clippedLine);
var lastNonWS = findLastNonWhiteSpaceCharacter(lineText);
return new Pos(clippedLine, Math.max(0, lastNonWS));

Copilot uses AI. Check for mistakes.
Comment on lines +544 to +561
testVim('g_', function(cm, vim, helpers) {
// Test basic g_ on line with trailing spaces
cm.setCursor(0, 0);
helpers.doKeys('g', '_');
helpers.assertCursorAt(0, 2);
// Test g_ on line without trailing spaces
cm.setCursor(1, 0);
helpers.doKeys('g', '_');
helpers.assertCursorAt(1, 2);
// Test g_ with count (2g_ goes to last non-whitespace of next line)
cm.setCursor(0, 0);
helpers.doKeys('2', 'g', '_');
helpers.assertCursorAt(1, 2);
// Test dg_ deletes to last non-whitespace (preserving trailing spaces)
cm.setCursor(0, 0);
helpers.doKeys('d', 'g', '_');
eq(' \nfoo', cm.getValue());
}, { value: 'foo \nfoo' });
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

The new g_ tests cover a small count (2g_) but don’t cover boundary behavior when the count would move past the end of the document. Since g_ is countable, adding a test like 100g_ on a short buffer (and asserting it lands on the last line’s last non-blank without throwing) would help lock in correct clipping behavior.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants