|
import { parser } from '@lezer/javascript'; |
|
import { syntaxTree, LRLanguage, indentNodeProp, continuedIndent, flatIndent, delimitedIndent, foldNodeProp, foldInside, defineLanguageFacet, sublanguageProp, LanguageSupport } from '@codemirror/language'; |
|
import { EditorSelection } from '@codemirror/state'; |
|
import { EditorView } from '@codemirror/view'; |
|
import { snippetCompletion, ifNotIn, completeFromList } from '@codemirror/autocomplete'; |
|
import { NodeWeakMap, IterMode } from '@lezer/common'; |
|
|
|
|
|
|
|
|
|
|
|
const snippets = [ |
|
snippetCompletion("function ${name}(${params}) {\n\t${}\n}", { |
|
label: "function", |
|
detail: "definition", |
|
type: "keyword" |
|
}), |
|
snippetCompletion("for (let ${index} = 0; ${index} < ${bound}; ${index}++) {\n\t${}\n}", { |
|
label: "for", |
|
detail: "loop", |
|
type: "keyword" |
|
}), |
|
snippetCompletion("for (let ${name} of ${collection}) {\n\t${}\n}", { |
|
label: "for", |
|
detail: "of loop", |
|
type: "keyword" |
|
}), |
|
snippetCompletion("do {\n\t${}\n} while (${})", { |
|
label: "do", |
|
detail: "loop", |
|
type: "keyword" |
|
}), |
|
snippetCompletion("while (${}) {\n\t${}\n}", { |
|
label: "while", |
|
detail: "loop", |
|
type: "keyword" |
|
}), |
|
snippetCompletion("try {\n\t${}\n} catch (${error}) {\n\t${}\n}", { |
|
label: "try", |
|
detail: "/ catch block", |
|
type: "keyword" |
|
}), |
|
snippetCompletion("if (${}) {\n\t${}\n}", { |
|
label: "if", |
|
detail: "block", |
|
type: "keyword" |
|
}), |
|
snippetCompletion("if (${}) {\n\t${}\n} else {\n\t${}\n}", { |
|
label: "if", |
|
detail: "/ else block", |
|
type: "keyword" |
|
}), |
|
snippetCompletion("class ${name} {\n\tconstructor(${params}) {\n\t\t${}\n\t}\n}", { |
|
label: "class", |
|
detail: "definition", |
|
type: "keyword" |
|
}), |
|
snippetCompletion("import {${names}} from \"${module}\"\n${}", { |
|
label: "import", |
|
detail: "named", |
|
type: "keyword" |
|
}), |
|
snippetCompletion("import ${name} from \"${module}\"\n${}", { |
|
label: "import", |
|
detail: "default", |
|
type: "keyword" |
|
}) |
|
]; |
|
|
|
|
|
|
|
|
|
const typescriptSnippets = snippets.concat([ |
|
snippetCompletion("interface ${name} {\n\t${}\n}", { |
|
label: "interface", |
|
detail: "definition", |
|
type: "keyword" |
|
}), |
|
snippetCompletion("type ${name} = ${type}", { |
|
label: "type", |
|
detail: "definition", |
|
type: "keyword" |
|
}), |
|
snippetCompletion("enum ${name} {\n\t${}\n}", { |
|
label: "enum", |
|
detail: "definition", |
|
type: "keyword" |
|
}) |
|
]); |
|
|
|
const cache = new NodeWeakMap(); |
|
const ScopeNodes = new Set([ |
|
"Script", "Block", |
|
"FunctionExpression", "FunctionDeclaration", "ArrowFunction", "MethodDeclaration", |
|
"ForStatement" |
|
]); |
|
function defID(type) { |
|
return (node, def) => { |
|
let id = node.node.getChild("VariableDefinition"); |
|
if (id) |
|
def(id, type); |
|
return true; |
|
}; |
|
} |
|
const functionContext = ["FunctionDeclaration"]; |
|
const gatherCompletions = { |
|
FunctionDeclaration: defID("function"), |
|
ClassDeclaration: defID("class"), |
|
ClassExpression: () => true, |
|
EnumDeclaration: defID("constant"), |
|
TypeAliasDeclaration: defID("type"), |
|
NamespaceDeclaration: defID("namespace"), |
|
VariableDefinition(node, def) { if (!node.matchContext(functionContext)) |
|
def(node, "variable"); }, |
|
TypeDefinition(node, def) { def(node, "type"); }, |
|
__proto__: null |
|
}; |
|
function getScope(doc, node) { |
|
let cached = cache.get(node); |
|
if (cached) |
|
return cached; |
|
let completions = [], top = true; |
|
function def(node, type) { |
|
let name = doc.sliceString(node.from, node.to); |
|
completions.push({ label: name, type }); |
|
} |
|
node.cursor(IterMode.IncludeAnonymous).iterate(node => { |
|
if (top) { |
|
top = false; |
|
} |
|
else if (node.name) { |
|
let gather = gatherCompletions[node.name]; |
|
if (gather && gather(node, def) || ScopeNodes.has(node.name)) |
|
return false; |
|
} |
|
else if (node.to - node.from > 8192) { |
|
|
|
for (let c of getScope(doc, node.node)) |
|
completions.push(c); |
|
return false; |
|
} |
|
}); |
|
cache.set(node, completions); |
|
return completions; |
|
} |
|
const Identifier = /^[\w$\xa1-\uffff][\w$\d\xa1-\uffff]*$/; |
|
const dontComplete = [ |
|
"TemplateString", "String", "RegExp", |
|
"LineComment", "BlockComment", |
|
"VariableDefinition", "TypeDefinition", "Label", |
|
"PropertyDefinition", "PropertyName", |
|
"PrivatePropertyDefinition", "PrivatePropertyName", |
|
".", "?." |
|
]; |
|
|
|
|
|
|
|
|
|
function localCompletionSource(context) { |
|
let inner = syntaxTree(context.state).resolveInner(context.pos, -1); |
|
if (dontComplete.indexOf(inner.name) > -1) |
|
return null; |
|
let isWord = inner.name == "VariableName" || |
|
inner.to - inner.from < 20 && Identifier.test(context.state.sliceDoc(inner.from, inner.to)); |
|
if (!isWord && !context.explicit) |
|
return null; |
|
let options = []; |
|
for (let pos = inner; pos; pos = pos.parent) { |
|
if (ScopeNodes.has(pos.name)) |
|
options = options.concat(getScope(context.state.doc, pos)); |
|
} |
|
return { |
|
options, |
|
from: isWord ? inner.from : context.pos, |
|
validFor: Identifier |
|
}; |
|
} |
|
function pathFor(read, member, name) { |
|
var _a; |
|
let path = []; |
|
for (;;) { |
|
let obj = member.firstChild, prop; |
|
if ((obj === null || obj === void 0 ? void 0 : obj.name) == "VariableName") { |
|
path.push(read(obj)); |
|
return { path: path.reverse(), name }; |
|
} |
|
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") { |
|
path.push(read(prop)); |
|
member = obj; |
|
} |
|
else { |
|
return null; |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function completionPath(context) { |
|
let read = (node) => context.state.doc.sliceString(node.from, node.to); |
|
let inner = syntaxTree(context.state).resolveInner(context.pos, -1); |
|
if (inner.name == "PropertyName") { |
|
return pathFor(read, inner.parent, read(inner)); |
|
} |
|
else if ((inner.name == "." || inner.name == "?.") && inner.parent.name == "MemberExpression") { |
|
return pathFor(read, inner.parent, ""); |
|
} |
|
else if (dontComplete.indexOf(inner.name) > -1) { |
|
return null; |
|
} |
|
else if (inner.name == "VariableName" || inner.to - inner.from < 20 && Identifier.test(read(inner))) { |
|
return { path: [], name: read(inner) }; |
|
} |
|
else if (inner.name == "MemberExpression") { |
|
return pathFor(read, inner, ""); |
|
} |
|
else { |
|
return context.explicit ? { path: [], name: "" } : null; |
|
} |
|
} |
|
function enumeratePropertyCompletions(obj, top) { |
|
let options = [], seen = new Set; |
|
for (let depth = 0;; depth++) { |
|
for (let name of (Object.getOwnPropertyNames || Object.keys)(obj)) { |
|
if (!/^[a-zA-Z_$\xaa-\uffdc][\w$\xaa-\uffdc]*$/.test(name) || seen.has(name)) |
|
continue; |
|
seen.add(name); |
|
let value; |
|
try { |
|
value = obj[name]; |
|
} |
|
catch (_) { |
|
continue; |
|
} |
|
options.push({ |
|
label: name, |
|
type: typeof value == "function" ? (/^[A-Z]/.test(name) ? "class" : top ? "function" : "method") |
|
: top ? "variable" : "property", |
|
boost: -depth |
|
}); |
|
} |
|
let next = Object.getPrototypeOf(obj); |
|
if (!next) |
|
return options; |
|
obj = next; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function scopeCompletionSource(scope) { |
|
let cache = new Map; |
|
return (context) => { |
|
let path = completionPath(context); |
|
if (!path) |
|
return null; |
|
let target = scope; |
|
for (let step of path.path) { |
|
target = target[step]; |
|
if (!target) |
|
return null; |
|
} |
|
let options = cache.get(target); |
|
if (!options) |
|
cache.set(target, options = enumeratePropertyCompletions(target, !path.path.length)); |
|
return { |
|
from: context.pos - path.name.length, |
|
options, |
|
validFor: Identifier |
|
}; |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
const javascriptLanguage = LRLanguage.define({ |
|
name: "javascript", |
|
parser: parser.configure({ |
|
props: [ |
|
indentNodeProp.add({ |
|
IfStatement: continuedIndent({ except: /^\s*({|else\b)/ }), |
|
TryStatement: continuedIndent({ except: /^\s*({|catch\b|finally\b)/ }), |
|
LabeledStatement: flatIndent, |
|
SwitchBody: context => { |
|
let after = context.textAfter, closed = /^\s*\}/.test(after), isCase = /^\s*(case|default)\b/.test(after); |
|
return context.baseIndent + (closed ? 0 : isCase ? 1 : 2) * context.unit; |
|
}, |
|
Block: delimitedIndent({ closing: "}" }), |
|
ArrowFunction: cx => cx.baseIndent + cx.unit, |
|
"TemplateString BlockComment": () => null, |
|
"Statement Property": continuedIndent({ except: /^{/ }), |
|
JSXElement(context) { |
|
let closed = /^\s*<\//.test(context.textAfter); |
|
return context.lineIndent(context.node.from) + (closed ? 0 : context.unit); |
|
}, |
|
JSXEscape(context) { |
|
let closed = /\s*\}/.test(context.textAfter); |
|
return context.lineIndent(context.node.from) + (closed ? 0 : context.unit); |
|
}, |
|
"JSXOpenTag JSXSelfClosingTag"(context) { |
|
return context.column(context.node.from) + context.unit; |
|
} |
|
}), |
|
foldNodeProp.add({ |
|
"Block ClassBody SwitchBody EnumBody ObjectExpression ArrayExpression ObjectType": foldInside, |
|
BlockComment(tree) { return { from: tree.from + 2, to: tree.to - 2 }; } |
|
}) |
|
] |
|
}), |
|
languageData: { |
|
closeBrackets: { brackets: ["(", "[", "{", "'", '"', "`"] }, |
|
commentTokens: { line: "//", block: { open: "/*", close: "*/" } }, |
|
indentOnInput: /^\s*(?:case |default:|\{|\}|<\/)$/, |
|
wordChars: "$" |
|
} |
|
}); |
|
const jsxSublanguage = { |
|
test: node => /^JSX/.test(node.name), |
|
facet: defineLanguageFacet({ commentTokens: { block: { open: "{/*", close: "*/}" } } }) |
|
}; |
|
|
|
|
|
|
|
const typescriptLanguage = javascriptLanguage.configure({ dialect: "ts" }, "typescript"); |
|
|
|
|
|
|
|
const jsxLanguage = javascriptLanguage.configure({ |
|
dialect: "jsx", |
|
props: [sublanguageProp.add(n => n.isTop ? [jsxSublanguage] : undefined)] |
|
}); |
|
|
|
|
|
|
|
const tsxLanguage = javascriptLanguage.configure({ |
|
dialect: "jsx ts", |
|
props: [sublanguageProp.add(n => n.isTop ? [jsxSublanguage] : undefined)] |
|
}, "typescript"); |
|
let kwCompletion = (name) => ({ label: name, type: "keyword" }); |
|
const keywords = "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); |
|
const typescriptKeywords = keywords.concat(["declare", "implements", "private", "protected", "public"].map(kwCompletion)); |
|
|
|
|
|
|
|
|
|
function javascript(config = {}) { |
|
let lang = config.jsx ? (config.typescript ? tsxLanguage : jsxLanguage) |
|
: config.typescript ? typescriptLanguage : javascriptLanguage; |
|
let completions = config.typescript ? typescriptSnippets.concat(typescriptKeywords) : snippets.concat(keywords); |
|
return new LanguageSupport(lang, [ |
|
javascriptLanguage.data.of({ |
|
autocomplete: ifNotIn(dontComplete, completeFromList(completions)) |
|
}), |
|
javascriptLanguage.data.of({ |
|
autocomplete: localCompletionSource |
|
}), |
|
config.jsx ? autoCloseTags : [], |
|
]); |
|
} |
|
function findOpenTag(node) { |
|
for (;;) { |
|
if (node.name == "JSXOpenTag" || node.name == "JSXSelfClosingTag" || node.name == "JSXFragmentTag") |
|
return node; |
|
if (node.name == "JSXEscape" || !node.parent) |
|
return null; |
|
node = node.parent; |
|
} |
|
} |
|
function elementName(doc, tree, max = doc.length) { |
|
for (let ch = tree === null || tree === void 0 ? void 0 : tree.firstChild; ch; ch = ch.nextSibling) { |
|
if (ch.name == "JSXIdentifier" || ch.name == "JSXBuiltin" || ch.name == "JSXNamespacedName" || |
|
ch.name == "JSXMemberExpression") |
|
return doc.sliceString(ch.from, Math.min(ch.to, max)); |
|
} |
|
return ""; |
|
} |
|
const android = typeof navigator == "object" && /Android\b/.test(navigator.userAgent); |
|
|
|
|
|
|
|
|
|
const autoCloseTags = EditorView.inputHandler.of((view, from, to, text, defaultInsert) => { |
|
if ((android ? view.composing : view.compositionStarted) || view.state.readOnly || |
|
from != to || (text != ">" && text != "/") || |
|
!javascriptLanguage.isActiveAt(view.state, from, -1)) |
|
return false; |
|
let base = defaultInsert(), { state } = base; |
|
let closeTags = state.changeByRange(range => { |
|
var _a; |
|
let { head } = range, around = syntaxTree(state).resolveInner(head - 1, -1), name; |
|
if (around.name == "JSXStartTag") |
|
around = around.parent; |
|
if (state.doc.sliceString(head - 1, head) != text || around.name == "JSXAttributeValue" && around.to > head) ; |
|
else if (text == ">" && around.name == "JSXFragmentTag") { |
|
return { range, changes: { from: head, insert: `</>` } }; |
|
} |
|
else if (text == "/" && around.name == "JSXStartCloseTag") { |
|
let empty = around.parent, base = empty.parent; |
|
if (base && empty.from == head - 2 && |
|
((name = elementName(state.doc, base.firstChild, head)) || ((_a = base.firstChild) === null || _a === void 0 ? void 0 : _a.name) == "JSXFragmentTag")) { |
|
let insert = `${name}>`; |
|
return { range: EditorSelection.cursor(head + insert.length, -1), changes: { from: head, insert } }; |
|
} |
|
} |
|
else if (text == ">") { |
|
let openTag = findOpenTag(around); |
|
if (openTag && openTag.name == "JSXOpenTag" && |
|
!/^\/?>|^<\//.test(state.doc.sliceString(head, head + 2)) && |
|
(name = elementName(state.doc, openTag, head))) |
|
return { range, changes: { from: head, insert: `</${name}>` } }; |
|
} |
|
return { range }; |
|
}); |
|
if (closeTags.changes.empty) |
|
return false; |
|
view.dispatch([ |
|
base, |
|
state.update(closeTags, { userEvent: "input.complete", scrollIntoView: true }) |
|
]); |
|
return true; |
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function esLint(eslint, config) { |
|
if (!config) { |
|
config = { |
|
parserOptions: { ecmaVersion: 2019, sourceType: "module" }, |
|
env: { browser: true, node: true, es6: true, es2015: true, es2017: true, es2020: true }, |
|
rules: {} |
|
}; |
|
eslint.getRules().forEach((desc, name) => { |
|
if (desc.meta.docs.recommended) |
|
config.rules[name] = 2; |
|
}); |
|
} |
|
return (view) => { |
|
let { state } = view, found = []; |
|
for (let { from, to } of javascriptLanguage.findRegions(state)) { |
|
let fromLine = state.doc.lineAt(from), offset = { line: fromLine.number - 1, col: from - fromLine.from, pos: from }; |
|
for (let d of eslint.verify(state.sliceDoc(from, to), config)) |
|
found.push(translateDiagnostic(d, state.doc, offset)); |
|
} |
|
return found; |
|
}; |
|
} |
|
function mapPos(line, col, doc, offset) { |
|
return doc.line(line + offset.line).from + col + (line == 1 ? offset.col - 1 : -1); |
|
} |
|
function translateDiagnostic(input, doc, offset) { |
|
let start = mapPos(input.line, input.column, doc, offset); |
|
let result = { |
|
from: start, |
|
to: input.endLine != null && input.endColumn != 1 ? mapPos(input.endLine, input.endColumn, doc, offset) : start, |
|
message: input.message, |
|
source: input.ruleId ? "eslint:" + input.ruleId : "eslint", |
|
severity: input.severity == 1 ? "warning" : "error", |
|
}; |
|
if (input.fix) { |
|
let { range, text } = input.fix, from = range[0] + offset.pos - start, to = range[1] + offset.pos - start; |
|
result.actions = [{ |
|
name: "fix", |
|
apply(view, start) { |
|
view.dispatch({ changes: { from: start + from, to: start + to, insert: text }, scrollIntoView: true }); |
|
} |
|
}]; |
|
} |
|
return result; |
|
} |
|
|
|
export { autoCloseTags, completionPath, esLint, javascript, javascriptLanguage, jsxLanguage, localCompletionSource, scopeCompletionSource, snippets, tsxLanguage, typescriptLanguage, typescriptSnippets }; |
|
|