@neo4j-cypher/react-codemirror 2.0.0-next.11 → 2.0.0-next.13
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.
- package/CHANGELOG.md +16 -0
- package/dist/CypherEditor.d.ts +10 -1
- package/dist/CypherEditor.js +2 -1
- package/dist/CypherEditor.js.map +1 -1
- package/dist/e2e_tests/autoCompletion.spec.js +13 -7
- package/dist/e2e_tests/autoCompletion.spec.js.map +1 -1
- package/dist/e2e_tests/signatureHelp.spec.js +11 -8
- package/dist/e2e_tests/signatureHelp.spec.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lang-cypher/autocomplete.js +43 -57
- package/dist/lang-cypher/autocomplete.js.map +1 -1
- package/dist/lang-cypher/createCypherTheme.js +3 -0
- package/dist/lang-cypher/createCypherTheme.js.map +1 -1
- package/dist/lang-cypher/langCypher.d.ts +0 -1
- package/dist/lang-cypher/langCypher.js.map +1 -1
- package/dist/lang-cypher/signatureHelp.js +22 -7
- package/dist/lang-cypher/signatureHelp.js.map +1 -1
- package/dist/lang-cypher/syntaxValidation.js +1 -2
- package/dist/lang-cypher/syntaxValidation.js.map +1 -1
- package/dist/neo4jSetup.d.ts +5 -1
- package/dist/neo4jSetup.js +8 -8
- package/dist/neo4jSetup.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/CypherEditor.tsx +13 -2
- package/src/e2e_tests/autoCompletion.spec.tsx +22 -13
- package/src/e2e_tests/signatureHelp.spec.tsx +12 -8
- package/src/index.ts +1 -4
- package/src/lang-cypher/autocomplete.ts +44 -59
- package/src/lang-cypher/createCypherTheme.ts +3 -0
- package/src/lang-cypher/langCypher.ts +0 -1
- package/src/lang-cypher/signatureHelp.ts +27 -9
- package/src/lang-cypher/syntaxValidation.ts +3 -3
- package/src/neo4jSetup.tsx +27 -16
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"codemirror",
|
|
18
18
|
"codemirror 6"
|
|
19
19
|
],
|
|
20
|
-
"version": "2.0.0-next.
|
|
20
|
+
"version": "2.0.0-next.13",
|
|
21
21
|
"main": "./dist/index.js",
|
|
22
22
|
"types": "./dist/index.d.ts",
|
|
23
23
|
"type": "module",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@codemirror/view": "^6.29.1",
|
|
52
52
|
"@lezer/common": "^1.0.2",
|
|
53
53
|
"@lezer/highlight": "^1.1.3",
|
|
54
|
-
"@neo4j-cypher/language-support": "2.0.0-next.
|
|
54
|
+
"@neo4j-cypher/language-support": "2.0.0-next.10",
|
|
55
55
|
"@types/prismjs": "^1.26.3",
|
|
56
56
|
"@types/workerpool": "^6.4.7",
|
|
57
57
|
"ayu": "^8.0.1",
|
package/src/CypherEditor.tsx
CHANGED
|
@@ -161,9 +161,19 @@ export interface CypherEditorProps {
|
|
|
161
161
|
readonly?: boolean;
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
|
-
* String value to assign to the aria-label attribute of the editor
|
|
164
|
+
* String value to assign to the aria-label attribute of the editor.
|
|
165
165
|
*/
|
|
166
166
|
ariaLabel?: string;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Whether keybindings for inserting indents with the Tab key should be disabled.
|
|
170
|
+
*
|
|
171
|
+
* true will not create keybindings for inserting indents.
|
|
172
|
+
* false will create keybindings for inserting indents.
|
|
173
|
+
*
|
|
174
|
+
* @default false
|
|
175
|
+
*/
|
|
176
|
+
moveFocusOnTab?: boolean;
|
|
167
177
|
}
|
|
168
178
|
|
|
169
179
|
const executeKeybinding = (
|
|
@@ -310,6 +320,7 @@ export class CypherEditor extends Component<
|
|
|
310
320
|
theme: 'light',
|
|
311
321
|
lineNumbers: true,
|
|
312
322
|
newLineOnEnter: false,
|
|
323
|
+
moveFocusOnTab: false,
|
|
313
324
|
};
|
|
314
325
|
|
|
315
326
|
private debouncedOnChange = this.props.onChange
|
|
@@ -383,7 +394,7 @@ export class CypherEditor extends Component<
|
|
|
383
394
|
]),
|
|
384
395
|
),
|
|
385
396
|
historyNavigation(this.props),
|
|
386
|
-
basicNeo4jSetup(),
|
|
397
|
+
basicNeo4jSetup(this.props),
|
|
387
398
|
themeCompartment.of(themeExtension),
|
|
388
399
|
changeListener,
|
|
389
400
|
cypher(this.schemaRef.current),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
1
2
|
import { testData } from '@neo4j-cypher/language-support';
|
|
2
3
|
import { expect, test } from '@playwright/experimental-ct-react';
|
|
3
4
|
import type { Page } from '@playwright/test';
|
|
@@ -113,6 +114,27 @@ test('can complete labels', async ({ mount, page }) => {
|
|
|
113
114
|
await expect(component).toContainText('MATCH (n :Pokemon');
|
|
114
115
|
});
|
|
115
116
|
|
|
117
|
+
test('can complete properties with backticks', async ({ mount, page }) => {
|
|
118
|
+
const component = await mount(
|
|
119
|
+
<CypherEditor
|
|
120
|
+
schema={{
|
|
121
|
+
propertyKeys: ['foo bar'],
|
|
122
|
+
}}
|
|
123
|
+
/>,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const textField = page.getByRole('textbox');
|
|
127
|
+
|
|
128
|
+
await textField.fill('MATCH (n) RETURN n.foo');
|
|
129
|
+
await textField.press('Escape');
|
|
130
|
+
await textField.press('Control+ ');
|
|
131
|
+
|
|
132
|
+
await page.locator('.cm-tooltip-autocomplete').getByText('foo bar').click();
|
|
133
|
+
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
|
|
134
|
+
|
|
135
|
+
await expect(component).toContainText('MATCH (n) RETURN n.`foo bar`');
|
|
136
|
+
});
|
|
137
|
+
|
|
116
138
|
test('can update dbschema', async ({ mount, page }) => {
|
|
117
139
|
const component = await mount(
|
|
118
140
|
<CypherEditor
|
|
@@ -380,19 +402,6 @@ test('shows deprecated function as strikethrough on auto-completion', async ({
|
|
|
380
402
|
await expect(page.locator('.cm-deprecated-completion')).toBeVisible();
|
|
381
403
|
});
|
|
382
404
|
|
|
383
|
-
test('does not signature help information on auto-completion if flag not enabled explicitly', async ({
|
|
384
|
-
page,
|
|
385
|
-
mount,
|
|
386
|
-
}) => {
|
|
387
|
-
await mount(<CypherEditor schema={testData.mockSchema} />);
|
|
388
|
-
|
|
389
|
-
const textField = page.getByRole('textbox');
|
|
390
|
-
await textField.fill('CALL apoc.periodic.');
|
|
391
|
-
|
|
392
|
-
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
|
|
393
|
-
await expect(page.locator('.cm-completionInfo')).not.toBeVisible();
|
|
394
|
-
});
|
|
395
|
-
|
|
396
405
|
test('does not signature help information on auto-completion if docs and signature are empty', async ({
|
|
397
406
|
page,
|
|
398
407
|
mount,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
1
2
|
import { testData } from '@neo4j-cypher/language-support';
|
|
2
3
|
import { expect, test } from '@playwright/experimental-ct-react';
|
|
3
4
|
import { Locator } from 'playwright/test';
|
|
@@ -10,6 +11,8 @@ type TooltipExpectations = {
|
|
|
10
11
|
excludes?: string[];
|
|
11
12
|
};
|
|
12
13
|
|
|
14
|
+
const importCsvProc = testData.mockSchema.procedures['apoc.import.csv'];
|
|
15
|
+
|
|
13
16
|
function testTooltip(tooltip: Locator, expectations: TooltipExpectations) {
|
|
14
17
|
const includes = expectations.includes ?? [];
|
|
15
18
|
const excludes = expectations.excludes ?? [];
|
|
@@ -83,8 +86,9 @@ test('Signature help shows the description for the first argument', async ({
|
|
|
83
86
|
|
|
84
87
|
await testTooltip(tooltip, {
|
|
85
88
|
includes: [
|
|
86
|
-
'
|
|
87
|
-
|
|
89
|
+
testData.mockSchema.procedures['apoc.import.csv'].argumentDescription[0]
|
|
90
|
+
.description,
|
|
91
|
+
testData.mockSchema.procedures['apoc.import.csv'].description,
|
|
88
92
|
],
|
|
89
93
|
});
|
|
90
94
|
});
|
|
@@ -103,8 +107,8 @@ test('Signature help shows the description for the first argument when the curso
|
|
|
103
107
|
|
|
104
108
|
await testTooltip(tooltip, {
|
|
105
109
|
includes: [
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
importCsvProc.argumentDescription[0].description,
|
|
111
|
+
importCsvProc.description,
|
|
108
112
|
],
|
|
109
113
|
});
|
|
110
114
|
});
|
|
@@ -127,8 +131,8 @@ test('Signature help shows the description for the second argument', async ({
|
|
|
127
131
|
|
|
128
132
|
await testTooltip(tooltip, {
|
|
129
133
|
includes: [
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
importCsvProc.argumentDescription[1].description,
|
|
135
|
+
importCsvProc.description,
|
|
132
136
|
],
|
|
133
137
|
});
|
|
134
138
|
});
|
|
@@ -147,8 +151,8 @@ test('Signature help shows the description for the second argument when the curs
|
|
|
147
151
|
|
|
148
152
|
await testTooltip(tooltip, {
|
|
149
153
|
includes: [
|
|
150
|
-
|
|
151
|
-
|
|
154
|
+
importCsvProc.argumentDescription[1].description,
|
|
155
|
+
importCsvProc.description,
|
|
152
156
|
],
|
|
153
157
|
});
|
|
154
158
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
CypherParser,
|
|
3
|
-
_internalFeatureFlags,
|
|
4
|
-
} from '@neo4j-cypher/language-support';
|
|
1
|
+
export * as LanguageSupport from '@neo4j-cypher/language-support';
|
|
5
2
|
export { CypherEditor } from './CypherEditor';
|
|
6
3
|
export { cypher } from './lang-cypher/langCypher';
|
|
7
4
|
export { darkThemeConstants, lightThemeConstants } from './themes';
|
|
@@ -77,73 +77,56 @@ export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
const options = autocomplete(
|
|
80
|
-
|
|
80
|
+
// TODO This is a temporary hack because completions are not working well
|
|
81
|
+
documentText.slice(0, context.pos),
|
|
81
82
|
config.schema ?? {},
|
|
82
83
|
context.pos,
|
|
83
84
|
context.explicit,
|
|
84
85
|
);
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const newDiv = document.createElement('div');
|
|
87
|
+
return {
|
|
88
|
+
from: context.matchBefore(/(\w|\$)*$/).from,
|
|
89
|
+
options: options.map((o) => {
|
|
90
|
+
let maybeInfo = {};
|
|
91
|
+
let emptyInfo = true;
|
|
92
|
+
const newDiv = document.createElement('div');
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
94
|
+
if (o.signature) {
|
|
95
|
+
const header = document.createElement('p');
|
|
96
|
+
header.setAttribute('class', 'cm-completionInfo-signature');
|
|
97
|
+
header.textContent = o.signature;
|
|
98
|
+
if (header.textContent.length > 0) {
|
|
99
|
+
emptyInfo = false;
|
|
100
|
+
newDiv.appendChild(header);
|
|
102
101
|
}
|
|
102
|
+
}
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
104
|
+
if (o.documentation) {
|
|
105
|
+
const paragraph = document.createElement('p');
|
|
106
|
+
paragraph.textContent = getDocString(o.documentation);
|
|
107
|
+
if (paragraph.textContent.length > 0) {
|
|
108
|
+
emptyInfo = false;
|
|
109
|
+
newDiv.appendChild(paragraph);
|
|
111
110
|
}
|
|
111
|
+
}
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
const deprecated =
|
|
119
|
-
o.tags?.find((tag) => tag === CompletionItemTag.Deprecated) ??
|
|
120
|
-
false;
|
|
121
|
-
// The negative boost moves the deprecation down the list
|
|
122
|
-
// so we offer the user the completions that are
|
|
123
|
-
// deprecated the last
|
|
124
|
-
const maybeDeprecated = deprecated
|
|
125
|
-
? { boost: -99, deprecated: true }
|
|
126
|
-
: {};
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
label: o.label,
|
|
130
|
-
type: completionKindToCodemirrorIcon(o.kind),
|
|
131
|
-
apply:
|
|
132
|
-
o.kind === CompletionItemKind.Snippet
|
|
133
|
-
? // codemirror requires an empty snippet space to be able to tab out of the completion
|
|
134
|
-
snippet((o.insertText ?? o.label) + '${}')
|
|
135
|
-
: undefined,
|
|
136
|
-
detail: o.detail,
|
|
137
|
-
...maybeDeprecated,
|
|
138
|
-
...maybeInfo,
|
|
113
|
+
if (!emptyInfo) {
|
|
114
|
+
maybeInfo = {
|
|
115
|
+
info: () => Promise.resolve(newDiv),
|
|
139
116
|
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
117
|
+
}
|
|
118
|
+
const deprecated =
|
|
119
|
+
o.tags?.find((tag) => tag === CompletionItemTag.Deprecated) ?? false;
|
|
120
|
+
// The negative boost moves the deprecation down the list
|
|
121
|
+
// so we offer the user the completions that are
|
|
122
|
+
// deprecated the last
|
|
123
|
+
const maybeDeprecated = deprecated
|
|
124
|
+
? { boost: -99, deprecated: true }
|
|
125
|
+
: {};
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
label: o.insertText ? o.insertText : o.label,
|
|
129
|
+
displayLabel: o.label,
|
|
147
130
|
type: completionKindToCodemirrorIcon(o.kind),
|
|
148
131
|
apply:
|
|
149
132
|
o.kind === CompletionItemKind.Snippet
|
|
@@ -151,7 +134,9 @@ export const cypherAutocomplete: (config: CypherConfig) => CompletionSource =
|
|
|
151
134
|
snippet((o.insertText ?? o.label) + '${}')
|
|
152
135
|
: undefined,
|
|
153
136
|
detail: o.detail,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
137
|
+
...maybeDeprecated,
|
|
138
|
+
...maybeInfo,
|
|
139
|
+
};
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
157
142
|
};
|
|
@@ -121,6 +121,9 @@ export const createCypherTheme = ({
|
|
|
121
121
|
'& .cm-signature-help-panel-name': {
|
|
122
122
|
padding: '5px',
|
|
123
123
|
},
|
|
124
|
+
'& .cm-signature-help-panel-arg-description': {
|
|
125
|
+
padding: '5px',
|
|
126
|
+
},
|
|
124
127
|
'& .cm-signature-help-panel-description': {
|
|
125
128
|
padding: '5px',
|
|
126
129
|
},
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { EditorState, StateField } from '@codemirror/state';
|
|
2
2
|
import { showTooltip, Tooltip } from '@codemirror/view';
|
|
3
3
|
import { signatureHelp } from '@neo4j-cypher/language-support';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
MarkupContent,
|
|
6
|
+
SignatureInformation,
|
|
7
|
+
} from 'vscode-languageserver-types';
|
|
5
8
|
import { CypherConfig } from './langCypher';
|
|
6
9
|
import { getDocString } from './utils';
|
|
7
10
|
|
|
@@ -38,24 +41,32 @@ const createSignatureHelpElement =
|
|
|
38
41
|
|
|
39
42
|
const signatureLabel = document.createElement('div');
|
|
40
43
|
signatureLabel.className = 'cm-signature-help-panel-name';
|
|
41
|
-
|
|
44
|
+
const methodName = signature.label.slice(0, signature.label.indexOf('('));
|
|
45
|
+
const returnType = signature.label.slice(signature.label.indexOf(')') + 1);
|
|
46
|
+
signatureLabel.appendChild(document.createTextNode(`${methodName}(`));
|
|
47
|
+
let currentParamDescription: string | undefined = undefined;
|
|
42
48
|
|
|
43
49
|
parameters.forEach((param, index) => {
|
|
44
|
-
if (typeof param.
|
|
50
|
+
if (typeof param.label === 'string') {
|
|
45
51
|
const span = document.createElement('span');
|
|
46
|
-
span.appendChild(document.createTextNode(param.
|
|
52
|
+
span.appendChild(document.createTextNode(param.label));
|
|
47
53
|
if (index !== parameters.length - 1) {
|
|
48
54
|
span.appendChild(document.createTextNode(', '));
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
if (index === activeParameter) {
|
|
52
58
|
span.className = 'cm-signature-help-panel-current-argument';
|
|
59
|
+
const paramDoc = param.documentation;
|
|
60
|
+
currentParamDescription = MarkupContent.is(paramDoc)
|
|
61
|
+
? paramDoc.value
|
|
62
|
+
: paramDoc;
|
|
53
63
|
}
|
|
54
64
|
signatureLabel.appendChild(span);
|
|
55
65
|
}
|
|
56
66
|
});
|
|
57
67
|
|
|
58
68
|
signatureLabel.appendChild(document.createTextNode(')'));
|
|
69
|
+
signatureLabel.appendChild(document.createTextNode(returnType));
|
|
59
70
|
|
|
60
71
|
contents.appendChild(signatureLabel);
|
|
61
72
|
|
|
@@ -64,11 +75,18 @@ const createSignatureHelpElement =
|
|
|
64
75
|
|
|
65
76
|
contents.appendChild(separator);
|
|
66
77
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
if (currentParamDescription !== undefined) {
|
|
79
|
+
const argDescription = document.createElement('div');
|
|
80
|
+
argDescription.className = 'cm-signature-help-panel-arg-description';
|
|
81
|
+
argDescription.appendChild(
|
|
82
|
+
document.createTextNode(currentParamDescription),
|
|
83
|
+
);
|
|
84
|
+
contents.appendChild(argDescription);
|
|
85
|
+
}
|
|
86
|
+
const methodDescription = document.createElement('div');
|
|
87
|
+
methodDescription.className = 'cm-signature-help-panel-description';
|
|
88
|
+
methodDescription.appendChild(document.createTextNode(doc));
|
|
89
|
+
contents.appendChild(methodDescription);
|
|
72
90
|
|
|
73
91
|
return { dom };
|
|
74
92
|
};
|
|
@@ -54,9 +54,9 @@ export const semanticAnalysisLinter: (config: CypherConfig) => Extension = (
|
|
|
54
54
|
const parse = parserWrapper.parse(query);
|
|
55
55
|
const statements = parse.statementsParsing;
|
|
56
56
|
|
|
57
|
-
const anySyntacticError =
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
const anySyntacticError = statements.some(
|
|
58
|
+
(statement) => statement.syntaxErrors.length !== 0,
|
|
59
|
+
);
|
|
60
60
|
|
|
61
61
|
if (anySyntacticError) {
|
|
62
62
|
return [];
|
package/src/neo4jSetup.tsx
CHANGED
|
@@ -61,7 +61,13 @@ const insertTab: StateCommand = (cmd) => {
|
|
|
61
61
|
return true;
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
type SetupProps = {
|
|
65
|
+
moveFocusOnTab?: boolean;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const basicNeo4jSetup = ({
|
|
69
|
+
moveFocusOnTab = false,
|
|
70
|
+
}: SetupProps): Extension[] => {
|
|
65
71
|
const keymaps: KeyBinding[] = [
|
|
66
72
|
closeBracketsKeymap,
|
|
67
73
|
defaultKeymap,
|
|
@@ -70,23 +76,28 @@ export const basicNeo4jSetup = (): Extension[] => {
|
|
|
70
76
|
foldKeymap,
|
|
71
77
|
completionKeymap,
|
|
72
78
|
lintKeymap,
|
|
73
|
-
{
|
|
74
|
-
key: 'Tab',
|
|
75
|
-
preventDefault: true,
|
|
76
|
-
run: acceptCompletion,
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
key: 'Tab',
|
|
80
|
-
preventDefault: true,
|
|
81
|
-
run: insertTab,
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
key: 'Shift-Tab',
|
|
85
|
-
preventDefault: true,
|
|
86
|
-
run: indentLess,
|
|
87
|
-
},
|
|
88
79
|
].flat();
|
|
89
80
|
|
|
81
|
+
if (!moveFocusOnTab) {
|
|
82
|
+
keymaps.push(
|
|
83
|
+
{
|
|
84
|
+
key: 'Tab',
|
|
85
|
+
preventDefault: true,
|
|
86
|
+
run: acceptCompletion,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: 'Tab',
|
|
90
|
+
preventDefault: true,
|
|
91
|
+
run: insertTab,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
key: 'Shift-Tab',
|
|
95
|
+
preventDefault: true,
|
|
96
|
+
run: indentLess,
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
const extensions: Extension[] = [];
|
|
91
102
|
|
|
92
103
|
extensions.push(highlightSpecialChars());
|