@lexical/code-prism 0.41.1-nightly.20260309.0
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/CodeHighlighterPrism.d.ts +23 -0
- package/FacadePrism.d.ts +44 -0
- package/LICENSE +21 -0
- package/LexicalCodePrism.dev.js +951 -0
- package/LexicalCodePrism.dev.mjs +939 -0
- package/LexicalCodePrism.js +11 -0
- package/LexicalCodePrism.js.flow +38 -0
- package/LexicalCodePrism.mjs +22 -0
- package/LexicalCodePrism.node.mjs +20 -0
- package/LexicalCodePrism.prod.js +9 -0
- package/LexicalCodePrism.prod.mjs +9 -0
- package/README.md +5 -0
- package/index.d.ts +9 -0
- package/package.json +46 -0
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { $createCodeHighlightNode, DEFAULT_CODE_LANGUAGE, $isCodeNode, $isCodeHighlightNode, $getFirstCodeNodeOfLine, $getLastCodeNodeOfLine, $getCodeLineDirection, $getStartOfCodeInLine, $getEndOfCodeInLine, CodeNode, CodeHighlightNode } from '@lexical/code-core';
|
|
10
|
+
import { $createLineBreakNode, $createTabNode, $createTextNode, $isLineBreakNode, $getNodeByKey, $getSelection, $isRangeSelection, $isTextNode, $isTabNode, $createPoint, INDENT_CONTENT_COMMAND, OUTDENT_CONTENT_COMMAND, INSERT_TAB_COMMAND, $setSelectionFromCaretRange, $getCaretRangeInDirection, $getCaretRange, $getTextPointCaret, $normalizeCaret, $getSiblingCaret, KEY_ARROW_UP_COMMAND, MOVE_TO_START, TextNode, KEY_TAB_COMMAND, COMMAND_PRIORITY_LOW, $insertNodes, $getRoot, KEY_ARROW_DOWN_COMMAND, MOVE_TO_END, mergeRegister } from 'lexical';
|
|
11
|
+
import 'prismjs';
|
|
12
|
+
import 'prismjs/components/prism-clike.js';
|
|
13
|
+
import 'prismjs/components/prism-javascript.js';
|
|
14
|
+
import 'prismjs/components/prism-markup.js';
|
|
15
|
+
import 'prismjs/components/prism-markdown.js';
|
|
16
|
+
import 'prismjs/components/prism-c.js';
|
|
17
|
+
import 'prismjs/components/prism-css.js';
|
|
18
|
+
import 'prismjs/components/prism-objectivec.js';
|
|
19
|
+
import 'prismjs/components/prism-sql.js';
|
|
20
|
+
import 'prismjs/components/prism-powershell.js';
|
|
21
|
+
import 'prismjs/components/prism-python.js';
|
|
22
|
+
import 'prismjs/components/prism-rust.js';
|
|
23
|
+
import 'prismjs/components/prism-swift.js';
|
|
24
|
+
import 'prismjs/components/prism-typescript.js';
|
|
25
|
+
import 'prismjs/components/prism-java.js';
|
|
26
|
+
import 'prismjs/components/prism-cpp.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
30
|
+
*
|
|
31
|
+
* This source code is licensed under the MIT license found in the
|
|
32
|
+
* LICENSE file in the root directory of this source tree.
|
|
33
|
+
*
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
// Do not require this module directly! Use normal `invariant` calls.
|
|
37
|
+
|
|
38
|
+
function formatDevErrorMessage(message) {
|
|
39
|
+
throw new Error(message);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
44
|
+
*
|
|
45
|
+
* This source code is licensed under the MIT license found in the
|
|
46
|
+
* LICENSE file in the root directory of this source tree.
|
|
47
|
+
*
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
// invariant(condition, message) will refine types based on "condition", and
|
|
51
|
+
// if "condition" is false will throw an error. This function is special-cased
|
|
52
|
+
// in flow itself, so we can't name it anything else.
|
|
53
|
+
function invariant(cond, message, ...args) {
|
|
54
|
+
if (cond) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
throw new Error('Internal Lexical error: invariant() is meant to be replaced at compile ' + 'time. There is no runtime version. Error: ' + message);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
(function (Prism) {
|
|
61
|
+
|
|
62
|
+
Prism.languages.diff = {
|
|
63
|
+
'coord': [
|
|
64
|
+
// Match all kinds of coord lines (prefixed by "+++", "---" or "***").
|
|
65
|
+
/^(?:\*{3}|-{3}|\+{3}).*$/m,
|
|
66
|
+
// Match "@@ ... @@" coord lines in unified diff.
|
|
67
|
+
/^@@.*@@$/m,
|
|
68
|
+
// Match coord lines in normal diff (starts with a number).
|
|
69
|
+
/^\d.*$/m
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
// deleted, inserted, unchanged, diff
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A map from the name of a block to its line prefix.
|
|
77
|
+
*
|
|
78
|
+
* @type {Object<string, string>}
|
|
79
|
+
*/
|
|
80
|
+
var PREFIXES = {
|
|
81
|
+
'deleted-sign': '-',
|
|
82
|
+
'deleted-arrow': '<',
|
|
83
|
+
'inserted-sign': '+',
|
|
84
|
+
'inserted-arrow': '>',
|
|
85
|
+
'unchanged': ' ',
|
|
86
|
+
'diff': '!',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// add a token for each prefix
|
|
90
|
+
Object.keys(PREFIXES).forEach(function (name) {
|
|
91
|
+
var prefix = PREFIXES[name];
|
|
92
|
+
|
|
93
|
+
var alias = [];
|
|
94
|
+
if (!/^\w+$/.test(name)) { // "deleted-sign" -> "deleted"
|
|
95
|
+
alias.push(/\w+/.exec(name)[0]);
|
|
96
|
+
}
|
|
97
|
+
if (name === 'diff') {
|
|
98
|
+
alias.push('bold');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Prism.languages.diff[name] = {
|
|
102
|
+
pattern: RegExp('^(?:[' + prefix + '].*(?:\r\n?|\n|(?![\\s\\S])))+', 'm'),
|
|
103
|
+
alias: alias,
|
|
104
|
+
inside: {
|
|
105
|
+
'line': {
|
|
106
|
+
pattern: /(.)(?=[\s\S]).*(?:\r\n?|\n)?/,
|
|
107
|
+
lookbehind: true
|
|
108
|
+
},
|
|
109
|
+
'prefix': {
|
|
110
|
+
pattern: /[\s\S]/,
|
|
111
|
+
alias: /\w+/.exec(name)[0]
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// make prefixes available to Diff plugin
|
|
119
|
+
Object.defineProperty(Prism.languages.diff, 'PREFIXES', {
|
|
120
|
+
value: PREFIXES
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
}(Prism));
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
127
|
+
*
|
|
128
|
+
* This source code is licensed under the MIT license found in the
|
|
129
|
+
* LICENSE file in the root directory of this source tree.
|
|
130
|
+
*
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
const Prism$1 = globalThis.Prism || window.Prism;
|
|
134
|
+
const CODE_LANGUAGE_FRIENDLY_NAME_MAP = {
|
|
135
|
+
c: 'C',
|
|
136
|
+
clike: 'C-like',
|
|
137
|
+
cpp: 'C++',
|
|
138
|
+
css: 'CSS',
|
|
139
|
+
html: 'HTML',
|
|
140
|
+
java: 'Java',
|
|
141
|
+
js: 'JavaScript',
|
|
142
|
+
markdown: 'Markdown',
|
|
143
|
+
objc: 'Objective-C',
|
|
144
|
+
plain: 'Plain Text',
|
|
145
|
+
powershell: 'PowerShell',
|
|
146
|
+
py: 'Python',
|
|
147
|
+
rust: 'Rust',
|
|
148
|
+
sql: 'SQL',
|
|
149
|
+
swift: 'Swift',
|
|
150
|
+
typescript: 'TypeScript',
|
|
151
|
+
xml: 'XML'
|
|
152
|
+
};
|
|
153
|
+
const CODE_LANGUAGE_MAP = {
|
|
154
|
+
cpp: 'cpp',
|
|
155
|
+
java: 'java',
|
|
156
|
+
javascript: 'js',
|
|
157
|
+
md: 'markdown',
|
|
158
|
+
plaintext: 'plain',
|
|
159
|
+
python: 'py',
|
|
160
|
+
text: 'plain',
|
|
161
|
+
ts: 'typescript'
|
|
162
|
+
};
|
|
163
|
+
function normalizeCodeLanguage(lang) {
|
|
164
|
+
return CODE_LANGUAGE_MAP[lang] || lang;
|
|
165
|
+
}
|
|
166
|
+
function getLanguageFriendlyName(lang) {
|
|
167
|
+
const _lang = normalizeCodeLanguage(lang);
|
|
168
|
+
return CODE_LANGUAGE_FRIENDLY_NAME_MAP[_lang] || _lang;
|
|
169
|
+
}
|
|
170
|
+
const getCodeLanguages = () => Object.keys(Prism$1.languages).filter(
|
|
171
|
+
// Prism has several language helpers mixed into languages object
|
|
172
|
+
// so filtering them out here to get langs list
|
|
173
|
+
language => typeof Prism$1.languages[language] !== 'function').sort();
|
|
174
|
+
function getCodeLanguageOptions() {
|
|
175
|
+
const options = [];
|
|
176
|
+
for (const [lang, friendlyName] of Object.entries(CODE_LANGUAGE_FRIENDLY_NAME_MAP)) {
|
|
177
|
+
options.push([lang, friendlyName]);
|
|
178
|
+
}
|
|
179
|
+
return options;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Prism has no theme support
|
|
183
|
+
function getCodeThemeOptions() {
|
|
184
|
+
const options = [];
|
|
185
|
+
return options;
|
|
186
|
+
}
|
|
187
|
+
function getDiffedLanguage(language) {
|
|
188
|
+
const DIFF_LANGUAGE_REGEX = /^diff-([\w-]+)/i;
|
|
189
|
+
const diffLanguageMatch = DIFF_LANGUAGE_REGEX.exec(language);
|
|
190
|
+
return diffLanguageMatch ? diffLanguageMatch[1] : null;
|
|
191
|
+
}
|
|
192
|
+
function isCodeLanguageLoaded(language) {
|
|
193
|
+
const diffedLanguage = getDiffedLanguage(language);
|
|
194
|
+
const langId = diffedLanguage ? diffedLanguage : language;
|
|
195
|
+
try {
|
|
196
|
+
// eslint-disable-next-line no-prototype-builtins
|
|
197
|
+
return langId ? Prism$1.languages.hasOwnProperty(langId) : false;
|
|
198
|
+
} catch (_unused) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function loadCodeLanguage(language, editor, codeNodeKey) {
|
|
203
|
+
// NOT IMPLEMENTED
|
|
204
|
+
}
|
|
205
|
+
function getTextContent(token) {
|
|
206
|
+
if (typeof token === 'string') {
|
|
207
|
+
return token;
|
|
208
|
+
} else if (Array.isArray(token)) {
|
|
209
|
+
return token.map(getTextContent).join('');
|
|
210
|
+
} else {
|
|
211
|
+
return getTextContent(token.content);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// The following code is extracted/adapted from prismjs v2
|
|
216
|
+
// It will probably be possible to use it directly from prism v2
|
|
217
|
+
// in the future when prismjs v2 is published and Lexical upgrades
|
|
218
|
+
// the prismsjs dependency
|
|
219
|
+
function tokenizeDiffHighlight(tokens, language) {
|
|
220
|
+
const diffLanguage = language;
|
|
221
|
+
const diffGrammar = Prism$1.languages[diffLanguage];
|
|
222
|
+
const env = {
|
|
223
|
+
tokens
|
|
224
|
+
};
|
|
225
|
+
const PREFIXES = Prism$1.languages.diff.PREFIXES;
|
|
226
|
+
for (const token of env.tokens) {
|
|
227
|
+
if (typeof token === 'string' || !(token.type in PREFIXES) || !Array.isArray(token.content)) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const type = token.type;
|
|
231
|
+
let insertedPrefixes = 0;
|
|
232
|
+
const getPrefixToken = () => {
|
|
233
|
+
insertedPrefixes++;
|
|
234
|
+
return new Prism$1.Token('prefix', PREFIXES[type], type.replace(/^(\w+).*/, '$1'));
|
|
235
|
+
};
|
|
236
|
+
const withoutPrefixes = token.content.filter(t => typeof t === 'string' || t.type !== 'prefix');
|
|
237
|
+
const prefixCount = token.content.length - withoutPrefixes.length;
|
|
238
|
+
const diffTokens = Prism$1.tokenize(getTextContent(withoutPrefixes), diffGrammar);
|
|
239
|
+
|
|
240
|
+
// re-insert prefixes
|
|
241
|
+
// always add a prefix at the start
|
|
242
|
+
diffTokens.unshift(getPrefixToken());
|
|
243
|
+
const LINE_BREAK = /\r\n|\n/g;
|
|
244
|
+
const insertAfterLineBreakString = text => {
|
|
245
|
+
const result = [];
|
|
246
|
+
LINE_BREAK.lastIndex = 0;
|
|
247
|
+
let last = 0;
|
|
248
|
+
let m;
|
|
249
|
+
while (insertedPrefixes < prefixCount && (m = LINE_BREAK.exec(text))) {
|
|
250
|
+
const end = m.index + m[0].length;
|
|
251
|
+
result.push(text.slice(last, end));
|
|
252
|
+
last = end;
|
|
253
|
+
result.push(getPrefixToken());
|
|
254
|
+
}
|
|
255
|
+
if (result.length === 0) {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
if (last < text.length) {
|
|
259
|
+
result.push(text.slice(last));
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
};
|
|
263
|
+
const insertAfterLineBreak = toks => {
|
|
264
|
+
for (let i = 0; i < toks.length && insertedPrefixes < prefixCount; i++) {
|
|
265
|
+
const tok = toks[i];
|
|
266
|
+
if (typeof tok === 'string') {
|
|
267
|
+
const inserted = insertAfterLineBreakString(tok);
|
|
268
|
+
if (inserted) {
|
|
269
|
+
toks.splice(i, 1, ...inserted);
|
|
270
|
+
i += inserted.length - 1;
|
|
271
|
+
}
|
|
272
|
+
} else if (typeof tok.content === 'string') {
|
|
273
|
+
const inserted = insertAfterLineBreakString(tok.content);
|
|
274
|
+
if (inserted) {
|
|
275
|
+
tok.content = inserted;
|
|
276
|
+
}
|
|
277
|
+
} else if (Array.isArray(tok.content)) {
|
|
278
|
+
insertAfterLineBreak(tok.content);
|
|
279
|
+
} else {
|
|
280
|
+
insertAfterLineBreak([tok.content]);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
insertAfterLineBreak(diffTokens);
|
|
285
|
+
if (insertedPrefixes < prefixCount) {
|
|
286
|
+
// we are missing the last prefix
|
|
287
|
+
diffTokens.push(getPrefixToken());
|
|
288
|
+
}
|
|
289
|
+
token.content = diffTokens;
|
|
290
|
+
}
|
|
291
|
+
return env.tokens;
|
|
292
|
+
}
|
|
293
|
+
function $getHighlightNodes(codeNode, language) {
|
|
294
|
+
const DIFF_LANGUAGE_REGEX = /^diff-([\w-]+)/i;
|
|
295
|
+
const diffLanguageMatch = DIFF_LANGUAGE_REGEX.exec(language);
|
|
296
|
+
const code = codeNode.getTextContent();
|
|
297
|
+
let tokens = Prism$1.tokenize(code, Prism$1.languages[diffLanguageMatch ? 'diff' : language]);
|
|
298
|
+
if (diffLanguageMatch) {
|
|
299
|
+
tokens = tokenizeDiffHighlight(tokens, diffLanguageMatch[1]);
|
|
300
|
+
}
|
|
301
|
+
return $mapTokensToLexicalStructure(tokens);
|
|
302
|
+
}
|
|
303
|
+
function $mapTokensToLexicalStructure(tokens, type) {
|
|
304
|
+
const nodes = [];
|
|
305
|
+
for (const token of tokens) {
|
|
306
|
+
if (typeof token === 'string') {
|
|
307
|
+
const partials = token.split(/(\n|\t)/);
|
|
308
|
+
const partialsLength = partials.length;
|
|
309
|
+
for (let i = 0; i < partialsLength; i++) {
|
|
310
|
+
const part = partials[i];
|
|
311
|
+
if (part === '\n' || part === '\r\n') {
|
|
312
|
+
nodes.push($createLineBreakNode());
|
|
313
|
+
} else if (part === '\t') {
|
|
314
|
+
nodes.push($createTabNode());
|
|
315
|
+
} else if (part.length > 0) {
|
|
316
|
+
nodes.push($createCodeHighlightNode(part, type));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
const {
|
|
321
|
+
content,
|
|
322
|
+
alias
|
|
323
|
+
} = token;
|
|
324
|
+
if (typeof content === 'string') {
|
|
325
|
+
nodes.push(...$mapTokensToLexicalStructure([content], token.type === 'prefix' && typeof alias === 'string' ? alias : token.type));
|
|
326
|
+
} else if (Array.isArray(content)) {
|
|
327
|
+
nodes.push(...$mapTokensToLexicalStructure(content, token.type === 'unchanged' ? undefined : token.type));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return nodes;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const PrismTokenizer = {
|
|
335
|
+
$tokenize(codeNode, language) {
|
|
336
|
+
return $getHighlightNodes(codeNode, language || this.defaultLanguage);
|
|
337
|
+
},
|
|
338
|
+
defaultLanguage: DEFAULT_CODE_LANGUAGE,
|
|
339
|
+
tokenize(code, language) {
|
|
340
|
+
return Prism$1.tokenize(code, Prism$1.languages[language || ''] || Prism$1.languages[this.defaultLanguage]);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
function $textNodeTransform(node, editor, tokenizer) {
|
|
344
|
+
// Since CodeNode has flat children structure we only need to check
|
|
345
|
+
// if node's parent is a code node and run highlighting if so
|
|
346
|
+
const parentNode = node.getParent();
|
|
347
|
+
if ($isCodeNode(parentNode)) {
|
|
348
|
+
codeNodeTransform(parentNode, editor, tokenizer);
|
|
349
|
+
} else if ($isCodeHighlightNode(node)) {
|
|
350
|
+
// When code block converted into paragraph or other element
|
|
351
|
+
// code highlight nodes converted back to normal text
|
|
352
|
+
node.replace($createTextNode(node.__text));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function updateCodeGutter(node, editor) {
|
|
356
|
+
const codeElement = editor.getElementByKey(node.getKey());
|
|
357
|
+
if (codeElement === null) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const children = node.getChildren();
|
|
361
|
+
const childrenLength = children.length;
|
|
362
|
+
// @ts-ignore: internal field
|
|
363
|
+
if (childrenLength === codeElement.__cachedChildrenLength) {
|
|
364
|
+
// Avoid updating the attribute if the children length hasn't changed.
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// @ts-ignore:: internal field
|
|
368
|
+
codeElement.__cachedChildrenLength = childrenLength;
|
|
369
|
+
let gutter = '1';
|
|
370
|
+
let count = 1;
|
|
371
|
+
for (let i = 0; i < childrenLength; i++) {
|
|
372
|
+
if ($isLineBreakNode(children[i])) {
|
|
373
|
+
gutter += '\n' + ++count;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
codeElement.setAttribute('data-gutter', gutter);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Using `skipTransforms` to prevent extra transforms since reformatting the code
|
|
380
|
+
// will not affect code block content itself.
|
|
381
|
+
//
|
|
382
|
+
// Using extra cache (`nodesCurrentlyHighlighting`) since both CodeNode and CodeHighlightNode
|
|
383
|
+
// transforms might be called at the same time (e.g. new CodeHighlight node inserted) and
|
|
384
|
+
// in both cases we'll rerun whole reformatting over CodeNode, which is redundant.
|
|
385
|
+
// Especially when pasting code into CodeBlock.
|
|
386
|
+
|
|
387
|
+
const nodesCurrentlyHighlighting = new Set();
|
|
388
|
+
function codeNodeTransform(node, editor, tokenizer) {
|
|
389
|
+
const nodeKey = node.getKey();
|
|
390
|
+
const cacheKey = editor.getKey() + '/' + nodeKey;
|
|
391
|
+
|
|
392
|
+
// When new code block inserted it might not have language selected
|
|
393
|
+
if (node.getLanguage() === undefined) {
|
|
394
|
+
node.setLanguage(tokenizer.defaultLanguage);
|
|
395
|
+
}
|
|
396
|
+
const language = node.getLanguage() || tokenizer.defaultLanguage;
|
|
397
|
+
if (isCodeLanguageLoaded(language)) {
|
|
398
|
+
if (!node.getIsSyntaxHighlightSupported()) {
|
|
399
|
+
node.setIsSyntaxHighlightSupported(true);
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
if (node.getIsSyntaxHighlightSupported()) {
|
|
403
|
+
node.setIsSyntaxHighlightSupported(false);
|
|
404
|
+
}
|
|
405
|
+
loadCodeLanguage(language, editor, nodeKey);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (nodesCurrentlyHighlighting.has(cacheKey)) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
nodesCurrentlyHighlighting.add(cacheKey);
|
|
412
|
+
|
|
413
|
+
// Using nested update call to pass `skipTransforms` since we don't want
|
|
414
|
+
// each individual CodeHighlightNode to be transformed again as it's already
|
|
415
|
+
// in its final state
|
|
416
|
+
editor.update(() => {
|
|
417
|
+
$updateAndRetainSelection(nodeKey, () => {
|
|
418
|
+
const currentNode = $getNodeByKey(nodeKey);
|
|
419
|
+
if (!$isCodeNode(currentNode) || !currentNode.isAttached()) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
//const DIFF_LANGUAGE_REGEX = /^diff-([\w-]+)/i;
|
|
423
|
+
const currentLanguage = currentNode.getLanguage() || tokenizer.defaultLanguage;
|
|
424
|
+
//const diffLanguageMatch = DIFF_LANGUAGE_REGEX.exec(currentLanguage);
|
|
425
|
+
|
|
426
|
+
const highlightNodes = tokenizer.$tokenize(currentNode, currentLanguage);
|
|
427
|
+
const diffRange = getDiffRange(currentNode.getChildren(), highlightNodes);
|
|
428
|
+
const {
|
|
429
|
+
from,
|
|
430
|
+
to,
|
|
431
|
+
nodesForReplacement
|
|
432
|
+
} = diffRange;
|
|
433
|
+
if (from !== to || nodesForReplacement.length) {
|
|
434
|
+
node.splice(from, to - from, nodesForReplacement);
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
438
|
+
});
|
|
439
|
+
}, {
|
|
440
|
+
onUpdate: () => {
|
|
441
|
+
nodesCurrentlyHighlighting.delete(cacheKey);
|
|
442
|
+
},
|
|
443
|
+
skipTransforms: true
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Wrapping update function into selection retainer, that tries to keep cursor at the same
|
|
448
|
+
// position as before.
|
|
449
|
+
function $updateAndRetainSelection(nodeKey, updateFn) {
|
|
450
|
+
const node = $getNodeByKey(nodeKey);
|
|
451
|
+
if (!$isCodeNode(node) || !node.isAttached()) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const selection = $getSelection();
|
|
455
|
+
// If it's not range selection (or null selection) there's no need to change it,
|
|
456
|
+
// but we can still run highlighting logic
|
|
457
|
+
if (!$isRangeSelection(selection)) {
|
|
458
|
+
updateFn();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const anchor = selection.anchor;
|
|
462
|
+
const anchorOffset = anchor.offset;
|
|
463
|
+
const isNewLineAnchor = anchor.type === 'element' && $isLineBreakNode(node.getChildAtIndex(anchor.offset - 1));
|
|
464
|
+
let textOffset = 0;
|
|
465
|
+
|
|
466
|
+
// Calculating previous text offset (all text node prior to anchor + anchor own text offset)
|
|
467
|
+
if (!isNewLineAnchor) {
|
|
468
|
+
const anchorNode = anchor.getNode();
|
|
469
|
+
textOffset = anchorOffset + anchorNode.getPreviousSiblings().reduce((offset, _node) => {
|
|
470
|
+
return offset + _node.getTextContentSize();
|
|
471
|
+
}, 0);
|
|
472
|
+
}
|
|
473
|
+
const hasChanges = updateFn();
|
|
474
|
+
if (!hasChanges) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Non-text anchors only happen for line breaks, otherwise
|
|
479
|
+
// selection will be within text node (code highlight node)
|
|
480
|
+
if (isNewLineAnchor) {
|
|
481
|
+
anchor.getNode().select(anchorOffset, anchorOffset);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// If it was non-element anchor then we walk through child nodes
|
|
486
|
+
// and looking for a position of original text offset
|
|
487
|
+
node.getChildren().some(_node => {
|
|
488
|
+
const isText = $isTextNode(_node);
|
|
489
|
+
if (isText || $isLineBreakNode(_node)) {
|
|
490
|
+
const textContentSize = _node.getTextContentSize();
|
|
491
|
+
if (isText && textContentSize >= textOffset) {
|
|
492
|
+
_node.select(textOffset, textOffset);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
textOffset -= textContentSize;
|
|
496
|
+
}
|
|
497
|
+
return false;
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Finds minimal diff range between two nodes lists. It returns from/to range boundaries of prevNodes
|
|
502
|
+
// that needs to be replaced with `nodes` (subset of nextNodes) to make prevNodes equal to nextNodes.
|
|
503
|
+
function getDiffRange(prevNodes, nextNodes) {
|
|
504
|
+
let leadingMatch = 0;
|
|
505
|
+
while (leadingMatch < prevNodes.length) {
|
|
506
|
+
if (!isEqual(prevNodes[leadingMatch], nextNodes[leadingMatch])) {
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
leadingMatch++;
|
|
510
|
+
}
|
|
511
|
+
const prevNodesLength = prevNodes.length;
|
|
512
|
+
const nextNodesLength = nextNodes.length;
|
|
513
|
+
const maxTrailingMatch = Math.min(prevNodesLength, nextNodesLength) - leadingMatch;
|
|
514
|
+
let trailingMatch = 0;
|
|
515
|
+
while (trailingMatch < maxTrailingMatch) {
|
|
516
|
+
trailingMatch++;
|
|
517
|
+
if (!isEqual(prevNodes[prevNodesLength - trailingMatch], nextNodes[nextNodesLength - trailingMatch])) {
|
|
518
|
+
trailingMatch--;
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const from = leadingMatch;
|
|
523
|
+
const to = prevNodesLength - trailingMatch;
|
|
524
|
+
const nodesForReplacement = nextNodes.slice(leadingMatch, nextNodesLength - trailingMatch);
|
|
525
|
+
return {
|
|
526
|
+
from,
|
|
527
|
+
nodesForReplacement,
|
|
528
|
+
to
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function isEqual(nodeA, nodeB) {
|
|
532
|
+
// Only checking for code highlight nodes, tabs and linebreaks. If it's regular text node
|
|
533
|
+
// returning false so that it's transformed into code highlight node
|
|
534
|
+
return $isCodeHighlightNode(nodeA) && $isCodeHighlightNode(nodeB) && nodeA.__text === nodeB.__text && nodeA.__highlightType === nodeB.__highlightType || $isTabNode(nodeA) && $isTabNode(nodeB) || $isLineBreakNode(nodeA) && $isLineBreakNode(nodeB);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Returns a boolean.
|
|
539
|
+
* Check that the selection span is within a single CodeNode.
|
|
540
|
+
* This is used to guard against executing handlers that can only be
|
|
541
|
+
* applied in a single CodeNode context
|
|
542
|
+
*/
|
|
543
|
+
function $isSelectionInCode(selection) {
|
|
544
|
+
if (!$isRangeSelection(selection)) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
const anchorNode = selection.anchor.getNode();
|
|
548
|
+
const maybeAnchorCodeNode = $isCodeNode(anchorNode) ? anchorNode : anchorNode.getParent();
|
|
549
|
+
const focusNode = selection.focus.getNode();
|
|
550
|
+
const maybeFocusCodeNode = $isCodeNode(focusNode) ? focusNode : focusNode.getParent();
|
|
551
|
+
return $isCodeNode(maybeAnchorCodeNode) && maybeAnchorCodeNode.is(maybeFocusCodeNode);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Returns an Array of code lines
|
|
556
|
+
* Take the sequence of LineBreakNode | TabNode | CodeHighlightNode forming
|
|
557
|
+
* the selection and split it by LineBreakNode.
|
|
558
|
+
* If the selection ends at the start of the last line, it is considered empty.
|
|
559
|
+
* Empty lines are discarded.
|
|
560
|
+
*/
|
|
561
|
+
function $getCodeLines(selection) {
|
|
562
|
+
const nodes = selection.getNodes();
|
|
563
|
+
const lines = [];
|
|
564
|
+
if (nodes.length === 1 && $isCodeNode(nodes[0])) {
|
|
565
|
+
return lines;
|
|
566
|
+
}
|
|
567
|
+
let lastLine = [];
|
|
568
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
569
|
+
const node = nodes[i];
|
|
570
|
+
if (!($isCodeHighlightNode(node) || $isTabNode(node) || $isLineBreakNode(node))) {
|
|
571
|
+
formatDevErrorMessage(`Expected selection to be inside CodeBlock and consisting of CodeHighlightNode, TabNode and LineBreakNode`);
|
|
572
|
+
}
|
|
573
|
+
if ($isLineBreakNode(node)) {
|
|
574
|
+
if (lastLine.length > 0) {
|
|
575
|
+
lines.push(lastLine);
|
|
576
|
+
lastLine = [];
|
|
577
|
+
}
|
|
578
|
+
} else {
|
|
579
|
+
lastLine.push(node);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (lastLine.length > 0) {
|
|
583
|
+
const selectionEnd = selection.isBackward() ? selection.anchor : selection.focus;
|
|
584
|
+
|
|
585
|
+
// Discard the last line if the selection ends exactly at the
|
|
586
|
+
// start of the line (no real selection)
|
|
587
|
+
const lastPoint = $createPoint(lastLine[0].getKey(), 0, 'text');
|
|
588
|
+
if (!selectionEnd.is(lastPoint)) {
|
|
589
|
+
lines.push(lastLine);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return lines;
|
|
593
|
+
}
|
|
594
|
+
function $handleTab(shiftKey) {
|
|
595
|
+
const selection = $getSelection();
|
|
596
|
+
if (!$isRangeSelection(selection) || !$isSelectionInCode(selection)) {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
const indentOrOutdent = !shiftKey ? INDENT_CONTENT_COMMAND : OUTDENT_CONTENT_COMMAND;
|
|
600
|
+
const tabOrOutdent = !shiftKey ? INSERT_TAB_COMMAND : OUTDENT_CONTENT_COMMAND;
|
|
601
|
+
const anchor = selection.anchor;
|
|
602
|
+
const focus = selection.focus;
|
|
603
|
+
|
|
604
|
+
// 1. early decision when there is no real selection
|
|
605
|
+
if (anchor.is(focus)) {
|
|
606
|
+
return tabOrOutdent;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// 2. If only empty lines or multiple non-empty lines are selected: indent/outdent
|
|
610
|
+
const codeLines = $getCodeLines(selection);
|
|
611
|
+
if (codeLines.length !== 1) {
|
|
612
|
+
return indentOrOutdent;
|
|
613
|
+
}
|
|
614
|
+
const codeLine = codeLines[0];
|
|
615
|
+
const codeLineLength = codeLine.length;
|
|
616
|
+
if (!(codeLineLength !== 0)) {
|
|
617
|
+
formatDevErrorMessage(`$getCodeLines only extracts non-empty lines`);
|
|
618
|
+
} // Take into account the direction of the selection
|
|
619
|
+
let selectionFirst;
|
|
620
|
+
let selectionLast;
|
|
621
|
+
if (selection.isBackward()) {
|
|
622
|
+
selectionFirst = focus;
|
|
623
|
+
selectionLast = anchor;
|
|
624
|
+
} else {
|
|
625
|
+
selectionFirst = anchor;
|
|
626
|
+
selectionLast = focus;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// find boundary elements of the line
|
|
630
|
+
// since codeLine only contains TabNode | CodeHighlightNode
|
|
631
|
+
// the result of these functions should is of Type TabNode | CodeHighlightNode
|
|
632
|
+
const firstOfLine = $getFirstCodeNodeOfLine(codeLine[0]);
|
|
633
|
+
const lastOfLine = $getLastCodeNodeOfLine(codeLine[0]);
|
|
634
|
+
const anchorOfLine = $createPoint(firstOfLine.getKey(), 0, 'text');
|
|
635
|
+
const focusOfLine = $createPoint(lastOfLine.getKey(), lastOfLine.getTextContentSize(), 'text');
|
|
636
|
+
|
|
637
|
+
// 3. multiline because selection started strictly before the line
|
|
638
|
+
if (selectionFirst.isBefore(anchorOfLine)) {
|
|
639
|
+
return indentOrOutdent;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// 4. multiline because the selection stops strictly after the line
|
|
643
|
+
if (focusOfLine.isBefore(selectionLast)) {
|
|
644
|
+
return indentOrOutdent;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// The selection if within the line.
|
|
648
|
+
// 4. If it does not touch both borders, it needs a tab
|
|
649
|
+
if (anchorOfLine.isBefore(selectionFirst) || selectionLast.isBefore(focusOfLine)) {
|
|
650
|
+
return tabOrOutdent;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// 5. Selection is matching a full line on non-empty code
|
|
654
|
+
return indentOrOutdent;
|
|
655
|
+
}
|
|
656
|
+
function $handleMultilineIndent(type) {
|
|
657
|
+
const selection = $getSelection();
|
|
658
|
+
if (!$isRangeSelection(selection) || !$isSelectionInCode(selection)) {
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
const codeLines = $getCodeLines(selection);
|
|
662
|
+
const codeLinesLength = codeLines.length;
|
|
663
|
+
|
|
664
|
+
// Special Indent case
|
|
665
|
+
// Selection is collapsed at the beginning of a line
|
|
666
|
+
if (codeLinesLength === 0 && selection.isCollapsed()) {
|
|
667
|
+
if (type === INDENT_CONTENT_COMMAND) {
|
|
668
|
+
selection.insertNodes([$createTabNode()]);
|
|
669
|
+
}
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Special Indent case
|
|
674
|
+
// Selection is matching only one LineBreak
|
|
675
|
+
if (codeLinesLength === 0 && type === INDENT_CONTENT_COMMAND && selection.getTextContent() === '\n') {
|
|
676
|
+
const tabNode = $createTabNode();
|
|
677
|
+
const lineBreakNode = $createLineBreakNode();
|
|
678
|
+
const direction = selection.isBackward() ? 'previous' : 'next';
|
|
679
|
+
selection.insertNodes([tabNode, lineBreakNode]);
|
|
680
|
+
$setSelectionFromCaretRange($getCaretRangeInDirection($getCaretRange($getTextPointCaret(tabNode, 'next', 0), $normalizeCaret($getSiblingCaret(lineBreakNode, 'next'))), direction));
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Indent Non Empty Lines
|
|
685
|
+
for (let i = 0; i < codeLinesLength; i++) {
|
|
686
|
+
const line = codeLines[i];
|
|
687
|
+
// a line here is never empty
|
|
688
|
+
if (line.length > 0) {
|
|
689
|
+
let firstOfLine = line[0];
|
|
690
|
+
|
|
691
|
+
// make sure to consider the first node on the first line
|
|
692
|
+
// because the line might not be fully selected
|
|
693
|
+
if (i === 0) {
|
|
694
|
+
firstOfLine = $getFirstCodeNodeOfLine(firstOfLine);
|
|
695
|
+
}
|
|
696
|
+
if (type === INDENT_CONTENT_COMMAND) {
|
|
697
|
+
const tabNode = $createTabNode();
|
|
698
|
+
firstOfLine.insertBefore(tabNode);
|
|
699
|
+
// First real code line may need selection adjustment
|
|
700
|
+
// when firstOfLine is at the selection boundary
|
|
701
|
+
if (i === 0) {
|
|
702
|
+
const anchorKey = selection.isBackward() ? 'focus' : 'anchor';
|
|
703
|
+
const anchorLine = $createPoint(firstOfLine.getKey(), 0, 'text');
|
|
704
|
+
if (selection[anchorKey].is(anchorLine)) {
|
|
705
|
+
selection[anchorKey].set(tabNode.getKey(), 0, 'text');
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
} else if ($isTabNode(firstOfLine)) {
|
|
709
|
+
firstOfLine.remove();
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
function $handleShiftLines(type, event) {
|
|
716
|
+
// We only care about the alt+arrow keys
|
|
717
|
+
const selection = $getSelection();
|
|
718
|
+
if (!$isRangeSelection(selection)) {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// I'm not quite sure why, but it seems like calling anchor.getNode() collapses the selection here
|
|
723
|
+
// So first, get the anchor and the focus, then get their nodes
|
|
724
|
+
const {
|
|
725
|
+
anchor,
|
|
726
|
+
focus
|
|
727
|
+
} = selection;
|
|
728
|
+
const anchorOffset = anchor.offset;
|
|
729
|
+
const focusOffset = focus.offset;
|
|
730
|
+
const anchorNode = anchor.getNode();
|
|
731
|
+
const focusNode = focus.getNode();
|
|
732
|
+
const arrowIsUp = type === KEY_ARROW_UP_COMMAND;
|
|
733
|
+
|
|
734
|
+
// Ensure the selection is within the codeblock
|
|
735
|
+
if (!$isSelectionInCode(selection) || !($isCodeHighlightNode(anchorNode) || $isTabNode(anchorNode)) || !($isCodeHighlightNode(focusNode) || $isTabNode(focusNode))) {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
if (!event.altKey) {
|
|
739
|
+
// Handle moving selection out of the code block, given there are no
|
|
740
|
+
// siblings that can natively take the selection.
|
|
741
|
+
if (selection.isCollapsed()) {
|
|
742
|
+
const codeNode = anchorNode.getParentOrThrow();
|
|
743
|
+
if (arrowIsUp && anchorOffset === 0 && anchorNode.getPreviousSibling() === null) {
|
|
744
|
+
const codeNodeSibling = codeNode.getPreviousSibling();
|
|
745
|
+
if (codeNodeSibling === null) {
|
|
746
|
+
codeNode.selectPrevious();
|
|
747
|
+
event.preventDefault();
|
|
748
|
+
return true;
|
|
749
|
+
}
|
|
750
|
+
} else if (!arrowIsUp && anchorOffset === anchorNode.getTextContentSize() && anchorNode.getNextSibling() === null) {
|
|
751
|
+
const codeNodeSibling = codeNode.getNextSibling();
|
|
752
|
+
if (codeNodeSibling === null) {
|
|
753
|
+
codeNode.selectNext();
|
|
754
|
+
event.preventDefault();
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
let start;
|
|
762
|
+
let end;
|
|
763
|
+
if (anchorNode.isBefore(focusNode)) {
|
|
764
|
+
start = $getFirstCodeNodeOfLine(anchorNode);
|
|
765
|
+
end = $getLastCodeNodeOfLine(focusNode);
|
|
766
|
+
} else {
|
|
767
|
+
start = $getFirstCodeNodeOfLine(focusNode);
|
|
768
|
+
end = $getLastCodeNodeOfLine(anchorNode);
|
|
769
|
+
}
|
|
770
|
+
if (start == null || end == null) {
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
const range = start.getNodesBetween(end);
|
|
774
|
+
for (let i = 0; i < range.length; i++) {
|
|
775
|
+
const node = range[i];
|
|
776
|
+
if (!$isCodeHighlightNode(node) && !$isTabNode(node) && !$isLineBreakNode(node)) {
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// After this point, we know the selection is within the codeblock. We may not be able to
|
|
782
|
+
// actually move the lines around, but we want to return true either way to prevent
|
|
783
|
+
// the event's default behavior
|
|
784
|
+
event.preventDefault();
|
|
785
|
+
event.stopPropagation(); // required to stop cursor movement under Firefox
|
|
786
|
+
|
|
787
|
+
const linebreak = arrowIsUp ? start.getPreviousSibling() : end.getNextSibling();
|
|
788
|
+
if (!$isLineBreakNode(linebreak)) {
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
791
|
+
const sibling = arrowIsUp ? linebreak.getPreviousSibling() : linebreak.getNextSibling();
|
|
792
|
+
if (sibling == null) {
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
const maybeInsertionPoint = $isCodeHighlightNode(sibling) || $isTabNode(sibling) || $isLineBreakNode(sibling) ? arrowIsUp ? $getFirstCodeNodeOfLine(sibling) : $getLastCodeNodeOfLine(sibling) : null;
|
|
796
|
+
let insertionPoint = maybeInsertionPoint != null ? maybeInsertionPoint : sibling;
|
|
797
|
+
linebreak.remove();
|
|
798
|
+
range.forEach(node => node.remove());
|
|
799
|
+
if (type === KEY_ARROW_UP_COMMAND) {
|
|
800
|
+
range.forEach(node => insertionPoint.insertBefore(node));
|
|
801
|
+
insertionPoint.insertBefore(linebreak);
|
|
802
|
+
} else {
|
|
803
|
+
insertionPoint.insertAfter(linebreak);
|
|
804
|
+
insertionPoint = linebreak;
|
|
805
|
+
range.forEach(node => {
|
|
806
|
+
insertionPoint.insertAfter(node);
|
|
807
|
+
insertionPoint = node;
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
selection.setTextNodeRange(anchorNode, anchorOffset, focusNode, focusOffset);
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
function $handleMoveTo(type, event) {
|
|
814
|
+
const selection = $getSelection();
|
|
815
|
+
if (!$isRangeSelection(selection)) {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
const {
|
|
819
|
+
anchor,
|
|
820
|
+
focus
|
|
821
|
+
} = selection;
|
|
822
|
+
const anchorNode = anchor.getNode();
|
|
823
|
+
const focusNode = focus.getNode();
|
|
824
|
+
const isMoveToStart = type === MOVE_TO_START;
|
|
825
|
+
|
|
826
|
+
// Ensure the selection is within the codeblock
|
|
827
|
+
if (!$isSelectionInCode(selection) || !($isCodeHighlightNode(anchorNode) || $isTabNode(anchorNode)) || !($isCodeHighlightNode(focusNode) || $isTabNode(focusNode))) {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
const focusLineNode = focusNode;
|
|
831
|
+
const direction = $getCodeLineDirection(focusLineNode);
|
|
832
|
+
const moveToStart = direction === 'rtl' ? !isMoveToStart : isMoveToStart;
|
|
833
|
+
if (moveToStart) {
|
|
834
|
+
const start = $getStartOfCodeInLine(focusLineNode, focus.offset);
|
|
835
|
+
if (start !== null) {
|
|
836
|
+
const {
|
|
837
|
+
node,
|
|
838
|
+
offset
|
|
839
|
+
} = start;
|
|
840
|
+
if ($isLineBreakNode(node)) {
|
|
841
|
+
node.selectNext(0, 0);
|
|
842
|
+
} else {
|
|
843
|
+
selection.setTextNodeRange(node, offset, node, offset);
|
|
844
|
+
}
|
|
845
|
+
} else {
|
|
846
|
+
focusLineNode.getParentOrThrow().selectStart();
|
|
847
|
+
}
|
|
848
|
+
} else {
|
|
849
|
+
const node = $getEndOfCodeInLine(focusLineNode);
|
|
850
|
+
node.select();
|
|
851
|
+
}
|
|
852
|
+
event.preventDefault();
|
|
853
|
+
event.stopPropagation();
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
function registerCodeHighlighting(editor, tokenizer) {
|
|
857
|
+
if (!editor.hasNodes([CodeNode, CodeHighlightNode])) {
|
|
858
|
+
throw new Error('CodeHighlightPlugin: CodeNode or CodeHighlightNode not registered on editor');
|
|
859
|
+
}
|
|
860
|
+
if (tokenizer == null) {
|
|
861
|
+
tokenizer = PrismTokenizer;
|
|
862
|
+
}
|
|
863
|
+
const registrations = [];
|
|
864
|
+
|
|
865
|
+
// Only register the mutation listener if not in headless mode
|
|
866
|
+
if (editor._headless !== true) {
|
|
867
|
+
registrations.push(editor.registerMutationListener(CodeNode, mutations => {
|
|
868
|
+
editor.getEditorState().read(() => {
|
|
869
|
+
for (const [key, type] of mutations) {
|
|
870
|
+
if (type !== 'destroyed') {
|
|
871
|
+
const node = $getNodeByKey(key);
|
|
872
|
+
if (node !== null) {
|
|
873
|
+
updateCodeGutter(node, editor);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
}, {
|
|
879
|
+
skipInitialization: false
|
|
880
|
+
}));
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Add the rest of the registrations
|
|
884
|
+
registrations.push(editor.registerNodeTransform(CodeNode, node => codeNodeTransform(node, editor, tokenizer)), editor.registerNodeTransform(TextNode, node => $textNodeTransform(node, editor, tokenizer)), editor.registerNodeTransform(CodeHighlightNode, node => $textNodeTransform(node, editor, tokenizer)), editor.registerCommand(KEY_TAB_COMMAND, event => {
|
|
885
|
+
const command = $handleTab(event.shiftKey);
|
|
886
|
+
if (command === null) {
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
event.preventDefault();
|
|
890
|
+
editor.dispatchCommand(command, undefined);
|
|
891
|
+
return true;
|
|
892
|
+
}, COMMAND_PRIORITY_LOW), editor.registerCommand(INSERT_TAB_COMMAND, () => {
|
|
893
|
+
const selection = $getSelection();
|
|
894
|
+
if (!$isSelectionInCode(selection)) {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
$insertNodes([$createTabNode()]);
|
|
898
|
+
return true;
|
|
899
|
+
}, COMMAND_PRIORITY_LOW), editor.registerCommand(INDENT_CONTENT_COMMAND, payload => $handleMultilineIndent(INDENT_CONTENT_COMMAND), COMMAND_PRIORITY_LOW), editor.registerCommand(OUTDENT_CONTENT_COMMAND, payload => $handleMultilineIndent(OUTDENT_CONTENT_COMMAND), COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ARROW_UP_COMMAND, event => {
|
|
900
|
+
const selection = $getSelection();
|
|
901
|
+
if (!$isRangeSelection(selection) || !$isSelectionInCode(selection)) {
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
const firstNode = $getRoot().getFirstDescendant();
|
|
905
|
+
const {
|
|
906
|
+
anchor
|
|
907
|
+
} = selection;
|
|
908
|
+
const anchorNode = anchor.getNode();
|
|
909
|
+
if (firstNode && anchorNode && firstNode.getKey() === anchorNode.getKey()) {
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
return $handleShiftLines(KEY_ARROW_UP_COMMAND, event);
|
|
913
|
+
}, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ARROW_DOWN_COMMAND, event => {
|
|
914
|
+
const selection = $getSelection();
|
|
915
|
+
if (!$isRangeSelection(selection) || !$isSelectionInCode(selection)) {
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
const lastNode = $getRoot().getLastDescendant();
|
|
919
|
+
const {
|
|
920
|
+
anchor
|
|
921
|
+
} = selection;
|
|
922
|
+
const anchorNode = anchor.getNode();
|
|
923
|
+
if (lastNode && anchorNode && lastNode.getKey() === anchorNode.getKey()) {
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
return $handleShiftLines(KEY_ARROW_DOWN_COMMAND, event);
|
|
927
|
+
}, COMMAND_PRIORITY_LOW), editor.registerCommand(MOVE_TO_START, event => $handleMoveTo(MOVE_TO_START, event), COMMAND_PRIORITY_LOW), editor.registerCommand(MOVE_TO_END, event => $handleMoveTo(MOVE_TO_END, event), COMMAND_PRIORITY_LOW));
|
|
928
|
+
return mergeRegister(...registrations);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
933
|
+
*
|
|
934
|
+
* This source code is licensed under the MIT license found in the
|
|
935
|
+
* LICENSE file in the root directory of this source tree.
|
|
936
|
+
*
|
|
937
|
+
*/
|
|
938
|
+
|
|
939
|
+
export { CODE_LANGUAGE_FRIENDLY_NAME_MAP, CODE_LANGUAGE_MAP, PrismTokenizer, getCodeLanguageOptions, getCodeLanguages, getCodeThemeOptions, getLanguageFriendlyName, isCodeLanguageLoaded, loadCodeLanguage, normalizeCodeLanguage, registerCodeHighlighting };
|