Skip to content

Commit 47b9931

Browse files
committed
fix: fixed #2732, fixed #2786
1 parent 116e630 commit 47b9931

File tree

4 files changed

+204
-17
lines changed

4 files changed

+204
-17
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,24 @@
1919
- **Accessibility enhancements**: Auto-generated ARIA labels with speakable
2020
text, MathML fallback for screen readers, keyboard navigation support when
2121
focusable (Space/Enter to speak formula), automatic `role="img"` attribute
22+
- **#2732** Pressing the Space key in LaTeX mode now completes the LaTeX command
23+
and exits LaTeX mode, similar to pressing Enter. Previously, users typing
24+
LaTeX commands like `\alpha` would remain in LaTeX mode until pressing Enter,
25+
and incomplete commands would be lost when dismissing dialogs. Now pressing
26+
Space after a valid LaTeX command completes it and returns to math mode,
27+
making LaTeX input more intuitive and preventing data loss. Additionally, when
28+
typing LaTeX commands with mandatory arguments (like `\frac{1}{2}`), the
29+
command now auto-completes and exits LaTeX mode when the closing brace of the
30+
last mandatory argument is typed. The implementation uses the command registry
31+
to determine the exact number of mandatory arguments, ensuring correct
32+
behavior for all commands including those with nested arguments.
33+
- **#2786** Fixed scientific notation handling when creating fractions. When
34+
typing scientific notation like `5e-2` or `3.14×10^{-2}` and then pressing `/`
35+
to create a fraction, the entire expression now stays together in the
36+
numerator instead of being split apart. Previously, only part of the number
37+
would be included (e.g., just `2` from `5e-2`), breaking the scientific
38+
notation. The fix recognizes both e-notation (`5e-2`, `3.14e+10`) and ×10^
39+
notation (`3.14×10^{-2}`, `5×10^3`) as atomic units.
2240

2341
### Resolved Issues
2442

src/editor-mathfield/keyboard-input.ts

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import { moveAfterParent } from '../editor-model/commands-move';
1616
import { range } from '../editor-model/selection-utils';
1717

1818
import {
19-
acceptCommandSuggestion,
19+
complete,
2020
removeSuggestion,
2121
updateAutocomplete,
2222
} from './autocomplete';
23+
import { getLatexGroupBody } from './mode-editor-latex';
24+
import { getDefinition } from '../latex-commands/definitions-utils';
2325
import { requestUpdate } from './render';
2426
import type { _Mathfield } from './mathfield-private';
2527
import { removeIsolatedSpace, smartMode } from './smartmode';
@@ -244,6 +246,20 @@ export function onKeystroke(
244246
return success;
245247
}
246248

249+
// Handle Space key in LaTeX mode to complete and exit
250+
if (keystroke === '[Space]' && model.mode === 'latex') {
251+
// Try to complete the LaTeX command and exit LaTeX mode
252+
if (complete(mathfield, 'accept-all')) {
253+
mathfield.dirty = true;
254+
mathfield.scrollIntoView();
255+
if (evt.preventDefault) {
256+
evt.preventDefault();
257+
evt.stopPropagation();
258+
}
259+
return false;
260+
}
261+
}
262+
247263
if ((!selector || keystroke === '[Space]') && model.mode === 'math') {
248264
//
249265
// 5.5 If this is the Space bar and we're just before or right after
@@ -254,18 +270,6 @@ export function onKeystroke(
254270
// (the bias is reset when the selection changes)
255271
mathfield.styleBias = 'none';
256272

257-
// Check if there's an active autocomplete suggestion and accept it
258-
if (acceptCommandSuggestion(model)) {
259-
mathfield.snapshot('accept-suggestion');
260-
mathfield.dirty = true;
261-
mathfield.scrollIntoView();
262-
if (evt.preventDefault) {
263-
evt.preventDefault();
264-
evt.stopPropagation();
265-
}
266-
return false;
267-
}
268-
269273
// The space bar can be used to separate inline shortcuts
270274
mathfield.flushInlineShortcutBuffer();
271275

@@ -595,6 +599,60 @@ export function onInput(
595599
updateAutocomplete(mathfield);
596600
}
597601
);
602+
603+
// Check if we just typed a closing brace that completes all mandatory arguments
604+
// This needs to be done AFTER deferNotifications completes
605+
if (text === '}') {
606+
const latexBody = getLatexGroupBody(model);
607+
const latex = latexBody.map((x) => x.value).join('');
608+
609+
// Extract the command name (e.g., "\frac" from "\frac{1}{2}")
610+
const commandMatch = latex.match(/^\\([a-zA-Z]+)/);
611+
if (!commandMatch) return;
612+
613+
const commandName = '\\' + commandMatch[1];
614+
615+
// Look up the command definition
616+
const def = getDefinition(commandName, 'math');
617+
if (!def || def.definitionType !== 'function') return;
618+
619+
// Count the number of mandatory (non-optional) arguments
620+
const mandatoryArgCount = def.params.filter((p) => !p.isOptional).length;
621+
if (mandatoryArgCount === 0) return;
622+
623+
// Count how many complete brace pairs we have at the top level
624+
let depth = 0;
625+
let completedBraces = 0;
626+
let inCommandName = true;
627+
628+
for (let i = 0; i < latex.length; i++) {
629+
const char = latex[i];
630+
631+
// Skip the command name itself
632+
if (inCommandName) {
633+
if (char === '\\' || /[a-zA-Z]/.test(char)) continue;
634+
inCommandName = false;
635+
}
636+
637+
if (char === '{') {
638+
depth++;
639+
} else if (char === '}') {
640+
depth--;
641+
// Count a completed brace pair when we return to depth 0
642+
if (depth === 0) completedBraces++;
643+
}
644+
}
645+
646+
// Auto-complete only if we've completed all mandatory arguments
647+
// (depth is 0 and we have the right number of completed brace pairs)
648+
if (depth === 0 && completedBraces === mandatoryArgCount) {
649+
if (complete(mathfield, 'accept-all')) {
650+
mathfield.dirty = true;
651+
mathfield.scrollIntoView();
652+
return;
653+
}
654+
}
655+
}
598656
} else if (model.mode === 'text') {
599657
const style = { ...getSelectionStyle(model), ...mathfield.defaultStyle };
600658
for (const c of graphemes)

src/editor-mathfield/mode-editor-math.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,113 @@ function getImplicitArgOffset(model: _Model): Offset {
548548
return model.offsetOf(atom);
549549
}
550550

551+
/**
552+
* Check if the atom is part of a scientific notation pattern
553+
* Handles both: 5e-2 and 3.14×10^-2 formats
554+
*/
555+
function isPartOfScientificNotation(atom: Atom): boolean {
556+
// Pattern 1: 'e' notation (e.g., 5e-2, 3.14e+10)
557+
// Check if this is 'e' preceded by a digit
558+
if (atom.type === 'mord' && atom.value === 'e') {
559+
const left = atom.leftSibling;
560+
if (left && left.isDigit()) return true;
561+
}
562+
563+
// Check if this is '+' or '-' preceded by 'e' and that 'e' is preceded by a digit
564+
// Note: minus might be '-' (hyphen-minus) or '−' (minus sign U+2212)
565+
if (
566+
atom.type === 'mbin' &&
567+
(atom.value === '+' || atom.value === '-' || atom.value === '−')
568+
) {
569+
const left = atom.leftSibling;
570+
const right = atom.rightSibling;
571+
if (
572+
left &&
573+
left.type === 'mord' &&
574+
left.value === 'e' &&
575+
right &&
576+
right.isDigit()
577+
) {
578+
const leftLeft = left.leftSibling;
579+
if (leftLeft && leftLeft.isDigit()) return true;
580+
}
581+
}
582+
583+
// Pattern 2: ×10^ notation (e.g., 3.14×10^-2, 5×10^3)
584+
// The structure is: digit(s) × 1 0 subsup
585+
// where the subsup has the exponent in its superscript branch
586+
587+
// Check if this is a subsup atom (the ^ part after 10)
588+
if (atom.type === 'subsup') {
589+
// Check if left siblings are "0" and "1"
590+
const left1 = atom.leftSibling; // should be "0"
591+
if (left1 && left1.isDigit() && left1.value === '0') {
592+
const left2 = left1.leftSibling; // should be "1"
593+
if (left2 && left2.isDigit() && left2.value === '1') {
594+
// Check if preceded by × (times)
595+
const left3 = left2.leftSibling;
596+
if (left3 && left3.type === 'mbin' && (left3.value === '×' || left3.value === '\\times')) {
597+
// Check if the times is preceded by a digit
598+
const left4 = left3.leftSibling;
599+
if (left4 && left4.isDigit()) return true;
600+
}
601+
}
602+
}
603+
}
604+
605+
// Check if this is "0" that's part of "10^"
606+
if (atom.isDigit() && atom.value === '0') {
607+
const left = atom.leftSibling; // should be "1"
608+
const right = atom.rightSibling; // should be subsup
609+
if (
610+
left && left.isDigit() && left.value === '1' &&
611+
right && right.type === 'subsup'
612+
) {
613+
// Check if "1" is preceded by ×
614+
const left2 = left.leftSibling;
615+
if (left2 && left2.type === 'mbin' && (left2.value === '×' || left2.value === '\\times')) {
616+
// Check if × is preceded by a digit
617+
const left3 = left2.leftSibling;
618+
if (left3 && left3.isDigit()) return true;
619+
}
620+
}
621+
}
622+
623+
// Check if this is "1" that's part of "10^"
624+
if (atom.isDigit() && atom.value === '1') {
625+
const right1 = atom.rightSibling; // should be "0"
626+
if (right1 && right1.isDigit() && right1.value === '0') {
627+
const right2 = right1.rightSibling; // should be subsup
628+
if (right2 && right2.type === 'subsup') {
629+
// Check if preceded by ×
630+
const left = atom.leftSibling;
631+
if (left && left.type === 'mbin' && (left.value === '×' || left.value === '\\times')) {
632+
// Check if × is preceded by a digit
633+
const left2 = left.leftSibling;
634+
if (left2 && left2.isDigit()) return true;
635+
}
636+
}
637+
}
638+
}
639+
640+
// Check if this is × (times) in the pattern digit × 10^exponent
641+
if (atom.type === 'mbin' && (atom.value === '×' || atom.value === '\\times')) {
642+
const left = atom.leftSibling;
643+
const right1 = atom.rightSibling; // should be "1"
644+
if (left && left.isDigit() && right1 && right1.isDigit() && right1.value === '1') {
645+
const right2 = right1.rightSibling; // should be "0"
646+
if (right2 && right2.isDigit() && right2.value === '0') {
647+
const right3 = right2.rightSibling; // should be subsup
648+
if (right3 && right3.type === 'subsup') {
649+
return true;
650+
}
651+
}
652+
}
653+
}
654+
655+
return false;
656+
}
657+
551658
/**
552659
*
553660
* Predicate returns true if the atom should be considered an implicit argument.
@@ -559,6 +666,10 @@ function getImplicitArgOffset(model: _Model): Offset {
559666
function isImplicitArg(atom: Atom): boolean {
560667
// A digit, or a decimal point
561668
if (atom.isDigit()) return true;
669+
670+
// Check for scientific notation patterns
671+
if (isPartOfScientificNotation(atom)) return true;
672+
562673
if (
563674
atom.type &&
564675
/^(mord|surd|subsup|leftright|mop|mclose)$/.test(atom.type)

src/editor/keyboard-layout.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -300,10 +300,10 @@ export function setKeyboardLayout(
300300
export function getActiveKeyboardLayout(): KeyboardLayout {
301301
// if ((gKeyboardLayout ?? gKeyboardLayouts[0])?.displayName === 'French')
302302
// debugger;
303-
console.log(
304-
'Active keyboard layout:',
305-
(gKeyboardLayout ?? gKeyboardLayouts[0])?.displayName
306-
);
303+
// console.log(
304+
// 'Active keyboard layout:',
305+
// (gKeyboardLayout ?? gKeyboardLayouts[0])?.displayName
306+
// );
307307
return gKeyboardLayout ?? gKeyboardLayouts[0];
308308
}
309309

0 commit comments

Comments
 (0)