Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol
diffdown.com
1import { parser } from '@lezer/javascript';
2import { syntaxTree, LRLanguage, indentNodeProp, continuedIndent, flatIndent, delimitedIndent, foldNodeProp, foldInside, defineLanguageFacet, sublanguageProp, LanguageSupport } from '@codemirror/language';
3import { EditorSelection } from '@codemirror/state';
4import { EditorView } from '@codemirror/view';
5import { snippetCompletion, ifNotIn, completeFromList } from '@codemirror/autocomplete';
6import { NodeWeakMap, IterMode } from '@lezer/common';
7
8/**
9A collection of JavaScript-related
10[snippets](https://codemirror.net/6/docs/ref/#autocomplete.snippet).
11*/
12const snippets = [
13 /*@__PURE__*/snippetCompletion("function ${name}(${params}) {\n\t${}\n}", {
14 label: "function",
15 detail: "definition",
16 type: "keyword"
17 }),
18 /*@__PURE__*/snippetCompletion("for (let ${index} = 0; ${index} < ${bound}; ${index}++) {\n\t${}\n}", {
19 label: "for",
20 detail: "loop",
21 type: "keyword"
22 }),
23 /*@__PURE__*/snippetCompletion("for (let ${name} of ${collection}) {\n\t${}\n}", {
24 label: "for",
25 detail: "of loop",
26 type: "keyword"
27 }),
28 /*@__PURE__*/snippetCompletion("do {\n\t${}\n} while (${})", {
29 label: "do",
30 detail: "loop",
31 type: "keyword"
32 }),
33 /*@__PURE__*/snippetCompletion("while (${}) {\n\t${}\n}", {
34 label: "while",
35 detail: "loop",
36 type: "keyword"
37 }),
38 /*@__PURE__*/snippetCompletion("try {\n\t${}\n} catch (${error}) {\n\t${}\n}", {
39 label: "try",
40 detail: "/ catch block",
41 type: "keyword"
42 }),
43 /*@__PURE__*/snippetCompletion("if (${}) {\n\t${}\n}", {
44 label: "if",
45 detail: "block",
46 type: "keyword"
47 }),
48 /*@__PURE__*/snippetCompletion("if (${}) {\n\t${}\n} else {\n\t${}\n}", {
49 label: "if",
50 detail: "/ else block",
51 type: "keyword"
52 }),
53 /*@__PURE__*/snippetCompletion("class ${name} {\n\tconstructor(${params}) {\n\t\t${}\n\t}\n}", {
54 label: "class",
55 detail: "definition",
56 type: "keyword"
57 }),
58 /*@__PURE__*/snippetCompletion("import {${names}} from \"${module}\"\n${}", {
59 label: "import",
60 detail: "named",
61 type: "keyword"
62 }),
63 /*@__PURE__*/snippetCompletion("import ${name} from \"${module}\"\n${}", {
64 label: "import",
65 detail: "default",
66 type: "keyword"
67 })
68];
69/**
70A collection of snippet completions for TypeScript. Includes the
71JavaScript [snippets](https://codemirror.net/6/docs/ref/#lang-javascript.snippets).
72*/
73const typescriptSnippets = /*@__PURE__*/snippets.concat([
74 /*@__PURE__*/snippetCompletion("interface ${name} {\n\t${}\n}", {
75 label: "interface",
76 detail: "definition",
77 type: "keyword"
78 }),
79 /*@__PURE__*/snippetCompletion("type ${name} = ${type}", {
80 label: "type",
81 detail: "definition",
82 type: "keyword"
83 }),
84 /*@__PURE__*/snippetCompletion("enum ${name} {\n\t${}\n}", {
85 label: "enum",
86 detail: "definition",
87 type: "keyword"
88 })
89]);
90
91const cache = /*@__PURE__*/new NodeWeakMap();
92const ScopeNodes = /*@__PURE__*/new Set([
93 "Script", "Block",
94 "FunctionExpression", "FunctionDeclaration", "ArrowFunction", "MethodDeclaration",
95 "ForStatement"
96]);
97function defID(type) {
98 return (node, def) => {
99 let id = node.node.getChild("VariableDefinition");
100 if (id)
101 def(id, type);
102 return true;
103 };
104}
105const functionContext = ["FunctionDeclaration"];
106const gatherCompletions = {
107 FunctionDeclaration: /*@__PURE__*/defID("function"),
108 ClassDeclaration: /*@__PURE__*/defID("class"),
109 ClassExpression: () => true,
110 EnumDeclaration: /*@__PURE__*/defID("constant"),
111 TypeAliasDeclaration: /*@__PURE__*/defID("type"),
112 NamespaceDeclaration: /*@__PURE__*/defID("namespace"),
113 VariableDefinition(node, def) { if (!node.matchContext(functionContext))
114 def(node, "variable"); },
115 TypeDefinition(node, def) { def(node, "type"); },
116 __proto__: null
117};
118function getScope(doc, node) {
119 let cached = cache.get(node);
120 if (cached)
121 return cached;
122 let completions = [], top = true;
123 function def(node, type) {
124 let name = doc.sliceString(node.from, node.to);
125 completions.push({ label: name, type });
126 }
127 node.cursor(IterMode.IncludeAnonymous).iterate(node => {
128 if (top) {
129 top = false;
130 }
131 else if (node.name) {
132 let gather = gatherCompletions[node.name];
133 if (gather && gather(node, def) || ScopeNodes.has(node.name))
134 return false;
135 }
136 else if (node.to - node.from > 8192) {
137 // Allow caching for bigger internal nodes
138 for (let c of getScope(doc, node.node))
139 completions.push(c);
140 return false;
141 }
142 });
143 cache.set(node, completions);
144 return completions;
145}
146const Identifier = /^[\w$\xa1-\uffff][\w$\d\xa1-\uffff]*$/;
147const dontComplete = [
148 "TemplateString", "String", "RegExp",
149 "LineComment", "BlockComment",
150 "VariableDefinition", "TypeDefinition", "Label",
151 "PropertyDefinition", "PropertyName",
152 "PrivatePropertyDefinition", "PrivatePropertyName",
153 "JSXText", "JSXAttributeValue", "JSXOpenTag", "JSXCloseTag", "JSXSelfClosingTag",
154 ".", "?."
155];
156/**
157Completion source that looks up locally defined names in
158JavaScript code.
159*/
160function localCompletionSource(context) {
161 let inner = syntaxTree(context.state).resolveInner(context.pos, -1);
162 if (dontComplete.indexOf(inner.name) > -1)
163 return null;
164 let isWord = inner.name == "VariableName" ||
165 inner.to - inner.from < 20 && Identifier.test(context.state.sliceDoc(inner.from, inner.to));
166 if (!isWord && !context.explicit)
167 return null;
168 let options = [];
169 for (let pos = inner; pos; pos = pos.parent) {
170 if (ScopeNodes.has(pos.name))
171 options = options.concat(getScope(context.state.doc, pos));
172 }
173 return {
174 options,
175 from: isWord ? inner.from : context.pos,
176 validFor: Identifier
177 };
178}
179function pathFor(read, member, name) {
180 var _a;
181 let path = [];
182 for (;;) {
183 let obj = member.firstChild, prop;
184 if ((obj === null || obj === void 0 ? void 0 : obj.name) == "VariableName") {
185 path.push(read(obj));
186 return { path: path.reverse(), name };
187 }
188 else if ((obj === null || obj === void 0 ? void 0 : obj.name) == "MemberExpression" && ((_a = (prop = obj.lastChild)) === null || _a === void 0 ? void 0 : _a.name) == "PropertyName") {
189 path.push(read(prop));
190 member = obj;
191 }
192 else {
193 return null;
194 }
195 }
196}
197/**
198Helper function for defining JavaScript completion sources. It
199returns the completable name and object path for a completion
200context, or null if no name/property completion should happen at
201that position. For example, when completing after `a.b.c` it will
202return `{path: ["a", "b"], name: "c"}`. When completing after `x`
203it will return `{path: [], name: "x"}`. When not in a property or
204name, it will return null if `context.explicit` is false, and
205`{path: [], name: ""}` otherwise.
206*/
207function completionPath(context) {
208 let read = (node) => context.state.doc.sliceString(node.from, node.to);
209 let inner = syntaxTree(context.state).resolveInner(context.pos, -1);
210 if (inner.name == "PropertyName") {
211 return pathFor(read, inner.parent, read(inner));
212 }
213 else if ((inner.name == "." || inner.name == "?.") && inner.parent.name == "MemberExpression") {
214 return pathFor(read, inner.parent, "");
215 }
216 else if (dontComplete.indexOf(inner.name) > -1) {
217 return null;
218 }
219 else if (inner.name == "VariableName" || inner.to - inner.from < 20 && Identifier.test(read(inner))) {
220 return { path: [], name: read(inner) };
221 }
222 else if (inner.name == "MemberExpression") {
223 return pathFor(read, inner, "");
224 }
225 else {
226 return context.explicit ? { path: [], name: "" } : null;
227 }
228}
229function enumeratePropertyCompletions(obj, top) {
230 let originalObj = obj;
231 let options = [], seen = new Set;
232 for (let depth = 0;; depth++) {
233 for (let name of (Object.getOwnPropertyNames || Object.keys)(obj)) {
234 if (!/^[a-zA-Z_$\xaa-\uffdc][\w$\xaa-\uffdc]*$/.test(name) || seen.has(name))
235 continue;
236 seen.add(name);
237 let value;
238 try {
239 value = originalObj[name];
240 }
241 catch (_) {
242 continue;
243 }
244 options.push({
245 label: name,
246 type: typeof value == "function" ? (/^[A-Z]/.test(name) ? "class" : top ? "function" : "method")
247 : top ? "variable" : "property",
248 boost: -depth
249 });
250 }
251 let next = Object.getPrototypeOf(obj);
252 if (!next)
253 return options;
254 obj = next;
255 }
256}
257/**
258Defines a [completion source](https://codemirror.net/6/docs/ref/#autocomplete.CompletionSource) that
259completes from the given scope object (for example `globalThis`).
260Will enter properties of the object when completing properties on
261a directly-named path.
262*/
263function scopeCompletionSource(scope) {
264 let cache = new Map;
265 return (context) => {
266 let path = completionPath(context);
267 if (!path)
268 return null;
269 let target = scope;
270 for (let step of path.path) {
271 target = target[step];
272 if (!target)
273 return null;
274 }
275 let options = cache.get(target);
276 if (!options)
277 cache.set(target, options = enumeratePropertyCompletions(target, !path.path.length));
278 return {
279 from: context.pos - path.name.length,
280 options,
281 validFor: Identifier
282 };
283 };
284}
285
286/**
287A language provider based on the [Lezer JavaScript
288parser](https://github.com/lezer-parser/javascript), extended with
289highlighting and indentation information.
290*/
291const javascriptLanguage = /*@__PURE__*/LRLanguage.define({
292 name: "javascript",
293 parser: /*@__PURE__*/parser.configure({
294 props: [
295 /*@__PURE__*/indentNodeProp.add({
296 IfStatement: /*@__PURE__*/continuedIndent({ except: /^\s*({|else\b)/ }),
297 TryStatement: /*@__PURE__*/continuedIndent({ except: /^\s*({|catch\b|finally\b)/ }),
298 LabeledStatement: flatIndent,
299 SwitchBody: context => {
300 let after = context.textAfter, closed = /^\s*\}/.test(after), isCase = /^\s*(case|default)\b/.test(after);
301 return context.baseIndent + (closed ? 0 : isCase ? 1 : 2) * context.unit;
302 },
303 Block: /*@__PURE__*/delimitedIndent({ closing: "}" }),
304 ArrowFunction: cx => cx.baseIndent + cx.unit,
305 "TemplateString BlockComment": () => null,
306 "Statement Property": /*@__PURE__*/continuedIndent({ except: /^\s*{/ }),
307 JSXElement(context) {
308 let closed = /^\s*<\//.test(context.textAfter);
309 return context.lineIndent(context.node.from) + (closed ? 0 : context.unit);
310 },
311 JSXEscape(context) {
312 let closed = /\s*\}/.test(context.textAfter);
313 return context.lineIndent(context.node.from) + (closed ? 0 : context.unit);
314 },
315 "JSXOpenTag JSXSelfClosingTag"(context) {
316 return context.column(context.node.from) + context.unit;
317 }
318 }),
319 /*@__PURE__*/foldNodeProp.add({
320 "Block ClassBody SwitchBody EnumBody ObjectExpression ArrayExpression ObjectType": foldInside,
321 BlockComment(tree) { return { from: tree.from + 2, to: tree.to - 2 }; },
322 JSXElement(tree) {
323 let open = tree.firstChild;
324 if (!open || open.name == "JSXSelfClosingTag")
325 return null;
326 let close = tree.lastChild;
327 return { from: open.to, to: close.type.isError ? tree.to : close.from };
328 },
329 "JSXSelfClosingTag JSXOpenTag"(tree) {
330 var _a;
331 let name = (_a = tree.firstChild) === null || _a === void 0 ? void 0 : _a.nextSibling, close = tree.lastChild;
332 if (!name || name.type.isError)
333 return null;
334 return { from: name.to, to: close.type.isError ? tree.to : close.from };
335 }
336 })
337 ]
338 }),
339 languageData: {
340 closeBrackets: { brackets: ["(", "[", "{", "'", '"', "`"] },
341 commentTokens: { line: "//", block: { open: "/*", close: "*/" } },
342 indentOnInput: /^\s*(?:case |default:|\{|\}|<\/)$/,
343 wordChars: "$"
344 }
345});
346const jsxSublanguage = {
347 test: node => /^JSX/.test(node.name),
348 facet: /*@__PURE__*/defineLanguageFacet({ commentTokens: { block: { open: "{/*", close: "*/}" } } })
349};
350/**
351A language provider for TypeScript.
352*/
353const typescriptLanguage = /*@__PURE__*/javascriptLanguage.configure({ dialect: "ts" }, "typescript");
354/**
355Language provider for JSX.
356*/
357const jsxLanguage = /*@__PURE__*/javascriptLanguage.configure({
358 dialect: "jsx",
359 props: [/*@__PURE__*/sublanguageProp.add(n => n.isTop ? [jsxSublanguage] : undefined)]
360});
361/**
362Language provider for JSX + TypeScript.
363*/
364const tsxLanguage = /*@__PURE__*/javascriptLanguage.configure({
365 dialect: "jsx ts",
366 props: [/*@__PURE__*/sublanguageProp.add(n => n.isTop ? [jsxSublanguage] : undefined)]
367}, "typescript");
368let kwCompletion = (name) => ({ label: name, type: "keyword" });
369const keywords = /*@__PURE__*/"break case const continue default delete export extends false finally in instanceof let new return static super switch this throw true typeof var yield".split(" ").map(kwCompletion);
370const typescriptKeywords = /*@__PURE__*/keywords.concat(/*@__PURE__*/["declare", "implements", "private", "protected", "public"].map(kwCompletion));
371/**
372JavaScript support. Includes [snippet](https://codemirror.net/6/docs/ref/#lang-javascript.snippets)
373and local variable completion.
374*/
375function javascript(config = {}) {
376 let lang = config.jsx ? (config.typescript ? tsxLanguage : jsxLanguage)
377 : config.typescript ? typescriptLanguage : javascriptLanguage;
378 let completions = config.typescript ? typescriptSnippets.concat(typescriptKeywords) : snippets.concat(keywords);
379 return new LanguageSupport(lang, [
380 javascriptLanguage.data.of({
381 autocomplete: ifNotIn(dontComplete, completeFromList(completions))
382 }),
383 javascriptLanguage.data.of({
384 autocomplete: localCompletionSource
385 }),
386 config.jsx ? autoCloseTags : [],
387 ]);
388}
389function findOpenTag(node) {
390 for (;;) {
391 if (node.name == "JSXOpenTag" || node.name == "JSXSelfClosingTag" || node.name == "JSXFragmentTag")
392 return node;
393 if (node.name == "JSXEscape" || !node.parent)
394 return null;
395 node = node.parent;
396 }
397}
398function elementName(doc, tree, max = doc.length) {
399 for (let ch = tree === null || tree === void 0 ? void 0 : tree.firstChild; ch; ch = ch.nextSibling) {
400 if (ch.name == "JSXIdentifier" || ch.name == "JSXBuiltin" || ch.name == "JSXNamespacedName" ||
401 ch.name == "JSXMemberExpression")
402 return doc.sliceString(ch.from, Math.min(ch.to, max));
403 }
404 return "";
405}
406const android = typeof navigator == "object" && /*@__PURE__*//Android\b/.test(navigator.userAgent);
407/**
408Extension that will automatically insert JSX close tags when a `>` or
409`/` is typed.
410*/
411const autoCloseTags = /*@__PURE__*/EditorView.inputHandler.of((view, from, to, text, defaultInsert) => {
412 if ((android ? view.composing : view.compositionStarted) || view.state.readOnly ||
413 from != to || (text != ">" && text != "/") ||
414 !javascriptLanguage.isActiveAt(view.state, from, -1))
415 return false;
416 let base = defaultInsert(), { state } = base;
417 let closeTags = state.changeByRange(range => {
418 var _a;
419 let { head } = range, around = syntaxTree(state).resolveInner(head - 1, -1), name;
420 if (around.name == "JSXStartTag")
421 around = around.parent;
422 if (state.doc.sliceString(head - 1, head) != text || around.name == "JSXAttributeValue" && around.to > head) ;
423 else if (text == ">" && around.name == "JSXFragmentTag") {
424 return { range, changes: { from: head, insert: `</>` } };
425 }
426 else if (text == "/" && around.name == "JSXStartCloseTag") {
427 let empty = around.parent, base = empty.parent;
428 if (base && empty.from == head - 2 &&
429 ((name = elementName(state.doc, base.firstChild, head)) || ((_a = base.firstChild) === null || _a === void 0 ? void 0 : _a.name) == "JSXFragmentTag")) {
430 let insert = `${name}>`;
431 return { range: EditorSelection.cursor(head + insert.length, -1), changes: { from: head, insert } };
432 }
433 }
434 else if (text == ">") {
435 let openTag = findOpenTag(around);
436 if (openTag && openTag.name == "JSXOpenTag" &&
437 !/^\/?>|^<\//.test(state.doc.sliceString(head, head + 2)) &&
438 (name = elementName(state.doc, openTag, head)))
439 return { range, changes: { from: head, insert: `</${name}>` } };
440 }
441 return { range };
442 });
443 if (closeTags.changes.empty)
444 return false;
445 view.dispatch([
446 base,
447 state.update(closeTags, { userEvent: "input.complete", scrollIntoView: true })
448 ]);
449 return true;
450});
451
452/**
453Connects an [ESLint](https://eslint.org/) linter to CodeMirror's
454[lint](https://codemirror.net/6/docs/ref/#lint) integration. `eslint` should be an instance of the
455[`Linter`](https://eslint.org/docs/developer-guide/nodejs-api#linter)
456class, and `config` an optional ESLint configuration. The return
457value of this function can be passed to [`linter`](https://codemirror.net/6/docs/ref/#lint.linter)
458to create a JavaScript linting extension.
459
460Note that ESLint targets node, and is tricky to run in the
461browser. The
462[eslint-linter-browserify](https://github.com/UziTech/eslint-linter-browserify)
463package may help with that (see
464[example](https://github.com/UziTech/eslint-linter-browserify/blob/master/example/script.js)).
465*/
466function esLint(eslint, config) {
467 if (!config) {
468 config = {
469 parserOptions: { ecmaVersion: 2019, sourceType: "module" },
470 env: { browser: true, node: true, es6: true, es2015: true, es2017: true, es2020: true },
471 rules: {}
472 };
473 eslint.getRules().forEach((desc, name) => {
474 var _a;
475 if ((_a = desc.meta.docs) === null || _a === void 0 ? void 0 : _a.recommended)
476 config.rules[name] = 2;
477 });
478 }
479 return (view) => {
480 let { state } = view, found = [];
481 for (let { from, to } of javascriptLanguage.findRegions(state)) {
482 let fromLine = state.doc.lineAt(from), offset = { line: fromLine.number - 1, col: from - fromLine.from, pos: from };
483 for (let d of eslint.verify(state.sliceDoc(from, to), config))
484 found.push(translateDiagnostic(d, state.doc, offset));
485 }
486 return found;
487 };
488}
489function mapPos(line, col, doc, offset) {
490 return doc.line(line + offset.line).from + col + (line == 1 ? offset.col - 1 : -1);
491}
492function translateDiagnostic(input, doc, offset) {
493 let start = mapPos(input.line, input.column, doc, offset);
494 let result = {
495 from: start,
496 to: input.endLine != null && input.endColumn != 1 ? mapPos(input.endLine, input.endColumn, doc, offset) : start,
497 message: input.message,
498 source: input.ruleId ? "eslint:" + input.ruleId : "eslint",
499 severity: input.severity == 1 ? "warning" : "error",
500 };
501 if (input.fix) {
502 let { range, text } = input.fix, from = range[0] + offset.pos - start, to = range[1] + offset.pos - start;
503 result.actions = [{
504 name: "fix",
505 apply(view, start) {
506 view.dispatch({ changes: { from: start + from, to: start + to, insert: text }, scrollIntoView: true });
507 }
508 }];
509 }
510 return result;
511}
512
513export { autoCloseTags, completionPath, esLint, javascript, javascriptLanguage, jsxLanguage, localCompletionSource, scopeCompletionSource, snippets, tsxLanguage, typescriptLanguage, typescriptSnippets };