@adecore/editor-core 0.44.0 → 0.44.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -97,6 +97,7 @@ export declare class DocumentModel {
97
97
  typeText(text: string, options?: TypeTextOptions): boolean;
98
98
  private takeBackArrows;
99
99
  private completeArrows;
100
+ private completeTemplates;
100
101
  paste(text: string, options?: CommandOptions & {
101
102
  wholeLines?: boolean;
102
103
  }): boolean;
package/dist/document.js CHANGED
@@ -14,7 +14,7 @@ import { blockFolds } from './block-folds.js';
14
14
  import { applyFoldHints, markFileHeader } from './fold-roles.js';
15
15
  import { lexicalFolds } from './lexical-folds.js';
16
16
  import { rangeOf, whitespaceOf } from './line-text.js';
17
- import { isQuote, replacesComparison, surround, swapQuotes } from './typing-handlers.js';
17
+ import { isQuote, replacesComparison, surround, swapQuotes, templateLiteralEdits, templateString } from './typing-handlers.js';
18
18
  import { scanBrackets } from './brackets.js';
19
19
  import { BracketTracker } from './bracket-tracker.js';
20
20
  import { hasSmartSemicolon, isPhp } from './lexical.js';
@@ -58,6 +58,10 @@ function trailingBlankLength(text) {
58
58
  function changesText(rope, edit) {
59
59
  return edit.text.length !== edit.to - edit.from || rope.slice(edit.from, edit.to) !== edit.text;
60
60
  }
61
+ /* Where an offset stands after edits that end at or before it moved it; an insertion at the offset pushes it on. */
62
+ function shifted(edits, offset) {
63
+ return edits.reduce((moved, edit) => (edit.to <= offset ? moved + edit.text.length - (edit.to - edit.from) : moved), offset);
64
+ }
61
65
  /* Merges the carets' edits into one simultaneous batch and works out where each caret lands in the result. */
62
66
  function selectionTransaction(plans) {
63
67
  const edits = [];
@@ -591,7 +595,8 @@ export class DocumentModel {
591
595
  /*
592
596
  * Types `text` at every caret as one keystroke would: it wraps a selection in a bracket or quote,
593
597
  * closes an opener, types over a closer that is already there and, in TypeScript, JavaScript and
594
- * PHP, moves a `;` to the end of a statement. In PHP a `-` after a variable becomes `->`. Comments
598
+ * PHP, moves a `;` to the end of a statement. In PHP a `-` after a variable becomes `->`. In JavaScript and
599
+ * TypeScript a `{` that completes `${` in a quoted string makes it a template literal. Comments
595
600
  * and string contents are left alone. Returns whether anything changed, a swallowed closer included.
596
601
  */
597
602
  typeText(text, options = {}) {
@@ -617,6 +622,7 @@ export class DocumentModel {
617
622
  surround: options.surroundSelection !== false && single,
618
623
  smartSemicolon: options.smartSemicolon !== false && hasSmartSemicolon(language),
619
624
  smartArrow: options.smartArrow !== false && single && isPhp(language),
625
+ templateLiterals: options.templateLiterals !== false && text === '{',
620
626
  overArrows: text === '>' ? arrows : new Set(),
621
627
  dedent: options.smartEnter !== false
622
628
  };
@@ -630,6 +636,7 @@ export class DocumentModel {
630
636
  this.rememberStatementClosers(plans, transaction, language);
631
637
  this.rememberPairs(plans, transaction);
632
638
  this.completeArrows(plans, transaction);
639
+ this.completeTemplates(plans, transaction, language);
633
640
  }
634
641
  return changed || plans.some((plan) => plan.consumed);
635
642
  }
@@ -654,6 +661,23 @@ export class DocumentModel {
654
661
  this.arrows = new Set(at.map(moved));
655
662
  }
656
663
  }
664
+ /* Turns the quoted strings a `${` was typed in into template literals, as a step of their own so the first undo leaves the quotes back. */
665
+ completeTemplates(plans, transaction, language) {
666
+ // Carets in one string find the same quotes.
667
+ const strings = new Map();
668
+ for (const plan of plans) {
669
+ if (plan.template) {
670
+ strings.set(shifted(transaction.edits, plan.template.open), shifted(transaction.edits, plan.template.close));
671
+ }
672
+ }
673
+ const source = this.editSource(language);
674
+ const edits = [...strings].flatMap(([open, close]) => templateLiteralEdits(source, open, close));
675
+ if (edits.length > 0) {
676
+ // A caret right before a closing quote stays there, which the default mapping of an edit that replaces the quote would not do.
677
+ const selections = this.selections.map((selection) => ({ anchor: shifted(edits, selection.anchor), head: shifted(edits, selection.head) }));
678
+ this.applyEdits(edits, { source: 'input', selections });
679
+ }
680
+ }
657
681
  /*
658
682
  * Pastes `text` at every selection. With as many lines as carets each caret takes one; `wholeLines`
659
683
  * marks text copied from a bare caret, which goes above the lines of bare carets.
@@ -844,6 +868,12 @@ export class DocumentModel {
844
868
  return wrapped;
845
869
  }
846
870
  }
871
+ if (input.templateLiterals && from === to) {
872
+ const template = templateString(this.editSource(language), from, language);
873
+ if (template) {
874
+ return { from, to, text: input.brackets ? '{}' : '{', anchor: 1, head: 1, paired: input.brackets, template };
875
+ }
876
+ }
847
877
  if (pairing && pairs[text] !== undefined && from === to && this.shouldClosePair(from, text, after, context, input)) {
848
878
  return { from, to, text: text + pairs[text], anchor: 1, head: 1, paired: true };
849
879
  }
package/dist/types.d.ts CHANGED
@@ -57,6 +57,7 @@ export interface CommandOptions {
57
57
  language?: string;
58
58
  smartSemicolon?: boolean;
59
59
  smartArrow?: boolean;
60
+ templateLiterals?: boolean;
60
61
  tabOutOfClosers?: boolean;
61
62
  smartEnter?: boolean;
62
63
  indentOnPaste?: boolean;
@@ -1,4 +1,5 @@
1
1
  import type { EditSource } from './edit-source.ts';
2
+ import type { TextEdit } from './types.ts';
2
3
  export interface HandlerEdit {
3
4
  from: number;
4
5
  to: number;
@@ -13,3 +14,8 @@ export declare function surround(selected: string, typed: string): {
13
14
  text: string;
14
15
  inner: string;
15
16
  };
17
+ export declare function templateString(source: EditSource, offset: number, language: string): {
18
+ open: number;
19
+ close: number;
20
+ } | null;
21
+ export declare function templateLiteralEdits(source: EditSource, open: number, close: number): TextEdit[];
@@ -123,3 +123,66 @@ export function surround(selected, typed) {
123
123
  }
124
124
  return { text: typed + inner + matchingDelimiter(typed), inner };
125
125
  }
126
+ const scriptLanguages = /^(javascript|typescript|jsx|tsx|javascriptreact|typescriptreact|js|ts|mjs|cjs|mts|cts)$/i;
127
+ const moduleSpecifier = /(?:\bfrom|\bimport|\brequire)\s*\(?\s*$/;
128
+ /*
129
+ * The quotes of the string a `${` is typed in when that makes it a template literal: a single- or
130
+ * double-quoted string of a script, closed on its line and not the specifier of a module.
131
+ */
132
+ export function templateString(source, offset, language) {
133
+ const line = source.lineAt(offset);
134
+ const script = scriptLanguages.test(language) || (/^vue$/i.test(language) && source.region(line) === 'script');
135
+ const { start, end } = source.line(line);
136
+ const before = source.slice(start, offset);
137
+ if (!script || !before.endsWith('$') || escapedIn(before, before.length - 1)) {
138
+ return null;
139
+ }
140
+ const context = source.context(offset);
141
+ if (context.mode !== 'quote' || context.escaped || (context.quote !== "'" && context.quote !== '"')) {
142
+ return null;
143
+ }
144
+ let open = before.length - 1;
145
+ while (open >= 0 && (before[open] !== context.quote || escapedIn(before, open))) {
146
+ open--;
147
+ }
148
+ if (open < 0 || moduleSpecifier.test(before.slice(0, open))) {
149
+ return null;
150
+ }
151
+ const after = source.slice(offset, end);
152
+ for (let at = 0; at < after.length; at++) {
153
+ if (after[at] === '\\') {
154
+ at++;
155
+ }
156
+ else if (after[at] === context.quote) {
157
+ return { open: start + open, close: offset + at };
158
+ }
159
+ }
160
+ return null;
161
+ }
162
+ function escapedIn(text, index) {
163
+ let slashes = 0;
164
+ while (index - slashes - 1 >= 0 && text[index - slashes - 1] === '\\') {
165
+ slashes++;
166
+ }
167
+ return slashes % 2 === 1;
168
+ }
169
+ /* The edits that make the string between the quotes at `open` and `close` a template literal: its backticks are escaped and its escaped old quotes are not. */
170
+ export function templateLiteralEdits(source, open, close) {
171
+ const quote = source.charAt(open);
172
+ const body = source.slice(open + 1, close);
173
+ const edits = [{ from: open, to: open + 1, text: '`' }];
174
+ for (let index = 0; index < body.length; index++) {
175
+ const at = open + 1 + index;
176
+ if (body[index] === '\\') {
177
+ if (body[index + 1] === quote) {
178
+ edits.push({ from: at, to: at + 1, text: '' });
179
+ }
180
+ index++;
181
+ }
182
+ else if (body[index] === '`') {
183
+ edits.push({ from: at, to: at + 1, text: '\\`' });
184
+ }
185
+ }
186
+ edits.push({ from: close, to: close + 1, text: '`' });
187
+ return edits;
188
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/editor-core",
3
- "version": "0.44.0",
3
+ "version": "0.44.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/document.ts CHANGED
@@ -16,7 +16,7 @@ import { blockFolds } from './block-folds.ts';
16
16
  import { applyFoldHints, markFileHeader } from './fold-roles.ts';
17
17
  import { lexicalFolds } from './lexical-folds.ts';
18
18
  import { rangeOf, whitespaceOf } from './line-text.ts';
19
- import { isQuote, replacesComparison, surround, swapQuotes } from './typing-handlers.ts';
19
+ import { isQuote, replacesComparison, surround, swapQuotes, templateLiteralEdits, templateString } from './typing-handlers.ts';
20
20
  import { scanBrackets } from './brackets.ts';
21
21
  import type { BracketIndex, BracketMatch } from './brackets.ts';
22
22
  import { BracketTracker } from './bracket-tracker.ts';
@@ -109,6 +109,8 @@ interface SelectionEdit extends TextEdit {
109
109
  paired?: boolean;
110
110
  /* A `-` the editor completes to `->` in a step of its own, once the `-` is in. */
111
111
  arrow?: boolean;
112
+ /* A `${` typed in a quoted string, which becomes a template literal in a step of its own; where its quotes stand before the edit. */
113
+ template?: { open: number; close: number };
112
114
  }
113
115
 
114
116
  interface SelectionTransaction {
@@ -130,6 +132,7 @@ interface TypingInput {
130
132
  surround: boolean;
131
133
  smartSemicolon: boolean;
132
134
  smartArrow: boolean;
135
+ templateLiterals: boolean;
133
136
  /* The carets right after a `->` the editor completed, where a typed `>` goes over the one that is there. */
134
137
  overArrows: ReadonlySet<number>;
135
138
  /* A closing bracket typed on a line of its own goes back to the indentation of its opener. */
@@ -187,6 +190,11 @@ function changesText(rope: TextRope, edit: TextEdit): boolean {
187
190
  return edit.text.length !== edit.to - edit.from || rope.slice(edit.from, edit.to) !== edit.text;
188
191
  }
189
192
 
193
+ /* Where an offset stands after edits that end at or before it moved it; an insertion at the offset pushes it on. */
194
+ function shifted(edits: readonly TextEdit[], offset: number): number {
195
+ return edits.reduce((moved, edit) => (edit.to <= offset ? moved + edit.text.length - (edit.to - edit.from) : moved), offset);
196
+ }
197
+
190
198
  /* Merges the carets' edits into one simultaneous batch and works out where each caret lands in the result. */
191
199
  function selectionTransaction(plans: readonly SelectionEdit[]): SelectionTransaction {
192
200
  const edits: TextEdit[] = [];
@@ -883,7 +891,8 @@ export class DocumentModel {
883
891
  /*
884
892
  * Types `text` at every caret as one keystroke would: it wraps a selection in a bracket or quote,
885
893
  * closes an opener, types over a closer that is already there and, in TypeScript, JavaScript and
886
- * PHP, moves a `;` to the end of a statement. In PHP a `-` after a variable becomes `->`. Comments
894
+ * PHP, moves a `;` to the end of a statement. In PHP a `-` after a variable becomes `->`. In JavaScript and
895
+ * TypeScript a `{` that completes `${` in a quoted string makes it a template literal. Comments
887
896
  * and string contents are left alone. Returns whether anything changed, a swallowed closer included.
888
897
  */
889
898
  typeText(text: string, options: TypeTextOptions = {}): boolean {
@@ -914,6 +923,7 @@ export class DocumentModel {
914
923
  surround: options.surroundSelection !== false && single,
915
924
  smartSemicolon: options.smartSemicolon !== false && hasSmartSemicolon(language),
916
925
  smartArrow: options.smartArrow !== false && single && isPhp(language),
926
+ templateLiterals: options.templateLiterals !== false && text === '{',
917
927
  overArrows: text === '>' ? arrows : new Set(),
918
928
  dedent: options.smartEnter !== false
919
929
  };
@@ -935,6 +945,7 @@ export class DocumentModel {
935
945
  this.rememberStatementClosers(plans, transaction, language);
936
946
  this.rememberPairs(plans, transaction);
937
947
  this.completeArrows(plans, transaction);
948
+ this.completeTemplates(plans, transaction, language);
938
949
  }
939
950
 
940
951
  return changed || plans.some((plan) => plan.consumed);
@@ -972,6 +983,27 @@ export class DocumentModel {
972
983
  }
973
984
  }
974
985
 
986
+ /* Turns the quoted strings a `${` was typed in into template literals, as a step of their own so the first undo leaves the quotes back. */
987
+ private completeTemplates(plans: readonly SelectionEdit[], transaction: SelectionTransaction, language: string): void {
988
+ // Carets in one string find the same quotes.
989
+ const strings = new Map<number, number>();
990
+
991
+ for (const plan of plans) {
992
+ if (plan.template) {
993
+ strings.set(shifted(transaction.edits, plan.template.open), shifted(transaction.edits, plan.template.close));
994
+ }
995
+ }
996
+
997
+ const source = this.editSource(language);
998
+ const edits = [...strings].flatMap(([open, close]) => templateLiteralEdits(source, open, close));
999
+
1000
+ if (edits.length > 0) {
1001
+ // A caret right before a closing quote stays there, which the default mapping of an edit that replaces the quote would not do.
1002
+ const selections = this.selections.map((selection) => ({ anchor: shifted(edits, selection.anchor), head: shifted(edits, selection.head) }));
1003
+ this.applyEdits(edits, { source: 'input', selections });
1004
+ }
1005
+ }
1006
+
975
1007
  /*
976
1008
  * Pastes `text` at every selection. With as many lines as carets each caret takes one; `wholeLines`
977
1009
  * marks text copied from a bare caret, which goes above the lines of bare carets.
@@ -1233,6 +1265,14 @@ export class DocumentModel {
1233
1265
  }
1234
1266
  }
1235
1267
 
1268
+ if (input.templateLiterals && from === to) {
1269
+ const template = templateString(this.editSource(language), from, language);
1270
+
1271
+ if (template) {
1272
+ return { from, to, text: input.brackets ? '{}' : '{', anchor: 1, head: 1, paired: input.brackets, template };
1273
+ }
1274
+ }
1275
+
1236
1276
  if (pairing && pairs[text] !== undefined && from === to && this.shouldClosePair(from, text, after, context, input)) {
1237
1277
  return { from, to, text: text + pairs[text], anchor: 1, head: 1, paired: true };
1238
1278
  }
package/src/types.ts CHANGED
@@ -147,6 +147,8 @@ export interface CommandOptions {
147
147
  smartSemicolon?: boolean;
148
148
  /* In PHP a `-` typed after a variable, a member or a `)` or `]` becomes `->`. On unless false. */
149
149
  smartArrow?: boolean;
150
+ /* In JavaScript and TypeScript a `${` typed in a quoted string makes it a template literal, in a second undo step. On unless false. */
151
+ templateLiterals?: boolean;
150
152
  /* Tab steps over a closer the editor inserted. On unless false. */
151
153
  tabOutOfClosers?: boolean;
152
154
  /* Enter computes the indentation of the new line, continues comments and closes braces. On unless false. */
@@ -1,4 +1,5 @@
1
1
  import type { EditSource } from './edit-source.ts';
2
+ import type { TextEdit } from './types.ts';
2
3
 
3
4
  /* A change of text in one place and where the caret stands in what replaces it. */
4
5
  export interface HandlerEdit {
@@ -169,3 +170,85 @@ export function surround(selected: string, typed: string): { text: string; inner
169
170
 
170
171
  return { text: typed + inner + matchingDelimiter(typed), inner };
171
172
  }
173
+
174
+ const scriptLanguages = /^(javascript|typescript|jsx|tsx|javascriptreact|typescriptreact|js|ts|mjs|cjs|mts|cts)$/i;
175
+ const moduleSpecifier = /(?:\bfrom|\bimport|\brequire)\s*\(?\s*$/;
176
+
177
+ /*
178
+ * The quotes of the string a `${` is typed in when that makes it a template literal: a single- or
179
+ * double-quoted string of a script, closed on its line and not the specifier of a module.
180
+ */
181
+ export function templateString(source: EditSource, offset: number, language: string): { open: number; close: number } | null {
182
+ const line = source.lineAt(offset);
183
+ const script = scriptLanguages.test(language) || (/^vue$/i.test(language) && source.region(line) === 'script');
184
+
185
+ const { start, end } = source.line(line);
186
+ const before = source.slice(start, offset);
187
+
188
+ if (!script || !before.endsWith('$') || escapedIn(before, before.length - 1)) {
189
+ return null;
190
+ }
191
+
192
+ const context = source.context(offset);
193
+
194
+ if (context.mode !== 'quote' || context.escaped || (context.quote !== "'" && context.quote !== '"')) {
195
+ return null;
196
+ }
197
+
198
+ let open = before.length - 1;
199
+
200
+ while (open >= 0 && (before[open] !== context.quote || escapedIn(before, open))) {
201
+ open--;
202
+ }
203
+
204
+ if (open < 0 || moduleSpecifier.test(before.slice(0, open))) {
205
+ return null;
206
+ }
207
+
208
+ const after = source.slice(offset, end);
209
+
210
+ for (let at = 0; at < after.length; at++) {
211
+ if (after[at] === '\\') {
212
+ at++;
213
+ } else if (after[at] === context.quote) {
214
+ return { open: start + open, close: offset + at };
215
+ }
216
+ }
217
+
218
+ return null;
219
+ }
220
+
221
+ function escapedIn(text: string, index: number): boolean {
222
+ let slashes = 0;
223
+
224
+ while (index - slashes - 1 >= 0 && text[index - slashes - 1] === '\\') {
225
+ slashes++;
226
+ }
227
+
228
+ return slashes % 2 === 1;
229
+ }
230
+
231
+ /* The edits that make the string between the quotes at `open` and `close` a template literal: its backticks are escaped and its escaped old quotes are not. */
232
+ export function templateLiteralEdits(source: EditSource, open: number, close: number): TextEdit[] {
233
+ const quote = source.charAt(open);
234
+ const body = source.slice(open + 1, close);
235
+ const edits: TextEdit[] = [{ from: open, to: open + 1, text: '`' }];
236
+
237
+ for (let index = 0; index < body.length; index++) {
238
+ const at = open + 1 + index;
239
+
240
+ if (body[index] === '\\') {
241
+ if (body[index + 1] === quote) {
242
+ edits.push({ from: at, to: at + 1, text: '' });
243
+ }
244
+
245
+ index++;
246
+ } else if (body[index] === '`') {
247
+ edits.push({ from: at, to: at + 1, text: '\\`' });
248
+ }
249
+ }
250
+
251
+ edits.push({ from: close, to: close + 1, text: '`' });
252
+
253
+ return edits;
254
+ }