From c23bcecb284156deb5a3b155c72248ca4648b44f Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 24 Jan 2026 08:57:55 +0100 Subject: [PATCH 1/3] Started working on increment with underscores --- .../src/actions/incrementDecrement.ts | 80 ++++++++++++++----- 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/packages/cursorless-engine/src/actions/incrementDecrement.ts b/packages/cursorless-engine/src/actions/incrementDecrement.ts index 51b60709bf..3ffed1ae61 100644 --- a/packages/cursorless-engine/src/actions/incrementDecrement.ts +++ b/packages/cursorless-engine/src/actions/incrementDecrement.ts @@ -7,7 +7,7 @@ import { runForEachEditor } from "../util/targetUtils"; import type { Actions } from "./Actions"; import type { ActionReturnValue } from "./actions.types"; -const REGEX = /-?\d+(\.\d+)?/g; +const REGEX = /-?[\d_]+(\.[\d_]+)?/g; class IncrementDecrement { constructor( @@ -95,20 +95,21 @@ function updateNumber(isIncrement: boolean, text: string): string { } function updateInteger(isIncrement: boolean, text: string): string { - const original = parseInt(text); + const original = parseInt(text.replaceAll("_", "")); const diff = 1; const value = original + (isIncrement ? diff : -diff); return formatNumber(value, text); } function updateFloat(isIncrement: boolean, text: string): string { - const original = parseFloat(text); + const textWithoutUnderscores = text.replaceAll("_", ""); + const original = parseFloat(textWithoutUnderscores); const isPercentage = Math.abs(original) <= 1.0; const diff = isPercentage ? 0.1 : 1; const updated = original + (isIncrement ? diff : -diff); // Remove precision problems that would add a lot of extra digits const value = parseFloat(updated.toPrecision(15)) / 1; - const decimalPlaces = text.split(".")[1]?.length; + const decimalPlaces = textWithoutUnderscores.split(".")[1]?.length; return formatNumber(value, text, decimalPlaces); } @@ -120,26 +121,65 @@ function formatNumber( const sign = value < 0 ? "-" : ""; const absValue = Math.abs(value); - if (hasLeadingZeros(text)) { - const integerPartLength = getIntegerPartLength(text); - const integerPart = Math.floor(absValue) - .toString() - .padStart(integerPartLength, "0"); - - if (decimalPlaces != null) { - const fractionPart = (absValue - Math.floor(absValue)) - .toFixed(decimalPlaces) - // Remove "0." - .slice(2); - return `${sign}${integerPart}.${fractionPart}`; + const result = (() => { + if (hasLeadingZeros(text)) { + return formatNumberWithLeadingZeros(absValue, sign, text, decimalPlaces); } - return `${sign}${integerPart}`; + return decimalPlaces != null + ? value.toFixed(decimalPlaces) + : value.toString(); + })(); + + return formatNumberWithUnderscores(text, result); +} + +function formatNumberWithLeadingZeros( + absValue: number, + sign: string, + text: string, + decimalPlaces?: number, +): string { + const integerPartLength = getIntegerPartLength(text); + const integerPart = Math.floor(absValue) + .toString() + .padStart(integerPartLength, "0"); + + if (decimalPlaces != null) { + const fractionPart = (absValue - Math.floor(absValue)) + .toFixed(decimalPlaces) + // Remove "0." + .slice(2); + return `${sign}${integerPart}.${fractionPart}`; + } + + return `${sign}${integerPart}`; +} + +function formatNumberWithUnderscores( + original: string, + updated: string, +): string { + const underscoreMatches = Array.from(original.matchAll(/_/g)); + + if (underscoreMatches.length === 0) { + return updated; + } + + // Reinsert underscores at original position + let resultWithUnderscores = updated; + + for (const match of underscoreMatches) { + const index = match.index; + if (index < resultWithUnderscores.length) { + resultWithUnderscores = + resultWithUnderscores.slice(0, index) + + "_" + + resultWithUnderscores.slice(index); + } } - return decimalPlaces != null - ? value.toFixed(decimalPlaces) - : value.toString(); + return resultWithUnderscores; } function hasLeadingZeros(text: string): boolean { From 4e7f78df5dc52ada2a6bb4903ae385e5f08aa024 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 24 Jan 2026 15:52:38 +0100 Subject: [PATCH 2/3] More fixes --- .../incrementDecrement/decrementFile3.yml | 56 +++++++++++++++++++ .../incrementDecrement/incrementFile3.yml | 56 +++++++++++++++++++ .../incrementDecrement/incrementThis.yml | 14 +---- .../src/actions/incrementDecrement.ts | 29 ++++++---- 4 files changed, 130 insertions(+), 25 deletions(-) create mode 100644 data/fixtures/recorded/actions/incrementDecrement/decrementFile3.yml create mode 100644 data/fixtures/recorded/actions/incrementDecrement/incrementFile3.yml diff --git a/data/fixtures/recorded/actions/incrementDecrement/decrementFile3.yml b/data/fixtures/recorded/actions/incrementDecrement/decrementFile3.yml new file mode 100644 index 0000000000..d3c7105d48 --- /dev/null +++ b/data/fixtures/recorded/actions/incrementDecrement/decrementFile3.yml @@ -0,0 +1,56 @@ +languageId: plaintext +command: + version: 7 + spokenForm: decrement file + action: + name: decrement + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: document} + usePrePhraseSnapshot: false +initialState: + documentContents: |- + 10_000 + 10_000.0 + 10_000.0_10 + 0_0.0_1 + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |- + 99_99 + 99_99.0 + 99_99.01_0 + -0_0.0_9 + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - type: UntypedTarget + contentRange: + start: {line: 0, character: 0} + end: {line: 0, character: 5} + isReversed: false + hasExplicitRange: true + - type: UntypedTarget + contentRange: + start: {line: 1, character: 0} + end: {line: 1, character: 7} + isReversed: false + hasExplicitRange: true + - type: UntypedTarget + contentRange: + start: {line: 2, character: 0} + end: {line: 2, character: 10} + isReversed: false + hasExplicitRange: true + - type: UntypedTarget + contentRange: + start: {line: 3, character: 0} + end: {line: 3, character: 8} + isReversed: false + hasExplicitRange: true diff --git a/data/fixtures/recorded/actions/incrementDecrement/incrementFile3.yml b/data/fixtures/recorded/actions/incrementDecrement/incrementFile3.yml new file mode 100644 index 0000000000..f3c370ae9d --- /dev/null +++ b/data/fixtures/recorded/actions/incrementDecrement/incrementFile3.yml @@ -0,0 +1,56 @@ +languageId: plaintext +command: + version: 7 + spokenForm: increment file + action: + name: increment + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: document} + usePrePhraseSnapshot: false +initialState: + documentContents: |- + 10_000 + 10_000.0 + 10_000.0_10 + 0_0.0_1 + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: |- + 10_001 + 10_001.0 + 10_001.0_10 + 0_0.1_1 + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - type: UntypedTarget + contentRange: + start: {line: 0, character: 0} + end: {line: 0, character: 6} + isReversed: false + hasExplicitRange: true + - type: UntypedTarget + contentRange: + start: {line: 1, character: 0} + end: {line: 1, character: 8} + isReversed: false + hasExplicitRange: true + - type: UntypedTarget + contentRange: + start: {line: 2, character: 0} + end: {line: 2, character: 11} + isReversed: false + hasExplicitRange: true + - type: UntypedTarget + contentRange: + start: {line: 3, character: 0} + end: {line: 3, character: 7} + isReversed: false + hasExplicitRange: true diff --git a/data/fixtures/recorded/actions/incrementDecrement/incrementThis.yml b/data/fixtures/recorded/actions/incrementDecrement/incrementThis.yml index 8d0407784f..be704aeb1b 100644 --- a/data/fixtures/recorded/actions/incrementDecrement/incrementThis.yml +++ b/data/fixtures/recorded/actions/incrementDecrement/incrementThis.yml @@ -15,7 +15,7 @@ initialState: active: {line: 0, character: 8} marks: {} finalState: - documentContents: "2_02_002" + documentContents: "1_01_002" selections: - anchor: {line: 0, character: 8} active: {line: 0, character: 8} @@ -23,18 +23,6 @@ finalState: - type: UntypedTarget contentRange: start: {line: 0, character: 0} - end: {line: 0, character: 1} - isReversed: false - hasExplicitRange: true - - type: UntypedTarget - contentRange: - start: {line: 0, character: 2} - end: {line: 0, character: 4} - isReversed: false - hasExplicitRange: true - - type: UntypedTarget - contentRange: - start: {line: 0, character: 5} end: {line: 0, character: 8} isReversed: false hasExplicitRange: true diff --git a/packages/cursorless-engine/src/actions/incrementDecrement.ts b/packages/cursorless-engine/src/actions/incrementDecrement.ts index 3ffed1ae61..3dc25015dd 100644 --- a/packages/cursorless-engine/src/actions/incrementDecrement.ts +++ b/packages/cursorless-engine/src/actions/incrementDecrement.ts @@ -95,10 +95,11 @@ function updateNumber(isIncrement: boolean, text: string): string { } function updateInteger(isIncrement: boolean, text: string): string { - const original = parseInt(text.replaceAll("_", "")); + const textWithoutUnderscores = text.replaceAll("_", ""); + const original = parseInt(textWithoutUnderscores); const diff = 1; const value = original + (isIncrement ? diff : -diff); - return formatNumber(value, text); + return formatNumber(value, text, textWithoutUnderscores); } function updateFloat(isIncrement: boolean, text: string): string { @@ -110,20 +111,22 @@ function updateFloat(isIncrement: boolean, text: string): string { // Remove precision problems that would add a lot of extra digits const value = parseFloat(updated.toPrecision(15)) / 1; const decimalPlaces = textWithoutUnderscores.split(".")[1]?.length; - return formatNumber(value, text, decimalPlaces); + return formatNumber(value, text, textWithoutUnderscores, decimalPlaces); } function formatNumber( value: number, text: string, + textWithoutUnderscores: string, decimalPlaces?: number, ): string { - const sign = value < 0 ? "-" : ""; - const absValue = Math.abs(value); - const result = (() => { - if (hasLeadingZeros(text)) { - return formatNumberWithLeadingZeros(absValue, sign, text, decimalPlaces); + if (hasLeadingZeros(textWithoutUnderscores)) { + return formatNumberWithLeadingZeros( + value, + textWithoutUnderscores, + decimalPlaces, + ); } return decimalPlaces != null @@ -135,11 +138,12 @@ function formatNumber( } function formatNumberWithLeadingZeros( - absValue: number, - sign: string, + value: number, text: string, decimalPlaces?: number, ): string { + const sign = value < 0 ? "-" : ""; + const absValue = Math.abs(value); const integerPartLength = getIntegerPartLength(text); const integerPart = Math.floor(absValue) .toString() @@ -156,6 +160,7 @@ function formatNumberWithLeadingZeros( return `${sign}${integerPart}`; } +// Reinsert underscores at original positions function formatNumberWithUnderscores( original: string, updated: string, @@ -166,11 +171,11 @@ function formatNumberWithUnderscores( return updated; } - // Reinsert underscores at original position let resultWithUnderscores = updated; + const offset = (updated[0] === "-" ? 1 : 0) - (original[0] === "-" ? 1 : 0); for (const match of underscoreMatches) { - const index = match.index; + const index = match.index + offset; if (index < resultWithUnderscores.length) { resultWithUnderscores = resultWithUnderscores.slice(0, index) + From dc2b8b99fca4e3964a4981f18572a84d487fe6c9 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 24 Jan 2026 16:17:38 +0100 Subject: [PATCH 3/3] More stable underscores --- .../incrementDecrement/decrementFile3.yml | 16 ++++++++++++---- .../incrementDecrement/incrementFile3.yml | 10 +++++++++- .../src/actions/incrementDecrement.ts | 12 +++++++++--- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/data/fixtures/recorded/actions/incrementDecrement/decrementFile3.yml b/data/fixtures/recorded/actions/incrementDecrement/decrementFile3.yml index d3c7105d48..588a1171b2 100644 --- a/data/fixtures/recorded/actions/incrementDecrement/decrementFile3.yml +++ b/data/fixtures/recorded/actions/incrementDecrement/decrementFile3.yml @@ -15,6 +15,7 @@ initialState: 10_000 10_000.0 10_000.0_10 + 010_000.0 0_0.0_1 selections: - anchor: {line: 0, character: 0} @@ -22,9 +23,10 @@ initialState: marks: {} finalState: documentContents: |- - 99_99 - 99_99.0 - 99_99.01_0 + 9_999 + 9_999.0 + 9_999.0_10 + 009_999.0 -0_0.0_9 selections: - anchor: {line: 0, character: 0} @@ -51,6 +53,12 @@ finalState: - type: UntypedTarget contentRange: start: {line: 3, character: 0} - end: {line: 3, character: 8} + end: {line: 3, character: 9} + isReversed: false + hasExplicitRange: true + - type: UntypedTarget + contentRange: + start: {line: 4, character: 0} + end: {line: 4, character: 8} isReversed: false hasExplicitRange: true diff --git a/data/fixtures/recorded/actions/incrementDecrement/incrementFile3.yml b/data/fixtures/recorded/actions/incrementDecrement/incrementFile3.yml index f3c370ae9d..9424308700 100644 --- a/data/fixtures/recorded/actions/incrementDecrement/incrementFile3.yml +++ b/data/fixtures/recorded/actions/incrementDecrement/incrementFile3.yml @@ -15,6 +15,7 @@ initialState: 10_000 10_000.0 10_000.0_10 + 010_000.0 0_0.0_1 selections: - anchor: {line: 0, character: 0} @@ -25,6 +26,7 @@ finalState: 10_001 10_001.0 10_001.0_10 + 010_001.0 0_0.1_1 selections: - anchor: {line: 0, character: 0} @@ -51,6 +53,12 @@ finalState: - type: UntypedTarget contentRange: start: {line: 3, character: 0} - end: {line: 3, character: 7} + end: {line: 3, character: 9} + isReversed: false + hasExplicitRange: true + - type: UntypedTarget + contentRange: + start: {line: 4, character: 0} + end: {line: 4, character: 7} isReversed: false hasExplicitRange: true diff --git a/packages/cursorless-engine/src/actions/incrementDecrement.ts b/packages/cursorless-engine/src/actions/incrementDecrement.ts index 3dc25015dd..776b07e231 100644 --- a/packages/cursorless-engine/src/actions/incrementDecrement.ts +++ b/packages/cursorless-engine/src/actions/incrementDecrement.ts @@ -134,13 +134,13 @@ function formatNumber( : value.toString(); })(); - return formatNumberWithUnderscores(text, result); + return formatNumberWithUnderscores(text, textWithoutUnderscores, result); } function formatNumberWithLeadingZeros( value: number, text: string, - decimalPlaces?: number, + decimalPlaces: number | undefined, ): string { const sign = value < 0 ? "-" : ""; const absValue = Math.abs(value); @@ -163,6 +163,7 @@ function formatNumberWithLeadingZeros( // Reinsert underscores at original positions function formatNumberWithUnderscores( original: string, + originalWithoutUnderscores: string, updated: string, ): string { const underscoreMatches = Array.from(original.matchAll(/_/g)); @@ -172,7 +173,12 @@ function formatNumberWithUnderscores( } let resultWithUnderscores = updated; - const offset = (updated[0] === "-" ? 1 : 0) - (original[0] === "-" ? 1 : 0); + const signOffset = + (updated[0] === "-" ? 1 : 0) - (original[0] === "-" ? 1 : 0); + const intOffset = + getIntegerPartLength(updated) - + getIntegerPartLength(originalWithoutUnderscores); + const offset = signOffset + intOffset; for (const match of underscoreMatches) { const index = match.index + offset;