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 };