Increase and decrease number under cursor

Author Mitja Felicijan <mitja.felicijan@gmail.com> 2026-05-20 02:44:54 +0200
Committer Mitja Felicijan <mitja.felicijan@gmail.com> 2026-05-20 02:50:26 +0200
Commit 2cbf9a62eca58e6e67e1ea4344ad7fa16029a7fb (patch)
-rw-r--r-- editor.go 146
-rw-r--r-- kevent.go 4
2 files changed, 150 insertions, 0 deletions
diff --git a/editor.go b/editor.go
...
1625
	return string(line[start:end])
1625
	return string(line[start:end])
1626
}
1626
}
1627
  
1627
  
  
1628
func (e *Editor) ModifyNumberUnderCursor(delta int) {
  
1629
	b := e.activeBuffer()
  
1630
	if b == nil || b.readOnly {
  
1631
		return
  
1632
	}
  
1633
  
  
1634
	e.saveState()
  
1635
	cursors := e.getSortedCursorsDesc()
  
1636
	modified := false
  
1637
  
  
1638
	for _, c := range cursors {
  
1639
		if c.Y >= len(b.buffer) {
  
1640
			continue
  
1641
		}
  
1642
		line := b.buffer[c.Y]
  
1643
		if len(line) == 0 {
  
1644
			continue
  
1645
		}
  
1646
  
  
1647
		x := c.X
  
1648
		if x >= len(line) {
  
1649
			x = len(line) - 1
  
1650
		}
  
1651
  
  
1652
		isDigit := func(r rune) bool { return r >= '0' && r <= '9' }
  
1653
  
  
1654
		// Helper to check if there is a valid number at or around a given position
  
1655
		checkAt := func(pos int) (int, int) {
  
1656
			if pos < 0 || pos >= len(line) {
  
1657
				return -1, -1
  
1658
			}
  
1659
			// Case 1: Position is on a word character.
  
1660
			if e.isWordChar(line[pos]) {
  
1661
				wStart := pos
  
1662
				for wStart > 0 && e.isWordChar(line[wStart-1]) {
  
1663
					wStart--
  
1664
				}
  
1665
				wEnd := pos
  
1666
				for wEnd < len(line) && e.isWordChar(line[wEnd]) {
  
1667
					wEnd++
  
1668
				}
  
1669
  
  
1670
				// Check if the entire word is digits.
  
1671
				allDigits := true
  
1672
				for i := wStart; i < wEnd; i++ {
  
1673
					if !isDigit(line[i]) {
  
1674
						allDigits = false
  
1675
						break
  
1676
					}
  
1677
				}
  
1678
  
  
1679
				if allDigits {
  
1680
					s := wStart
  
1681
					en := wEnd
  
1682
					// Check for leading minus sign.
  
1683
					if s > 0 && line[s-1] == '-' {
  
1684
						// Ensure the minus isn't preceded by another word character.
  
1685
						if s == 1 || !e.isWordChar(line[s-2]) {
  
1686
							s--
  
1687
						}
  
1688
					}
  
1689
					return s, en
  
1690
				}
  
1691
			} else if line[pos] == '-' && pos+1 < len(line) && isDigit(line[pos+1]) {
  
1692
				// Case 2: Position is on a minus sign followed by a digit.
  
1693
				wStart := pos + 1
  
1694
				wEnd := pos + 1
  
1695
				for wEnd < len(line) && e.isWordChar(line[wEnd]) {
  
1696
					wEnd++
  
1697
				}
  
1698
  
  
1699
				// Check if the word following the minus sign is all digits.
  
1700
				allDigits := true
  
1701
				for i := wStart; i < wEnd; i++ {
  
1702
					if !isDigit(line[i]) {
  
1703
						allDigits = false
  
1704
						break
  
1705
					}
  
1706
				}
  
1707
  
  
1708
				if allDigits {
  
1709
					return pos, wEnd
  
1710
				}
  
1711
			}
  
1712
			return -1, -1
  
1713
		}
  
1714
  
  
1715
		start := -1
  
1716
		end := -1
  
1717
  
  
1718
		// 1. Check if there's a number at the current cursor position.
  
1719
		start, end = checkAt(x)
  
1720
  
  
1721
		// 2. If not, search forward on the current line.
  
1722
		if start == -1 {
  
1723
			for i := x + 1; i < len(line); i++ {
  
1724
				// Optimization: only check if it looks like a number start.
  
1725
				if isDigit(line[i]) || (line[i] == '-' && i+1 < len(line) && isDigit(line[i+1])) {
  
1726
					start, end = checkAt(i)
  
1727
					if start != -1 {
  
1728
						break
  
1729
					}
  
1730
				}
  
1731
			}
  
1732
		}
  
1733
  
  
1734
		if start == -1 {
  
1735
			continue
  
1736
		}
  
1737
  
  
1738
		numStr := string(line[start:end])
  
1739
		val, err := strconv.Atoi(numStr)
  
1740
		if err != nil {
  
1741
			continue
  
1742
		}
  
1743
  
  
1744
		newVal := val + delta
  
1745
		newStr := strconv.Itoa(newVal)
  
1746
		newRunes := []rune(newStr)
  
1747
  
  
1748
		// Replace the number in the buffer.
  
1749
		newLine := append(line[:start], append(newRunes, line[end:]...)...)
  
1750
		b.buffer[c.Y] = newLine
  
1751
  
  
1752
		// Handle syntax update.
  
1753
		if b.syntax != nil {
  
1754
			deletedBytes := uint32(len(string(line[start:end])))
  
1755
			addedBytes := uint32(len(string(newRunes)))
  
1756
			oldColBytes := b.getLineByteOffset(line, start)
  
1757
			newColBytes := b.getLineByteOffset(newLine, start)
  
1758
			b.handleEdit(c.Y, start, deletedBytes, addedBytes, c.Y, oldColBytes+deletedBytes, c.Y, newColBytes+addedBytes)
  
1759
		}
  
1760
  
  
1761
		// Keep cursor on the number (at its start).
  
1762
		c.X = start
  
1763
		modified = true
  
1764
	}
  
1765
  
  
1766
	if modified {
  
1767
		if b.syntax != nil {
  
1768
			b.syntax.Reparse([]byte(b.toString()))
  
1769
		}
  
1770
		e.markModified()
  
1771
	}
  
1772
}
  
1773
  
1628
func (e *Editor) isPathChar(r rune) bool {
1774
func (e *Editor) isPathChar(r rune) bool {
1629
	return e.isWordChar(r) || r == '/' || r == '.' || r == '-' || r == '_' || r == '~' || r == '\\' || r == ':'
1775
	return e.isWordChar(r) || r == '/' || r == '.' || r == '-' || r == '_' || r == '~' || r == '\\' || r == ':'
1630
}
1776
}
...
diff --git a/kevent.go b/kevent.go
...
239
		e.mode = ModeVisualBlock
239
		e.mode = ModeVisualBlock
240
	case termbox.KeyCtrlK:
240
	case termbox.KeyCtrlK:
241
		e.triggerHover()
241
		e.triggerHover()
  
242
	case termbox.KeyCtrlA:
  
243
		e.ModifyNumberUnderCursor(1)
  
244
	case termbox.KeyCtrlZ:
  
245
		e.ModifyNumberUnderCursor(-1)
242
	}
246
	}
243
  
247
  
244
	// Prevent key event fallthrough.
248
	// Prevent key event fallthrough.
...