Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 77 additions & 1 deletion CSharpMath.Core.Tests/Editor/KeyPressTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -402,5 +402,81 @@ public void Return(params K[] inputs) =>
T(@"\frac{\lim _{x\rightarrow 2}}{■}", K.LimitWithBase, K.SmallX, K.RightArrow, K.D2, K.Right, K.Slash),
]
public void Slash(string latex, params K[] inputs) => Test(latex, inputs);

[Theory]
[T(@"\log ", K.SmallL, K.SmallO, K.SmallG)]
[T(@"\ln ", K.SmallL, K.SmallN)]
[T(@"\exp ", K.SmallE, K.SmallX, K.SmallP)]
[T(@"\sin ", K.SmallS, K.SmallI, K.SmallN)]
[T(@"\cos ", K.SmallC, K.SmallO, K.SmallS)]
[T(@"\tan ", K.SmallT, K.SmallA, K.SmallN)]
[T(@"\sinh ", K.SmallS, K.SmallI, K.SmallN, K.SmallH)]
[T(@"\cosh ", K.SmallC, K.SmallO, K.SmallS, K.SmallH)]
[T(@"\tanh ", K.SmallT, K.SmallA, K.SmallN, K.SmallH)]
[T(@"\sec ", K.SmallS, K.SmallE, K.SmallC)]
[T(@"\csc ", K.SmallC, K.SmallS, K.SmallC)]
[T(@"\csc ", K.SmallC, K.SmallO, K.SmallS, K.SmallE, K.SmallC)]
[T(@"\cot ", K.SmallC, K.SmallO, K.SmallT)]
[T(@"\arcsin ", K.SmallA, K.SmallR, K.SmallC, K.SmallS, K.SmallI, K.SmallN)]
public void TypedFunctions(string latex, params K[] inputs) => Test(latex, inputs);

[Fact]
public void TypedFunctionDoesNotRewriteLongerIdentifier() {
Test(@"sinx", new[] { K.SmallS, K.SmallI, K.SmallN, K.SmallX });
}

[Fact]
public void BackspaceExpandsFunctionAndDeletesLastLetter() {
Test(@"si", new[] { K.SmallS, K.SmallI, K.SmallN, K.Backspace });
}

[Fact]
public void MovingIntoTypedFunctionMakesItsLettersEditable() {
Test(@"sicn", new[] { K.SmallS, K.SmallI, K.SmallN, K.Left, K.SmallC });
}

[Fact]
public void TypedFunctionInFraction() {
Test(@"\frac{\sin }{■}", new[] { K.SmallS, K.SmallI, K.SmallN, K.Slash });
}

[Fact]
public void TypedFunctionInScript() {
Test(@"1^{\cos }", new[] { K.D1, K.Power, K.SmallC, K.SmallO, K.SmallS });
}

[Fact]
public void TypedFunctionInRadical() {
Test(@"\sqrt{\tan }", new[] { K.SquareRoot, K.SmallT, K.SmallA, K.SmallN });
}

[Fact]
public void TypedFunctionInInner() {
Test(@"\left( \cot \right) ", new[] { K.BothRoundBrackets, K.SmallC, K.SmallO, K.SmallT });
}

[Fact]
public void DedicatedFunctionFollowedByLetterStaysAnOperator() {
Test(@"\sin x", new[] { K.Sine, K.SmallX });
}

[Fact]
public void DedicatedFunctionBackspaceRemovesTheOperator() {
Test(@"", new[] { K.Sine, K.Backspace });
}

[Fact]
public void DedicatedFunctionLeftMovementDoesNotUnfoldIt() {
Test(@"c\sin ", new[] { K.Sine, K.Left, K.SmallC });
}

[Fact]
public void MovingRightIntoTypedFunctionPlacesCaretInside() {
var keyboard = new MathKeyboard<TestFont, TGlyph>(context, new TestFont());
keyboard.KeyPress(K.SmallS, K.SmallI, K.SmallN);
keyboard.InsertionIndex = new MathListIndex(0);
keyboard.KeyPress(K.Right, K.SmallX);
Assert.Equal(@"sxin", keyboard.LaTeX);
}
}
}
}
136 changes: 134 additions & 2 deletions CSharpMath/Editor/MathKeyboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace CSharpMath.Editor {
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.CompilerServices;
using System.Timers;
using Atom;
using Display;
Expand Down Expand Up @@ -62,6 +63,10 @@ public bool InsertionPositionHighlighted {
public MathList MathList { get; } = [];
public string LaTeX => LaTeXParser.MathListToLaTeX(MathList).ToString();
private MathListIndex _insertionIndex = new(0);
// ConditionalWeakTable uses reference identity and does not retain operators
// after their enclosing math list has been removed.
readonly ConditionalWeakTable<MathAtom, object> _typedFunctions = new();
bool IsTypedFunction(MathAtom atom) => _typedFunctions.TryGetValue(atom, out _);
public MathListIndex InsertionIndex {
get => _insertionIndex;
set {
Expand Down Expand Up @@ -176,6 +181,13 @@ void InsertInner(string left, string right) =>
MathListSubIndexType.Inner);

void MoveCursorLeft() {
if (_insertionIndex.Previous is MathListIndex functionIndex &&
MathList.AtomAt(functionIndex) is Atoms.LargeOperator functionOperator && IsTypedFunction(functionOperator) &&
TryTypedFunctionName(functionOperator, out var functionName)) {
ExpandFunction(functionIndex, functionName, removeLastLetter: false);
MoveCursorLeft();
return;
}
var prev = _insertionIndex.Previous;
switch (MathList.AtomAt(prev)) {
case var _ when prev is null:
Expand Down Expand Up @@ -261,6 +273,14 @@ void MoveCursorLeft() {
void MoveCursorRight() {
if (_insertionIndex is null)
throw new InvalidOperationException($"{nameof(_insertionIndex)} is null.");
if (MathList.AtomAt(_insertionIndex) is Atoms.LargeOperator functionOperator && IsTypedFunction(functionOperator) &&
TryTypedFunctionName(functionOperator, out var functionName)) {
var functionIndex = _insertionIndex;
ExpandFunction(functionIndex, functionName, removeLastLetter: false);
_insertionIndex = ReplaceLeafIndex(_insertionIndex, functionIndex.FinalIndex);
MoveCursorRight();
return;
}
switch (MathList.AtomAt(_insertionIndex)) {
case null: // After Count
var levelDown = _insertionIndex.LevelDown();
Expand Down Expand Up @@ -348,7 +368,117 @@ void MoveCursorRight() {
void DeleteBackwards() {
// delete the last atom from the list
if (HasText && _insertionIndex.Previous is MathListIndex previous)
_insertionIndex = MathList.RemoveAt(previous);
if (MathList.AtomAt(previous) is Atoms.LargeOperator op && IsTypedFunction(op) && TryTypedFunctionName(op, out var functionName))
ExpandFunction(previous, functionName, removeLastLetter: true);
else
_insertionIndex = MathList.RemoveAt(previous);
}

// A function typed as ordinary letters is promoted to the same operator atom
// used by the corresponding keyboard button. Promotion is deliberately
// local to the current list, so it also works inside fractions, scripts and
// other inner lists.
MathList CurrentList() {
MathList Find(MathList list, MathListIndex index) {
if (index.SubIndexInfo is not { } info) return list;
var atom = list[index.AtomIndex];
var child = info.SubIndexType switch {
MathListSubIndexType.Superscript => atom.Superscript,
MathListSubIndexType.Subscript => atom.Subscript,
MathListSubIndexType.Numerator => ((Atoms.Fraction)atom).Numerator,
MathListSubIndexType.Denominator => ((Atoms.Fraction)atom).Denominator,
MathListSubIndexType.Radicand => ((Atoms.Radical)atom).Radicand,
MathListSubIndexType.Degree => ((Atoms.Radical)atom).Degree,
MathListSubIndexType.Inner => ((Atoms.Inner)atom).InnerList,
MathListSubIndexType.BetweenBaseAndScripts => list,
_ => throw new InvalidCodePathException("Unknown math list sub-index")
};
return Find(child, info.SubIndex);
}
return Find(MathList, _insertionIndex);
}
static MathListIndex ReplaceLeafIndex(MathListIndex index, int leafIndex) =>
index.SubIndexInfo is null
? new(leafIndex)
: new(index.AtomIndex, (index.SubIndexInfo.Value.SubIndexType,
ReplaceLeafIndex(index.SubIndexInfo.Value.SubIndex, leafIndex)));
static string? TypedFunctionCommand(string text) => text switch {
"log" => @"\log",
"ln" => @"\ln",
"exp" => @"\exp",
"sin" => @"\sin",
"cos" => @"\cos",
"tan" => @"\tan",
"sinh" => @"\sinh",
"cosh" => @"\cosh",
"tanh" => @"\tanh",
"sec" => @"\sec",
"csc" => @"\csc",
"cosec" => @"\csc",
"cot" => @"\cot",
"arcsin" => @"\arcsin",
"arccos" => @"\arccos",
"arctan" => @"\arctan",
"arccot" => @"\arccot",
"arcsec" => @"\arcsec",
"arccsc" => @"\arccsc",
_ => null
};
static bool TryTypedFunctionName(MathAtom atom, out string name) {
name = LaTeXSettings.CommandForAtom(atom) switch {
@"\log" => "log",
@"\ln" => "ln",
@"\exp" => "exp",
@"\sin" => "sin",
@"\cos" => "cos",
@"\tan" => "tan",
@"\sinh" => "sinh",
@"\cosh" => "cosh",
@"\tanh" => "tanh",
@"\sec" => "sec",
@"\csc" => "csc",
@"\cot" => "cot",
@"\arcsin" => "arcsin",
@"\arccos" => "arccos",
@"\arctan" => "arctan",
@"\arccot" => "arccot",
@"\arcsec" => "arcsec",
@"\arccsc" => "arccsc",
_ => null
} ?? "";
return name.Length != 0;
}
void ExpandFunction(MathListIndex operatorIndex, string name, bool removeLastLetter) {
var list = CurrentList();
var start = operatorIndex.FinalIndex;
var atom = list[start];
_typedFunctions.Remove(atom);
list.RemoveAt(start);
var letters = name.Substring(0, removeLastLetter ? name.Length - 1 : name.Length);
for (var i = 0; i < letters.Length; i++)
list.Insert(start + i, new Atoms.Variable(letters[i].ToString()));
_insertionIndex = ReplaceLeafIndex(_insertionIndex, start + letters.Length);
}
void ExpandPreviousFunction() {
if (_insertionIndex.Previous is MathListIndex previous &&
MathList.AtomAt(previous) is Atoms.LargeOperator op && IsTypedFunction(op) && TryTypedFunctionName(op, out var name))
ExpandFunction(previous, name, removeLastLetter: false);
}
void RecognizeTypedFunction() {
var list = CurrentList();
var end = _insertionIndex.FinalIndex;
var start = end;
const int maxFunctionNameLength = 7;
while (start > 0 && end - start < maxFunctionNameLength && list[start - 1] is Atoms.Variable) start--;
if (start > 0 && list[start - 1] is Atoms.Variable) return;
if (start == end) return;
var text = string.Concat(list.Atoms.GetRange(start, end - start).ConvertAll(a => a.Nucleus));
var command = TypedFunctionCommand(text);
if (command is null || LaTeXSettings.AtomForCommand(command) is not { } atom) return;
list.RemoveAtoms(start, end - start);
list.Insert(start, atom);
_typedFunctions.Add(atom, new object());
_insertionIndex = ReplaceLeafIndex(_insertionIndex, start + 1);
}

static bool IsPlaceholderList(MathList ml) => ml.Count == 1 && ml[0] is Atoms.Placeholder;
Expand Down Expand Up @@ -727,8 +857,10 @@ void InsertSymbolName(string name, bool subscript = false, bool superscript = fa
case MathKeyboardInput.SmallX:
case MathKeyboardInput.SmallY:
case MathKeyboardInput.SmallZ:
ExpandPreviousFunction();
InsertAtom(LaTeXSettings.AtomForCommand(new string((char)input, 1))
?? throw new InvalidCodePathException($"{nameof(LaTeXSettings.AtomForCommand)} returned null for {input}"));
RecognizeTypedFunction();
break;
case MathKeyboardInput.Alpha:
case MathKeyboardInput.Beta:
Expand Down Expand Up @@ -828,4 +960,4 @@ public void Dispose() {
((IDisposable)blinkTimer).Dispose();
}
}
}
}
Loading