|
|
@@ -9,6 +9,8 @@
|
|
|
/// 脚本式输入还是文本方便)。两者序列化后交给同一个求值内核。
|
|
|
library;
|
|
|
|
|
|
+import 'package:meta/meta.dart';
|
|
|
+
|
|
|
/// 表达式节点。
|
|
|
sealed class ExprNode {
|
|
|
const ExprNode();
|
|
|
@@ -50,6 +52,15 @@ class GroupNode extends ExprNode {
|
|
|
GroupNode(this.inner);
|
|
|
}
|
|
|
|
|
|
+/// 花括号列表 `{ … }`。
|
|
|
+///
|
|
|
+/// 计算器自己不用它(Sum 的迭代范围是一个字面量),但**粘贴**进来的表达式
|
|
|
+/// 可能带花括号(`Sum[f, {n, 1, Infinity}]`),没有这个节点就解析不了。
|
|
|
+class BraceNode extends ExprNode {
|
|
|
+ SeqNode inner;
|
|
|
+ BraceNode(this.inner);
|
|
|
+}
|
|
|
+
|
|
|
/// 后缀运算符,目前只有阶乘 `!`。
|
|
|
class PostfixNode extends ExprNode {
|
|
|
SeqNode operand;
|
|
|
@@ -122,6 +133,33 @@ class SlotPath {
|
|
|
Object.hash(offset, Object.hashAll(steps.map((s) => Object.hash(s.child, s.slot))));
|
|
|
}
|
|
|
|
|
|
+/// 把一个槽序列化成 Symja 表达式。
|
|
|
+///
|
|
|
+/// 序列化不依赖任何文档状态,所以给出这个顶层入口,
|
|
|
+/// 免得为了转个字符串还要 new 一个 ExprDoc。
|
|
|
+String symjaOf(SeqNode seq) => ExprDoc().serializeSeq(seq);
|
|
|
+
|
|
|
+/// 选区:**同一个槽里**一段连续的子节点。
|
|
|
+///
|
|
|
+/// 不做跨槽的任意范围 —— 那要处理"从分子选到分母"这种语义模糊的情况,
|
|
|
+/// 而实际用起来,想剪切的东西几乎总在同一层(`2x+1` 就是根槽里连续的四个节点)。
|
|
|
+@immutable
|
|
|
+class SlotRange {
|
|
|
+ final SlotPath path;
|
|
|
+ final int start; // 含
|
|
|
+ final int end; // 不含
|
|
|
+ const SlotRange(this.path, this.start, this.end);
|
|
|
+
|
|
|
+ bool get isEmpty => end <= start;
|
|
|
+
|
|
|
+ @override
|
|
|
+ bool operator ==(Object other) =>
|
|
|
+ other is SlotRange && other.path == path && other.start == start && other.end == end;
|
|
|
+
|
|
|
+ @override
|
|
|
+ int get hashCode => Object.hash(path, start, end);
|
|
|
+}
|
|
|
+
|
|
|
/// 编辑器文档:持有一棵树和一个光标,并实现所有编辑操作。
|
|
|
///
|
|
|
/// 可变对象,调用方自己 setState。刻意不做成 ChangeNotifier —— 操作很密集
|
|
|
@@ -130,6 +168,64 @@ class ExprDoc {
|
|
|
final SeqNode root = SeqNode();
|
|
|
SlotPath cursor = SlotPath.root;
|
|
|
|
|
|
+ /// 当前选区。null = 没选中(此时操作都作用在光标处)。
|
|
|
+ SlotRange? selection;
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------ 选区
|
|
|
+
|
|
|
+ void selectNodeAt(SlotPath path, int index) {
|
|
|
+ selection = SlotRange(path, index, index + 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 把选区扩展到包含第 [index] 个节点(按住不放再点另一个节点的语义)。
|
|
|
+ /// 只支持同一个槽内,跨槽忽略。
|
|
|
+ void extendSelectionTo(SlotPath path, int index) {
|
|
|
+ final s = selection;
|
|
|
+ if (s == null || !s.path.sameSlotAs(path)) return;
|
|
|
+ selection = SlotRange(
|
|
|
+ s.path,
|
|
|
+ index < s.start ? index : s.start,
|
|
|
+ index + 1 > s.end ? index + 1 : s.end,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ void clearSelection() => selection = null;
|
|
|
+
|
|
|
+ /// 选中的节点(按原位序)。
|
|
|
+ List<ExprNode> get selectedNodes {
|
|
|
+ final s = selection;
|
|
|
+ if (s == null || s.isEmpty) return const <ExprNode>[];
|
|
|
+ final children = slotAt(s.path).children;
|
|
|
+ final from = s.start.clamp(0, children.length);
|
|
|
+ final to = s.end.clamp(0, children.length);
|
|
|
+ return children.sublist(from, to);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 选区的 Symja 文本(复制到剪贴板的内容)。
|
|
|
+ String get selectionText => symjaOf(SeqNode(selectedNodes));
|
|
|
+
|
|
|
+ /// 删掉选区,光标落到选区起点。
|
|
|
+ void deleteSelection() {
|
|
|
+ final s = selection;
|
|
|
+ if (s == null || s.isEmpty) return;
|
|
|
+ final children = slotAt(s.path).children;
|
|
|
+ final from = s.start.clamp(0, children.length);
|
|
|
+ final to = s.end.clamp(0, children.length);
|
|
|
+ children.removeRange(from, to);
|
|
|
+ cursor = SlotPath(s.path.steps, from);
|
|
|
+ selection = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 在光标处插入一串节点(粘贴用),插完光标落在它们之后。
|
|
|
+ void insertNodesAtCursor(List<ExprNode> nodes) {
|
|
|
+ if (nodes.isEmpty) return;
|
|
|
+ final seq = current;
|
|
|
+ final at = cursor.offset.clamp(0, seq.children.length);
|
|
|
+ seq.children.insertAll(at, nodes);
|
|
|
+ cursor = SlotPath(cursor.steps, at + nodes.length);
|
|
|
+ selection = null;
|
|
|
+ }
|
|
|
+
|
|
|
// ------------------------------------------------------------------ 取槽
|
|
|
|
|
|
/// 按路径找到对应的槽。路径失效时返回根槽。
|
|
|
@@ -150,6 +246,7 @@ class ExprDoc {
|
|
|
FracNode f => [f.num, f.den], // 0=分子 1=分母
|
|
|
PowNode p => [p.base, p.exp], // 0=底 1=指数
|
|
|
GroupNode g => [g.inner],
|
|
|
+ BraceNode b => [b.inner],
|
|
|
PostfixNode p => [p.operand],
|
|
|
LitNode _ => const <SeqNode>[],
|
|
|
};
|
|
|
@@ -235,55 +332,6 @@ class ExprDoc {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- /// 槽尾是不是运算符 —— 说明用户这句还没写完。
|
|
|
- static const _operators = {'+', '-', '*', '/', '^', ','};
|
|
|
-
|
|
|
- static bool _endsWithOperator(SeqNode seq) {
|
|
|
- if (seq.children.isEmpty) return false;
|
|
|
- final last = seq.children.last;
|
|
|
- return last is LitNode && _operators.contains(last.text);
|
|
|
- }
|
|
|
-
|
|
|
- /// 单参数(或多参数带预置)函数键的行为。
|
|
|
- ///
|
|
|
- /// 两种情况,按"用户此刻想干什么"分:
|
|
|
- ///
|
|
|
- /// 1. **光标在槽尾、槽里有内容、且最后不是运算符** → 把**整槽内容**包进函数。
|
|
|
- /// 用户打完 `2x+1` 按 `ln`,要的是 `Log[2x+1]`,不是 `Log[]` 再让他重打。
|
|
|
- /// (注意是**整槽**不是"左边那个节点" —— `2x+1` 是四个节点,
|
|
|
- /// 只包最后一个会变成 `2x+Log[1]`。)
|
|
|
- ///
|
|
|
- /// 2. **其余情况** → 插一个空模板,光标进括号里。
|
|
|
- /// 空槽时按 `ln` 就是这种情况;光标停在中间时也是。
|
|
|
- ///
|
|
|
- /// 一个刻意的例外:槽尾是运算符(用户刚打完 `1+`)**不包**。
|
|
|
- /// 那时他想要的是 `1+Log[...]`,包起来会变成 `Log[1+]`。
|
|
|
- ///
|
|
|
- /// [prefill] 里的第一项留空表示"由上面两种情况决定",其余项原样预置。
|
|
|
- void applyFunction(String name, {List<String> prefill = const []}) {
|
|
|
- final seq = current;
|
|
|
- final atEnd = cursor.offset >= seq.children.length;
|
|
|
- final shouldWrap =
|
|
|
- atEnd && seq.children.isNotEmpty && !_endsWithOperator(seq);
|
|
|
-
|
|
|
- // prefill 的第一项是"要填的那个参数",包裹时被整槽内容顶替;
|
|
|
- // 没有 prefill 的函数(Sin、Log…)就只有一个参数。
|
|
|
- final rest = prefill.isEmpty ? const <String>[] : prefill.sublist(1);
|
|
|
-
|
|
|
- if (!shouldWrap) {
|
|
|
- makeFunction(name, prefill: prefill);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- final inner = SeqNode(List<ExprNode>.from(seq.children));
|
|
|
- seq.children.clear();
|
|
|
- seq.children.add(FuncNode(name, [
|
|
|
- inner,
|
|
|
- for (final p in rest) p.isEmpty ? SeqNode() : SeqNode([LitNode(p)]),
|
|
|
- ]));
|
|
|
- cursor = SlotPath(cursor.steps, 1);
|
|
|
- }
|
|
|
-
|
|
|
/// 切到当前函数调用的下一个参数;已经是最后一个就新建一个参数槽。
|
|
|
///
|
|
|
/// 只能用在函数参数里(光标路径的最后一跳指向某个 FuncNode 的参数槽时)。
|
|
|
@@ -367,6 +415,14 @@ class ExprDoc {
|
|
|
cursor = up;
|
|
|
}
|
|
|
|
|
|
+ /// 用另一个槽的内容整体替换(粘贴用)。光标落到末尾 —— 粘完接着打最自然。
|
|
|
+ void replaceWith(SeqNode other) {
|
|
|
+ root.children
|
|
|
+ ..clear()
|
|
|
+ ..addAll(other.children);
|
|
|
+ cursor = SlotPath(const <SlotStep>[], root.children.length);
|
|
|
+ }
|
|
|
+
|
|
|
void clear() {
|
|
|
root.children.clear();
|
|
|
cursor = SlotPath.root;
|
|
|
@@ -390,14 +446,53 @@ class ExprDoc {
|
|
|
FracNode f => '${_grouped(serializeSeq(f.num))}/${_grouped(serializeSeq(f.den))}',
|
|
|
PowNode p => '${_grouped(serializeSeq(p.base))}^${_grouped(serializeSeq(p.exp))}',
|
|
|
GroupNode g => '(${serializeSeq(g.inner)})',
|
|
|
+ BraceNode b => '{${serializeSeq(b.inner)}}',
|
|
|
PostfixNode p => '${_grouped(serializeSeq(p.operand))}${p.op}',
|
|
|
};
|
|
|
|
|
|
/// 复合结构当操作数时要加括号,否则 `x+1` 当分母会算错优先级。
|
|
|
+ ///
|
|
|
+ /// 但**不多加**:整体已经被一对括号包住的(`(x+1)`)不再套一层成 `((x+1))`;
|
|
|
+ /// 单个项(字面量、函数调用)本来就不需要括号。
|
|
|
String _grouped(String s) {
|
|
|
if (s.isEmpty) return s;
|
|
|
- final needsParens = s.contains(RegExp(r'[+\-*/^]')) ||
|
|
|
- (s.contains(' ') && s.contains(','));
|
|
|
- return needsParens ? '($s)' : s;
|
|
|
+ if (_isFullyParenthesized(s)) return s;
|
|
|
+ if (_isSimpleTerm(s)) return s;
|
|
|
+ return '($s)';
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 整个串恰好被一对括号包住(括号配平、且中间没有提前闭合)。
|
|
|
+ static bool _isFullyParenthesized(String s) {
|
|
|
+ if (!s.startsWith('(') || !s.endsWith(')')) return false;
|
|
|
+ var depth = 0;
|
|
|
+ for (var i = 0; i < s.length; i++) {
|
|
|
+ if (s[i] == '(') {
|
|
|
+ depth++;
|
|
|
+ } else if (s[i] == ')') {
|
|
|
+ depth--;
|
|
|
+ if (depth == 0) return i == s.length - 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 顶层没有运算符 —— 单个字面量、函数调用或括号组,当操作数不需要括号。
|
|
|
+ ///
|
|
|
+ /// `[` `]` `{` `}` 也算一层,所以 `Sin[x+1]` 里的 `+` 不算顶层运算符 ——
|
|
|
+ /// 它本来就不用括号。开头的正负号是正负号不是运算符,跳过。
|
|
|
+ static bool _isSimpleTerm(String s) {
|
|
|
+ var depth = 0;
|
|
|
+ for (var i = 0; i < s.length; i++) {
|
|
|
+ final c = s[i];
|
|
|
+ if (c == '(' || c == '[' || c == '{') {
|
|
|
+ depth++;
|
|
|
+ } else if (c == ')' || c == ']' || c == '}') {
|
|
|
+ depth--;
|
|
|
+ } else if (depth == 0 && '+-*/^'.contains(c)) {
|
|
|
+ if (i == 0 && (c == '-' || c == '+')) continue;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true;
|
|
|
}
|
|
|
}
|