Skip to content

Commit 4e3da34

Browse files
committed
[IMP] composer: prettify formula when content is too long.
Prettify formula depending on the content width. task: 4735172
1 parent 68bed3e commit 4e3da34

File tree

4 files changed

+229
-3
lines changed

4 files changed

+229
-3
lines changed

src/components/composer/composer/abstract_composer_store.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -534,11 +534,12 @@ export abstract class AbstractComposerStore extends SpreadsheetStore {
534534
if (isNewCurrentContent || this.editionMode !== "inactive") {
535535
const locale = this.getters.getLocale();
536536
this.currentTokens = isFormula(text) ? composerTokenize(text, locale) : [];
537-
if (this.currentTokens.length > 100) {
537+
// this._currentContent = prettify(parseTokens(this.currentTokens));
538+
if (this.currentTokens.length > 500) {
538539
if (raise) {
539540
this.notificationStore.raiseError(
540541
_t(
541-
"This formula has over 100 parts. It can't be processed properly, consider splitting it into multiple cells"
542+
"This formula has over 500 parts. It can't be processed properly, consider splitting it into multiple cells"
542543
)
543544
);
544545
}

src/components/composer/composer/cell_composer_store.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { parseTokens } from "../../../formulas/parser";
12
import { parseLiteral } from "../../../helpers/cells";
23
import {
34
formatValue,
@@ -26,6 +27,7 @@ import {
2627
isMatrix,
2728
} from "../../../types";
2829
import { AbstractComposerStore } from "./abstract_composer_store";
30+
import { prettify } from "./prettifier_content";
2931

3032
const CELL_DELETED_MESSAGE = _t("The cell you are trying to edit has been deleted.");
3133

@@ -202,7 +204,10 @@ export class CellComposerStore extends AbstractComposerStore {
202204
const locale = this.getters.getLocale();
203205
const cell = this.getters.getCell(position);
204206
if (cell?.isFormula) {
205-
return localizeFormula(cell.content, locale);
207+
const pretifiedContent = cell.compiledFormula.isBadExpression
208+
? cell.content
209+
: prettify(parseTokens(cell.compiledFormula.tokens));
210+
return localizeFormula(pretifiedContent, locale);
206211
}
207212
const spreader = this.model.getters.getArrayFormulaSpreadingOn(position);
208213
if (spreader) {

src/components/composer/composer/composer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ css/* scss */ `
6060
padding-right: 3px;
6161
outline: none;
6262
63+
tab-size: 4;
64+
6365
p {
6466
margin-bottom: 0px;
6567
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { AST } from "../../../formulas/parser";
2+
3+
// ---------------------------------------
4+
// Prettify rule
5+
// ---------------------------------------
6+
7+
// PrettifyRule is a data structure that represents how to format an AST into a human-readable string.
8+
// We find there all the formatting structures like line breaks, indentation and token grouping.
9+
// It is used to create a layout that fits depending on the available width.
10+
11+
type PrettifyRule = string | ChooseBetween | Concat | Nest | Line;
12+
type ChooseBetween = { type: "chooseBetween"; a: PrettifyRule; b: PrettifyRule };
13+
type Concat = { type: "concat"; rules: PrettifyRule[] };
14+
type Nest = { type: "nest"; indent: number; a: PrettifyRule };
15+
type Line = { type: "line" };
16+
17+
/** Useful for indicating where to insert a new line. Placed in a group, it will be used if there is insufficient space*/
18+
function line(): Line {
19+
return { type: "line" };
20+
}
21+
22+
/** Useful for indicating where to insert a new line with a specific indentation level.
23+
* Should be placed before a line. Placed in a group, it will be used if there is insufficient space*/
24+
function nest(indent: number, x: PrettifyRule): Nest {
25+
return { type: "nest", indent, a: x };
26+
}
27+
28+
/** Useful for join few rules into a single rule.
29+
* Note that, this is the only way to melt strings (depending the AST value)
30+
* with other rules (ChooseBetween | Concat | Nest | Line)
31+
*/
32+
function join(rules: PrettifyRule[]): Concat {
33+
return { type: "concat", rules };
34+
}
35+
36+
/** Useful for indicating where to print a rule into a single line depending on the available space.*/
37+
function group(x: PrettifyRule): ChooseBetween {
38+
return chooseBetween(flatten(x), x);
39+
}
40+
41+
/** Used exclusively for `group`, indicating that we can choose between two rules depending on the available space.*/
42+
function chooseBetween(a: PrettifyRule, b: PrettifyRule): ChooseBetween {
43+
return { type: "chooseBetween", a, b };
44+
}
45+
46+
/** Recursive function used exclusively for `group`, to indicate how to flatten the rules.*/
47+
function flatten(x: PrettifyRule): PrettifyRule {
48+
if (typeof x === "string") {
49+
return x;
50+
}
51+
if (x.type === "chooseBetween") {
52+
// normally should be "chooseBetween(flatten(x.a), flatten(x.b))" but compute time is too high
53+
return flatten(x.a);
54+
}
55+
if (x.type === "concat") {
56+
return join(x.rules.map(flatten));
57+
}
58+
if (x.type === "nest") {
59+
return {
60+
type: "nest",
61+
indent: x.indent,
62+
a: flatten(x.a),
63+
};
64+
}
65+
if (x.type === "line") {
66+
return " ";
67+
}
68+
return x;
69+
}
70+
71+
// ---------------------------------------
72+
// printer part
73+
// ---------------------------------------
74+
75+
// Use the PrettifyRule to create a layout that fits within a given width.
76+
77+
type SubLine = {
78+
type: "subline";
79+
indent: number;
80+
rule: SubRule;
81+
};
82+
type SubText = {
83+
type: "subtext";
84+
text: string;
85+
rule: SubRule;
86+
};
87+
88+
type SubRule = SubLine | SubText | null;
89+
type RuleFit = [number, PrettifyRule];
90+
91+
/**
92+
* Print a PrettifyRule composed of sub-PrettyRules, depending on the available width.
93+
*/
94+
function printRuleDependingWidth(x: PrettifyRule, width: number): string {
95+
return layout(best(width, 0, x));
96+
}
97+
98+
function layout(x: SubRule): string {
99+
if (x && "type" in x) {
100+
if (x.type === "subline") {
101+
return "\n" + "\t".repeat(x.indent) + layout(x.rule);
102+
}
103+
if (x.type === "subtext") {
104+
return x.text + layout(x.rule);
105+
}
106+
}
107+
return "";
108+
}
109+
110+
function best(width: number, k: number, x: PrettifyRule): SubRule {
111+
return be(width, k, [[0, x]]);
112+
}
113+
114+
function be(width: number, k: number, x: RuleFit[]): SubRule {
115+
if (x.length === 0) return null;
116+
const [first, ...rest] = x;
117+
const [i, rule] = first;
118+
if (rule !== null) {
119+
if (typeof rule === "string") {
120+
return {
121+
type: "subtext",
122+
text: rule,
123+
rule: be(width, k + rule.length, rest),
124+
};
125+
}
126+
if (rule.type === "concat") {
127+
const concatDocs: RuleFit[] = rule.rules.map((d) => [i, d]);
128+
const res = be(width, k, [...concatDocs, ...rest]);
129+
return res;
130+
}
131+
if (rule.type === "nest") {
132+
return be(width, k, [[i + rule.indent, rule.a], ...rest]);
133+
}
134+
if (rule.type === "line") {
135+
return {
136+
type: "subline",
137+
indent: i,
138+
rule: be(width, i, rest),
139+
};
140+
}
141+
if (rule.type === "chooseBetween") {
142+
const a = be(width, k, [[i, rule.a], ...rest]);
143+
const b = be(width, k, [[i, rule.b], ...rest]);
144+
return fits(width - k, a) ? a : b;
145+
}
146+
}
147+
return null;
148+
}
149+
150+
function fits(width: number, x: SubRule): boolean {
151+
if (width < 0) return false;
152+
if (x === null) {
153+
return true;
154+
} else if (x.type === "subline") {
155+
return true;
156+
} else if (x.type === "subtext") {
157+
return fits(width - x.text.length, x.rule);
158+
}
159+
return false;
160+
}
161+
162+
// ---------------------------------------
163+
// AST part
164+
// ---------------------------------------
165+
166+
export function prettify(ast: AST) {
167+
return "= " + printRuleDependingWidth(astToDoc(ast), 30);
168+
}
169+
170+
/** transform an AST composed of sub-ASTs into a PrettifyRule composed of sub-PrettifyRules.*/
171+
function astToDoc(ast: AST): PrettifyRule {
172+
switch (ast.type) {
173+
case "NUMBER":
174+
return String(ast.value);
175+
176+
case "STRING":
177+
return `"${ast.value}"`;
178+
179+
case "BOOLEAN":
180+
return ast.value ? "TRUE" : "FALSE";
181+
182+
case "REFERENCE":
183+
return ast.value;
184+
185+
case "FUNCALL":
186+
const argsDocs = ast.args.map(astToDoc);
187+
return group(
188+
join([
189+
ast.value,
190+
"(",
191+
nest(
192+
1,
193+
join([line(), join(argsDocs.map((doc, i) => (i > 0 ? join([",", line(), doc]) : doc)))])
194+
),
195+
line(),
196+
")",
197+
])
198+
);
199+
200+
case "UNARY_OPERATION":
201+
return ast.postfix
202+
? join([astToDoc(ast.operand), ast.value])
203+
: join([ast.value, astToDoc(ast.operand)]);
204+
205+
case "BIN_OPERATION": {
206+
const leftDoc = astToDoc(ast.left);
207+
const rightDoc = astToDoc(ast.right);
208+
const operator = ` ${ast.value}`;
209+
return group(join([leftDoc, operator, nest(1, join([line(), rightDoc]))]));
210+
}
211+
212+
case "SYMBOL":
213+
return ast.value;
214+
215+
case "EMPTY":
216+
return "";
217+
}
218+
}

0 commit comments

Comments
 (0)