vim-sim 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1547,9 +1547,342 @@ var CompletionManager = class _CompletionManager {
1547
1547
  }
1548
1548
  };
1549
1549
 
1550
+ // src/core/config-manager.ts
1551
+ var ConfigManager = class _ConfigManager {
1552
+ config;
1553
+ listeners = /* @__PURE__ */ new Map();
1554
+ constructor(initialConfig) {
1555
+ this.config = {
1556
+ // Display
1557
+ number: false,
1558
+ relativenumber: false,
1559
+ wrap: true,
1560
+ cursorline: false,
1561
+ // Indentation
1562
+ tabstop: 4,
1563
+ shiftwidth: 2,
1564
+ expandtab: true,
1565
+ autoindent: true,
1566
+ smartindent: false,
1567
+ // Search
1568
+ ignorecase: false,
1569
+ smartcase: false,
1570
+ hlsearch: true,
1571
+ incsearch: true,
1572
+ wrapscan: true,
1573
+ // Motion behavior
1574
+ whichwrap: "h,l",
1575
+ // vim-sim default: h,l wrap (Vim default: "b,s")
1576
+ startofline: true,
1577
+ // Vim default: true
1578
+ virtualedit: "",
1579
+ // Vim default: ""
1580
+ // Editing
1581
+ undolevels: 1e3,
1582
+ clipboard: "",
1583
+ ...initialConfig
1584
+ };
1585
+ }
1586
+ /**
1587
+ * Get a config value
1588
+ */
1589
+ get(key) {
1590
+ return this.config[key];
1591
+ }
1592
+ /**
1593
+ * Set a config value
1594
+ */
1595
+ set(key, value) {
1596
+ const oldValue = this.config[key];
1597
+ if (oldValue === value) return;
1598
+ this.config[key] = value;
1599
+ this.notifyListeners(key, value);
1600
+ }
1601
+ /**
1602
+ * Set multiple config values at once
1603
+ */
1604
+ setMultiple(updates) {
1605
+ for (const [key, value] of Object.entries(updates)) {
1606
+ this.set(key, value);
1607
+ }
1608
+ }
1609
+ /**
1610
+ * Get all config values
1611
+ */
1612
+ getAll() {
1613
+ return { ...this.config };
1614
+ }
1615
+ /**
1616
+ * Reset to default configuration
1617
+ */
1618
+ reset() {
1619
+ const defaults = new _ConfigManager().getAll();
1620
+ this.setMultiple(defaults);
1621
+ }
1622
+ /**
1623
+ * Toggle a boolean config option
1624
+ */
1625
+ toggle(key) {
1626
+ const value = this.config[key];
1627
+ if (typeof value === "boolean") {
1628
+ this.set(key, !value);
1629
+ }
1630
+ }
1631
+ /**
1632
+ * Subscribe to config changes
1633
+ * @param key - Config key to watch, or '*' for all changes
1634
+ * @param callback - Function to call when config changes
1635
+ * @returns Unsubscribe function
1636
+ */
1637
+ onChange(key, callback) {
1638
+ if (!this.listeners.has(key)) {
1639
+ this.listeners.set(key, /* @__PURE__ */ new Set());
1640
+ }
1641
+ this.listeners.get(key).add(callback);
1642
+ return () => {
1643
+ this.listeners.get(key)?.delete(callback);
1644
+ };
1645
+ }
1646
+ /**
1647
+ * Notify all listeners of a config change
1648
+ */
1649
+ notifyListeners(key, value) {
1650
+ this.listeners.get(key)?.forEach((callback) => callback(key, value));
1651
+ this.listeners.get("*")?.forEach((callback) => callback(key, value));
1652
+ }
1653
+ /**
1654
+ * Parse a Vim set command string (e.g., "number", "nonumber", "tabstop=4")
1655
+ */
1656
+ parseSetCommand(command) {
1657
+ const noMatch = command.match(/^no(\w+)$/);
1658
+ if (noMatch) {
1659
+ const key2 = noMatch[1];
1660
+ if (key2 in this.config && typeof this.config[key2] === "boolean") {
1661
+ this.set(key2, false);
1662
+ return true;
1663
+ }
1664
+ return false;
1665
+ }
1666
+ const valueMatch = command.match(/^(\w+)=(.+)$/);
1667
+ if (valueMatch) {
1668
+ const key2 = valueMatch[1];
1669
+ const value = valueMatch[2];
1670
+ if (!(key2 in this.config)) return false;
1671
+ const currentValue = this.config[key2];
1672
+ if (typeof currentValue === "number") {
1673
+ const numValue = parseInt(value, 10);
1674
+ if (!isNaN(numValue)) {
1675
+ this.set(key2, numValue);
1676
+ return true;
1677
+ }
1678
+ } else if (typeof currentValue === "string") {
1679
+ this.set(key2, value);
1680
+ return true;
1681
+ }
1682
+ return false;
1683
+ }
1684
+ const key = command;
1685
+ if (key in this.config && typeof this.config[key] === "boolean") {
1686
+ this.set(key, true);
1687
+ return true;
1688
+ }
1689
+ return false;
1690
+ }
1691
+ /**
1692
+ * Get a formatted string representation of a config value
1693
+ */
1694
+ formatValue(key) {
1695
+ const value = this.config[key];
1696
+ if (typeof value === "boolean") {
1697
+ return value ? key : `no${key}`;
1698
+ }
1699
+ return `${key}=${value}`;
1700
+ }
1701
+ /**
1702
+ * Get metadata about a config option
1703
+ */
1704
+ getMetadata(key) {
1705
+ return CONFIG_METADATA[key];
1706
+ }
1707
+ /**
1708
+ * Get all options that differ from vanilla Vim
1709
+ */
1710
+ getDifferencesFromVim() {
1711
+ return Object.entries(CONFIG_METADATA).filter(([_, meta]) => meta.differsFromVim).map(([key, _]) => key);
1712
+ }
1713
+ /**
1714
+ * Get options by category
1715
+ */
1716
+ getByCategory(category) {
1717
+ return Object.entries(CONFIG_METADATA).filter(([_, meta]) => meta.category === category).map(([key, _]) => key);
1718
+ }
1719
+ };
1720
+ var CONFIG_METADATA = {
1721
+ // Display options
1722
+ number: {
1723
+ description: "Show line numbers in the left margin",
1724
+ vimDefault: false,
1725
+ vimSimDefault: false,
1726
+ category: "display",
1727
+ type: "boolean",
1728
+ differsFromVim: false
1729
+ },
1730
+ relativenumber: {
1731
+ description: "Show line numbers relative to cursor position",
1732
+ vimDefault: false,
1733
+ vimSimDefault: false,
1734
+ category: "display",
1735
+ type: "boolean",
1736
+ differsFromVim: false
1737
+ },
1738
+ wrap: {
1739
+ description: "Wrap long lines to fit in window",
1740
+ vimDefault: true,
1741
+ vimSimDefault: true,
1742
+ category: "display",
1743
+ type: "boolean",
1744
+ differsFromVim: false
1745
+ },
1746
+ cursorline: {
1747
+ description: "Highlight the screen line of the cursor",
1748
+ vimDefault: false,
1749
+ vimSimDefault: false,
1750
+ category: "display",
1751
+ type: "boolean",
1752
+ differsFromVim: false
1753
+ },
1754
+ // Indentation options
1755
+ tabstop: {
1756
+ description: "Number of spaces that a <Tab> in the file counts for",
1757
+ vimDefault: 8,
1758
+ vimSimDefault: 4,
1759
+ category: "indentation",
1760
+ type: "number",
1761
+ differsFromVim: true
1762
+ },
1763
+ shiftwidth: {
1764
+ description: "Number of spaces to use for each step of (auto)indent",
1765
+ vimDefault: 8,
1766
+ vimSimDefault: 2,
1767
+ category: "indentation",
1768
+ type: "number",
1769
+ differsFromVim: true
1770
+ },
1771
+ expandtab: {
1772
+ description: "Use spaces instead of tabs when pressing <Tab>",
1773
+ vimDefault: false,
1774
+ vimSimDefault: true,
1775
+ category: "indentation",
1776
+ type: "boolean",
1777
+ differsFromVim: true
1778
+ },
1779
+ autoindent: {
1780
+ description: "Copy indent from current line when starting a new line",
1781
+ vimDefault: false,
1782
+ vimSimDefault: true,
1783
+ category: "indentation",
1784
+ type: "boolean",
1785
+ differsFromVim: true
1786
+ },
1787
+ smartindent: {
1788
+ description: "Do smart autoindenting when starting a new line",
1789
+ vimDefault: false,
1790
+ vimSimDefault: false,
1791
+ category: "indentation",
1792
+ type: "boolean",
1793
+ differsFromVim: false
1794
+ },
1795
+ // Search options
1796
+ ignorecase: {
1797
+ description: "Ignore case in search patterns",
1798
+ vimDefault: false,
1799
+ vimSimDefault: false,
1800
+ category: "search",
1801
+ type: "boolean",
1802
+ differsFromVim: false
1803
+ },
1804
+ smartcase: {
1805
+ description: "Override ignorecase if search pattern contains uppercase",
1806
+ vimDefault: false,
1807
+ vimSimDefault: false,
1808
+ category: "search",
1809
+ type: "boolean",
1810
+ differsFromVim: false
1811
+ },
1812
+ hlsearch: {
1813
+ description: "Highlight all matches for the search pattern",
1814
+ vimDefault: false,
1815
+ vimSimDefault: true,
1816
+ category: "search",
1817
+ type: "boolean",
1818
+ differsFromVim: true
1819
+ },
1820
+ incsearch: {
1821
+ description: "Show match for partly typed search pattern",
1822
+ vimDefault: false,
1823
+ vimSimDefault: true,
1824
+ category: "search",
1825
+ type: "boolean",
1826
+ differsFromVim: true
1827
+ },
1828
+ wrapscan: {
1829
+ description: "Searches wrap around the end of the file",
1830
+ vimDefault: true,
1831
+ vimSimDefault: true,
1832
+ category: "search",
1833
+ type: "boolean",
1834
+ differsFromVim: false
1835
+ },
1836
+ // Motion behavior options
1837
+ whichwrap: {
1838
+ description: "Keys that wrap to prev/next line (b=<BS>, s=<Space>, h=h, l=l, <=<Left>, >=<Right>, [=<Left> in insert, ]=<Right> in insert)",
1839
+ vimDefault: "b,s",
1840
+ vimSimDefault: "h,l",
1841
+ category: "motion",
1842
+ type: "string",
1843
+ differsFromVim: true,
1844
+ validValues: ["b", "s", "h", "l", "<", ">", "[", "]", "~"]
1845
+ },
1846
+ startofline: {
1847
+ description: "Move cursor to first non-blank when changing lines with G, gg, d, etc.",
1848
+ vimDefault: true,
1849
+ vimSimDefault: true,
1850
+ category: "motion",
1851
+ type: "boolean",
1852
+ differsFromVim: false
1853
+ },
1854
+ virtualedit: {
1855
+ description: "Allow cursor positioning where there is no character (block, insert, all, onemore)",
1856
+ vimDefault: "",
1857
+ vimSimDefault: "",
1858
+ category: "motion",
1859
+ type: "string",
1860
+ differsFromVim: false,
1861
+ validValues: ["block", "insert", "all", "onemore", ""]
1862
+ },
1863
+ // Editing options
1864
+ undolevels: {
1865
+ description: "Maximum number of changes that can be undone",
1866
+ vimDefault: 1e3,
1867
+ vimSimDefault: 1e3,
1868
+ category: "editing",
1869
+ type: "number",
1870
+ differsFromVim: false
1871
+ },
1872
+ clipboard: {
1873
+ description: "Use system clipboard for unnamed register",
1874
+ vimDefault: "",
1875
+ vimSimDefault: "",
1876
+ category: "editing",
1877
+ type: "string",
1878
+ differsFromVim: false,
1879
+ validValues: ["unnamed", "unnamedplus", ""]
1880
+ }
1881
+ };
1882
+
1550
1883
  // src/core/state.ts
1551
1884
  var State = class {
1552
- constructor(buffer, cursor, selection, mode, commandLine = "", desiredColumn = null, viewport = new VimViewport(), visualAnchor = null, lastVisualSelection = null, recordingRegister = null, lastMacroRegister = null, foldManager = new FoldManager(), markManager = new MarkManager(), jumpListManager = new JumpListManager(), fileSystem = new FileSystemManager(), windowManager = new WindowManager(), lastSearch = null, spellChecker = new SpellChecker(), completionManager = new CompletionManager(), undoTree = null, lastCommand = null) {
1885
+ constructor(buffer, cursor, selection, mode, commandLine = "", desiredColumn = null, viewport = new VimViewport(), visualAnchor = null, lastVisualSelection = null, recordingRegister = null, lastMacroRegister = null, foldManager = new FoldManager(), markManager = new MarkManager(), jumpListManager = new JumpListManager(), fileSystem = new FileSystemManager(), windowManager = new WindowManager(), lastSearch = null, spellChecker = new SpellChecker(), completionManager = new CompletionManager(), undoTree = null, lastCommand = null, configManager = null) {
1553
1886
  this.buffer = buffer;
1554
1887
  this.cursor = cursor;
1555
1888
  this.selection = selection;
@@ -1571,6 +1904,7 @@ var State = class {
1571
1904
  this.completionManager = completionManager;
1572
1905
  this.undoTree = undoTree;
1573
1906
  this.lastCommand = lastCommand;
1907
+ this.configManager = configManager;
1574
1908
  }
1575
1909
  };
1576
1910
  function createEmptyState() {
@@ -1596,8 +1930,10 @@ function createEmptyState() {
1596
1930
  new CompletionManager(),
1597
1931
  null,
1598
1932
  // undoTree will be initialized separately
1599
- null
1933
+ null,
1600
1934
  // lastCommand
1935
+ new ConfigManager()
1936
+ // configManager with default settings
1601
1937
  );
1602
1938
  return state;
1603
1939
  }
@@ -2139,7 +2475,17 @@ function withBufferContent(state, content) {
2139
2475
  state.visualAnchor,
2140
2476
  state.lastVisualSelection,
2141
2477
  state.recordingRegister,
2142
- state.lastMacroRegister
2478
+ state.lastMacroRegister,
2479
+ state.foldManager,
2480
+ state.markManager,
2481
+ state.jumpListManager,
2482
+ state.fileSystem,
2483
+ state.windowManager,
2484
+ state.lastSearch,
2485
+ state.spellChecker,
2486
+ state.completionManager,
2487
+ state.undoTree,
2488
+ state.lastCommand
2143
2489
  );
2144
2490
  }
2145
2491
  function withCursor(state, cursor, preserveDesiredColumn = true) {
@@ -2154,7 +2500,17 @@ function withCursor(state, cursor, preserveDesiredColumn = true) {
2154
2500
  state.visualAnchor,
2155
2501
  state.lastVisualSelection,
2156
2502
  state.recordingRegister,
2157
- state.lastMacroRegister
2503
+ state.lastMacroRegister,
2504
+ state.foldManager,
2505
+ state.markManager,
2506
+ state.jumpListManager,
2507
+ state.fileSystem,
2508
+ state.windowManager,
2509
+ state.lastSearch,
2510
+ state.spellChecker,
2511
+ state.completionManager,
2512
+ state.undoTree,
2513
+ state.lastCommand
2158
2514
  );
2159
2515
  }
2160
2516
  function withCursorVertical(state, cursor, targetColumn) {
@@ -2166,11 +2522,22 @@ function withCursorVertical(state, cursor, targetColumn) {
2166
2522
  state.mode,
2167
2523
  state.commandLine,
2168
2524
  desiredCol,
2525
+ // Maintain desiredColumn for vertical movements
2169
2526
  state.viewport,
2170
2527
  state.visualAnchor,
2171
2528
  state.lastVisualSelection,
2172
2529
  state.recordingRegister,
2173
- state.lastMacroRegister
2530
+ state.lastMacroRegister,
2531
+ state.foldManager,
2532
+ state.markManager,
2533
+ state.jumpListManager,
2534
+ state.fileSystem,
2535
+ state.windowManager,
2536
+ state.lastSearch,
2537
+ state.spellChecker,
2538
+ state.completionManager,
2539
+ state.undoTree,
2540
+ state.lastCommand
2174
2541
  );
2175
2542
  }
2176
2543
  function withCursorHorizontal(state, cursor) {
@@ -2181,11 +2548,22 @@ function withCursorHorizontal(state, cursor) {
2181
2548
  state.mode,
2182
2549
  state.commandLine,
2183
2550
  null,
2551
+ // Reset desiredColumn for horizontal movements
2184
2552
  state.viewport,
2185
2553
  state.visualAnchor,
2186
2554
  state.lastVisualSelection,
2187
2555
  state.recordingRegister,
2188
- state.lastMacroRegister
2556
+ state.lastMacroRegister,
2557
+ state.foldManager,
2558
+ state.markManager,
2559
+ state.jumpListManager,
2560
+ state.fileSystem,
2561
+ state.windowManager,
2562
+ state.lastSearch,
2563
+ state.spellChecker,
2564
+ state.completionManager,
2565
+ state.undoTree,
2566
+ state.lastCommand
2189
2567
  );
2190
2568
  }
2191
2569
  function withViewport(state, topLine) {
@@ -2471,18 +2849,24 @@ var B = class extends Motion {
2471
2849
  };
2472
2850
 
2473
2851
  // src/motions/basic.ts
2852
+ function canWrap(state, key) {
2853
+ const whichwrap = state.configManager?.get("whichwrap") ?? "h,l";
2854
+ const allowed = whichwrap.split(",").map((s) => s.trim());
2855
+ return allowed.includes(key);
2856
+ }
2474
2857
  var h = class extends Motion {
2475
2858
  key = "h";
2476
2859
  execute(state, context) {
2477
2860
  let count = getCount(context);
2478
2861
  let line = state.cursor.line;
2479
2862
  let col = state.cursor.column;
2863
+ const allowWrap = canWrap(state, "h");
2480
2864
  while (count > 0) {
2481
2865
  if (col > 0) {
2482
2866
  const moveAmount = Math.min(col, count);
2483
2867
  col -= moveAmount;
2484
2868
  count -= moveAmount;
2485
- } else if (line > 0) {
2869
+ } else if (line > 0 && allowWrap) {
2486
2870
  line--;
2487
2871
  const prevLine = getLine(state.buffer, line);
2488
2872
  col = Math.max(0, prevLine.length - 1);
@@ -2494,11 +2878,53 @@ var h = class extends Motion {
2494
2878
  return withCursorHorizontal(state, new Cursor(line, col));
2495
2879
  }
2496
2880
  };
2497
- var Backspace = class extends h {
2881
+ var Backspace = class extends Motion {
2498
2882
  key = "Backspace";
2883
+ execute(state, context) {
2884
+ let count = getCount(context);
2885
+ let line = state.cursor.line;
2886
+ let col = state.cursor.column;
2887
+ const allowWrap = canWrap(state, "b");
2888
+ while (count > 0) {
2889
+ if (col > 0) {
2890
+ const moveAmount = Math.min(col, count);
2891
+ col -= moveAmount;
2892
+ count -= moveAmount;
2893
+ } else if (line > 0 && allowWrap) {
2894
+ line--;
2895
+ const prevLine = getLine(state.buffer, line);
2896
+ col = Math.max(0, prevLine.length - 1);
2897
+ count--;
2898
+ } else {
2899
+ break;
2900
+ }
2901
+ }
2902
+ return withCursorHorizontal(state, new Cursor(line, col));
2903
+ }
2499
2904
  };
2500
- var ArrowLeft = class extends h {
2905
+ var ArrowLeft = class extends Motion {
2501
2906
  key = "ArrowLeft";
2907
+ execute(state, context) {
2908
+ let count = getCount(context);
2909
+ let line = state.cursor.line;
2910
+ let col = state.cursor.column;
2911
+ const allowWrap = canWrap(state, "<");
2912
+ while (count > 0) {
2913
+ if (col > 0) {
2914
+ const moveAmount = Math.min(col, count);
2915
+ col -= moveAmount;
2916
+ count -= moveAmount;
2917
+ } else if (line > 0 && allowWrap) {
2918
+ line--;
2919
+ const prevLine = getLine(state.buffer, line);
2920
+ col = Math.max(0, prevLine.length - 1);
2921
+ count--;
2922
+ } else {
2923
+ break;
2924
+ }
2925
+ }
2926
+ return withCursorHorizontal(state, new Cursor(line, col));
2927
+ }
2502
2928
  };
2503
2929
  var l = class extends Motion {
2504
2930
  key = "l";
@@ -2506,7 +2932,7 @@ var l = class extends Motion {
2506
2932
  let count = getCount(context);
2507
2933
  let line = state.cursor.line;
2508
2934
  let col = state.cursor.column;
2509
- const totalLines = lastLine(state) + 1;
2935
+ const allowWrap = canWrap(state, "l");
2510
2936
  while (count > 0) {
2511
2937
  const currentLine2 = getLine(state.buffer, line);
2512
2938
  const maxCol = Math.max(0, currentLine2.length - 1);
@@ -2514,7 +2940,7 @@ var l = class extends Motion {
2514
2940
  const moveAmount = Math.min(maxCol - col, count);
2515
2941
  col += moveAmount;
2516
2942
  count -= moveAmount;
2517
- } else if (line < lastLine(state)) {
2943
+ } else if (line < lastLine(state) && allowWrap) {
2518
2944
  line++;
2519
2945
  col = 0;
2520
2946
  count--;
@@ -2525,8 +2951,30 @@ var l = class extends Motion {
2525
2951
  return withCursorHorizontal(state, new Cursor(line, col));
2526
2952
  }
2527
2953
  };
2528
- var ArrowRight = class extends l {
2954
+ var ArrowRight = class extends Motion {
2529
2955
  key = "ArrowRight";
2956
+ execute(state, context) {
2957
+ let count = getCount(context);
2958
+ let line = state.cursor.line;
2959
+ let col = state.cursor.column;
2960
+ const allowWrap = canWrap(state, ">");
2961
+ while (count > 0) {
2962
+ const currentLine2 = getLine(state.buffer, line);
2963
+ const maxCol = Math.max(0, currentLine2.length - 1);
2964
+ if (col < maxCol) {
2965
+ const moveAmount = Math.min(maxCol - col, count);
2966
+ col += moveAmount;
2967
+ count -= moveAmount;
2968
+ } else if (line < lastLine(state) && allowWrap) {
2969
+ line++;
2970
+ col = 0;
2971
+ count--;
2972
+ } else {
2973
+ break;
2974
+ }
2975
+ }
2976
+ return withCursorHorizontal(state, new Cursor(line, col));
2977
+ }
2530
2978
  };
2531
2979
  var j = class extends Motion {
2532
2980
  key = "j";
@@ -2838,151 +3286,6 @@ var IndentationManager = class {
2838
3286
  }
2839
3287
  };
2840
3288
 
2841
- // src/core/config-manager.ts
2842
- var ConfigManager = class _ConfigManager {
2843
- config;
2844
- listeners = /* @__PURE__ */ new Map();
2845
- constructor(initialConfig) {
2846
- this.config = {
2847
- // Display
2848
- number: false,
2849
- relativenumber: false,
2850
- wrap: true,
2851
- cursorline: false,
2852
- // Indentation
2853
- tabstop: 4,
2854
- shiftwidth: 2,
2855
- expandtab: true,
2856
- autoindent: true,
2857
- smartindent: false,
2858
- // Search
2859
- ignorecase: false,
2860
- smartcase: false,
2861
- hlsearch: true,
2862
- incsearch: true,
2863
- // Editing
2864
- undolevels: 1e3,
2865
- clipboard: "",
2866
- ...initialConfig
2867
- };
2868
- }
2869
- /**
2870
- * Get a config value
2871
- */
2872
- get(key) {
2873
- return this.config[key];
2874
- }
2875
- /**
2876
- * Set a config value
2877
- */
2878
- set(key, value) {
2879
- const oldValue = this.config[key];
2880
- if (oldValue === value) return;
2881
- this.config[key] = value;
2882
- this.notifyListeners(key, value);
2883
- }
2884
- /**
2885
- * Set multiple config values at once
2886
- */
2887
- setMultiple(updates) {
2888
- for (const [key, value] of Object.entries(updates)) {
2889
- this.set(key, value);
2890
- }
2891
- }
2892
- /**
2893
- * Get all config values
2894
- */
2895
- getAll() {
2896
- return { ...this.config };
2897
- }
2898
- /**
2899
- * Reset to default configuration
2900
- */
2901
- reset() {
2902
- const defaults = new _ConfigManager().getAll();
2903
- this.setMultiple(defaults);
2904
- }
2905
- /**
2906
- * Toggle a boolean config option
2907
- */
2908
- toggle(key) {
2909
- const value = this.config[key];
2910
- if (typeof value === "boolean") {
2911
- this.set(key, !value);
2912
- }
2913
- }
2914
- /**
2915
- * Subscribe to config changes
2916
- * @param key - Config key to watch, or '*' for all changes
2917
- * @param callback - Function to call when config changes
2918
- * @returns Unsubscribe function
2919
- */
2920
- onChange(key, callback) {
2921
- if (!this.listeners.has(key)) {
2922
- this.listeners.set(key, /* @__PURE__ */ new Set());
2923
- }
2924
- this.listeners.get(key).add(callback);
2925
- return () => {
2926
- this.listeners.get(key)?.delete(callback);
2927
- };
2928
- }
2929
- /**
2930
- * Notify all listeners of a config change
2931
- */
2932
- notifyListeners(key, value) {
2933
- this.listeners.get(key)?.forEach((callback) => callback(key, value));
2934
- this.listeners.get("*")?.forEach((callback) => callback(key, value));
2935
- }
2936
- /**
2937
- * Parse a Vim set command string (e.g., "number", "nonumber", "tabstop=4")
2938
- */
2939
- parseSetCommand(command) {
2940
- const noMatch = command.match(/^no(\w+)$/);
2941
- if (noMatch) {
2942
- const key2 = noMatch[1];
2943
- if (key2 in this.config && typeof this.config[key2] === "boolean") {
2944
- this.set(key2, false);
2945
- return true;
2946
- }
2947
- return false;
2948
- }
2949
- const valueMatch = command.match(/^(\w+)=(.+)$/);
2950
- if (valueMatch) {
2951
- const key2 = valueMatch[1];
2952
- const value = valueMatch[2];
2953
- if (!(key2 in this.config)) return false;
2954
- const currentValue = this.config[key2];
2955
- if (typeof currentValue === "number") {
2956
- const numValue = parseInt(value, 10);
2957
- if (!isNaN(numValue)) {
2958
- this.set(key2, numValue);
2959
- return true;
2960
- }
2961
- } else if (typeof currentValue === "string") {
2962
- this.set(key2, value);
2963
- return true;
2964
- }
2965
- return false;
2966
- }
2967
- const key = command;
2968
- if (key in this.config && typeof this.config[key] === "boolean") {
2969
- this.set(key, true);
2970
- return true;
2971
- }
2972
- return false;
2973
- }
2974
- /**
2975
- * Get a formatted string representation of a config value
2976
- */
2977
- formatValue(key) {
2978
- const value = this.config[key];
2979
- if (typeof value === "boolean") {
2980
- return value ? key : `no${key}`;
2981
- }
2982
- return `${key}=${value}`;
2983
- }
2984
- };
2985
-
2986
3289
  // src/core/mode-transition.ts
2987
3290
  var ModeTransition = class extends Command {
2988
3291
  // Optional hook to modify state before mode transition
@@ -3841,7 +4144,7 @@ var gE = class extends Motion {
3841
4144
  return repeatCursorMotion(state, context, findPrevWORDEnd);
3842
4145
  }
3843
4146
  };
3844
- var LastNonBlank = class extends Motion {
4147
+ var g_ = class extends Motion {
3845
4148
  key = "g_";
3846
4149
  inclusive = true;
3847
4150
  // g_ is inclusive
@@ -3855,7 +4158,7 @@ var LastNonBlank = class extends Motion {
3855
4158
  return withCursorHorizontal(state, new Cursor(state.cursor.line, col));
3856
4159
  }
3857
4160
  };
3858
- var MiddleOfScreenLine = class extends Motion {
4161
+ var gm = class extends Motion {
3859
4162
  key = "gm";
3860
4163
  execute(state) {
3861
4164
  const line = getLine(state.buffer, state.cursor.line);
@@ -6234,18 +6537,11 @@ var Session = class _Session {
6234
6537
  }
6235
6538
  this.keyBuffer.push(key);
6236
6539
  const input = this.keyBuffer.join("");
6237
- console.log("[DEBUG] handleKey - input:", input, "keyBuffer:", this.keyBuffer);
6238
6540
  const result = this.match(input, this.keyBuffer);
6239
- console.log("[DEBUG] handleKey - match result:", result ? result === "partial" ? "partial" : `complete: ${result.command.key}` : "null");
6240
6541
  if (result && result !== "partial" && result.isComplete) {
6241
6542
  const stateBefore = this.state;
6242
6543
  result.context.registerManager = this.registerManager;
6243
6544
  result.context.indentationManager = this.indentationManager;
6244
- console.log("[DEBUG] Executing command:", result.command.key, "with context:", {
6245
- count: result.context.count,
6246
- motion: result.context.motion?.key,
6247
- char: result.context.char
6248
- });
6249
6545
  this.state = result.command.execute(this.state, result.context);
6250
6546
  if ((stateBefore.mode === 2 /* VISUAL */ || stateBefore.mode === 3 /* VISUAL_LINE */ || stateBefore.mode === 4 /* VISUAL_BLOCK */) && this.state.mode === stateBefore.mode && // Still in same visual mode
6251
6547
  stateBefore.visualAnchor && (this.state.cursor.line !== stateBefore.cursor.line || this.state.cursor.column !== stateBefore.cursor.column)) {
@@ -6648,8 +6944,8 @@ var Session = class _Session {
6648
6944
  addMotionAllModes(new MatchPercent());
6649
6945
  addMotionAllModes(new ge());
6650
6946
  addMotionAllModes(new gE());
6651
- addMotionAllModes(new LastNonBlank());
6652
- addMotionAllModes(new MiddleOfScreenLine());
6947
+ addMotionAllModes(new g_());
6948
+ addMotionAllModes(new gm());
6653
6949
  addMotionAllModes(new MiddleOfLine());
6654
6950
  addMotionAllModes(new Column());
6655
6951
  addMotionAllModes(new Star());
@@ -6845,6 +7141,7 @@ var PatternRegistry = class {
6845
7141
  };
6846
7142
  export {
6847
7143
  Buffer,
7144
+ CONFIG_METADATA,
6848
7145
  Change,
6849
7146
  Command,
6850
7147
  ConfigManager,