|
import { NodeProp, IterMode, Tree, TreeFragment, Parser, NodeType, NodeSet } from '@lezer/common'; |
|
import { StateEffect, StateField, Facet, EditorState, countColumn, combineConfig, RangeSet, RangeSetBuilder, Prec } from '@codemirror/state'; |
|
import { ViewPlugin, logException, EditorView, Decoration, WidgetType, gutter, GutterMarker, Direction } from '@codemirror/view'; |
|
import { tags, tagHighlighter, highlightTree, styleTags } from '@lezer/highlight'; |
|
import { StyleModule } from 'style-mod'; |
|
|
|
var _a; |
|
|
|
|
|
|
|
|
|
const languageDataProp = new NodeProp(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function defineLanguageFacet(baseData) { |
|
return Facet.define({ |
|
combine: baseData ? values => values.concat(baseData) : undefined |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
const sublanguageProp = new NodeProp(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Language { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor( |
|
|
|
|
|
|
|
|
|
data, parser, extraExtensions = [], |
|
|
|
|
|
|
|
name = "") { |
|
this.data = data; |
|
this.name = name; |
|
|
|
|
|
|
|
if (!EditorState.prototype.hasOwnProperty("tree")) |
|
Object.defineProperty(EditorState.prototype, "tree", { get() { return syntaxTree(this); } }); |
|
this.parser = parser; |
|
this.extension = [ |
|
language.of(this), |
|
EditorState.languageData.of((state, pos, side) => { |
|
let top = topNodeAt(state, pos, side), data = top.type.prop(languageDataProp); |
|
if (!data) |
|
return []; |
|
let base = state.facet(data), sub = top.type.prop(sublanguageProp); |
|
if (sub) { |
|
let innerNode = top.resolve(pos - top.from, side); |
|
for (let sublang of sub) |
|
if (sublang.test(innerNode, state)) { |
|
let data = state.facet(sublang.facet); |
|
return sublang.type == "replace" ? data : data.concat(base); |
|
} |
|
} |
|
return base; |
|
}) |
|
].concat(extraExtensions); |
|
} |
|
|
|
|
|
|
|
isActiveAt(state, pos, side = -1) { |
|
return topNodeAt(state, pos, side).type.prop(languageDataProp) == this.data; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
findRegions(state) { |
|
let lang = state.facet(language); |
|
if ((lang === null || lang === void 0 ? void 0 : lang.data) == this.data) |
|
return [{ from: 0, to: state.doc.length }]; |
|
if (!lang || !lang.allowsNesting) |
|
return []; |
|
let result = []; |
|
let explore = (tree, from) => { |
|
if (tree.prop(languageDataProp) == this.data) { |
|
result.push({ from, to: from + tree.length }); |
|
return; |
|
} |
|
let mount = tree.prop(NodeProp.mounted); |
|
if (mount) { |
|
if (mount.tree.prop(languageDataProp) == this.data) { |
|
if (mount.overlay) |
|
for (let r of mount.overlay) |
|
result.push({ from: r.from + from, to: r.to + from }); |
|
else |
|
result.push({ from: from, to: from + tree.length }); |
|
return; |
|
} |
|
else if (mount.overlay) { |
|
let size = result.length; |
|
explore(mount.tree, mount.overlay[0].from + from); |
|
if (result.length > size) |
|
return; |
|
} |
|
} |
|
for (let i = 0; i < tree.children.length; i++) { |
|
let ch = tree.children[i]; |
|
if (ch instanceof Tree) |
|
explore(ch, tree.positions[i] + from); |
|
} |
|
}; |
|
explore(syntaxTree(state), 0); |
|
return result; |
|
} |
|
|
|
|
|
|
|
|
|
get allowsNesting() { return true; } |
|
} |
|
|
|
|
|
|
|
Language.setState = StateEffect.define(); |
|
function topNodeAt(state, pos, side) { |
|
let topLang = state.facet(language), tree = syntaxTree(state).topNode; |
|
if (!topLang || topLang.allowsNesting) { |
|
for (let node = tree; node; node = node.enter(pos, side, IterMode.ExcludeBuffers)) |
|
if (node.type.isTop) |
|
tree = node; |
|
} |
|
return tree; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
class LRLanguage extends Language { |
|
constructor(data, parser, name) { |
|
super(data, parser, [], name); |
|
this.parser = parser; |
|
} |
|
|
|
|
|
|
|
static define(spec) { |
|
let data = defineLanguageFacet(spec.languageData); |
|
return new LRLanguage(data, spec.parser.configure({ |
|
props: [languageDataProp.add(type => type.isTop ? data : undefined)] |
|
}), spec.name); |
|
} |
|
|
|
|
|
|
|
|
|
configure(options, name) { |
|
return new LRLanguage(this.data, this.parser.configure(options), name || this.name); |
|
} |
|
get allowsNesting() { return this.parser.hasWrappers(); } |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function syntaxTree(state) { |
|
let field = state.field(Language.state, false); |
|
return field ? field.tree : Tree.empty; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function ensureSyntaxTree(state, upto, timeout = 50) { |
|
var _a; |
|
let parse = (_a = state.field(Language.state, false)) === null || _a === void 0 ? void 0 : _a.context; |
|
if (!parse) |
|
return null; |
|
let oldVieport = parse.viewport; |
|
parse.updateViewport({ from: 0, to: upto }); |
|
let result = parse.isDone(upto) || parse.work(timeout, upto) ? parse.tree : null; |
|
parse.updateViewport(oldVieport); |
|
return result; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function syntaxTreeAvailable(state, upto = state.doc.length) { |
|
var _a; |
|
return ((_a = state.field(Language.state, false)) === null || _a === void 0 ? void 0 : _a.context.isDone(upto)) || false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function forceParsing(view, upto = view.viewport.to, timeout = 100) { |
|
let success = ensureSyntaxTree(view.state, upto, timeout); |
|
if (success != syntaxTree(view.state)) |
|
view.dispatch({}); |
|
return !!success; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function syntaxParserRunning(view) { |
|
var _a; |
|
return ((_a = view.plugin(parseWorker)) === null || _a === void 0 ? void 0 : _a.isWorking()) || false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
class DocInput { |
|
|
|
|
|
|
|
constructor(doc) { |
|
this.doc = doc; |
|
this.cursorPos = 0; |
|
this.string = ""; |
|
this.cursor = doc.iter(); |
|
} |
|
get length() { return this.doc.length; } |
|
syncTo(pos) { |
|
this.string = this.cursor.next(pos - this.cursorPos).value; |
|
this.cursorPos = pos + this.string.length; |
|
return this.cursorPos - this.string.length; |
|
} |
|
chunk(pos) { |
|
this.syncTo(pos); |
|
return this.string; |
|
} |
|
get lineChunks() { return true; } |
|
read(from, to) { |
|
let stringStart = this.cursorPos - this.string.length; |
|
if (from < stringStart || to >= this.cursorPos) |
|
return this.doc.sliceString(from, to); |
|
else |
|
return this.string.slice(from - stringStart, to - stringStart); |
|
} |
|
} |
|
let currentContext = null; |
|
|
|
|
|
|
|
class ParseContext { |
|
constructor(parser, |
|
|
|
|
|
|
|
state, |
|
|
|
|
|
|
|
fragments = [], |
|
|
|
|
|
|
|
tree, |
|
|
|
|
|
|
|
treeLen, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
viewport, |
|
|
|
|
|
|
|
skipped, |
|
|
|
|
|
|
|
|
|
|
|
scheduleOn) { |
|
this.parser = parser; |
|
this.state = state; |
|
this.fragments = fragments; |
|
this.tree = tree; |
|
this.treeLen = treeLen; |
|
this.viewport = viewport; |
|
this.skipped = skipped; |
|
this.scheduleOn = scheduleOn; |
|
this.parse = null; |
|
|
|
|
|
|
|
this.tempSkipped = []; |
|
} |
|
|
|
|
|
|
|
static create(parser, state, viewport) { |
|
return new ParseContext(parser, state, [], Tree.empty, 0, viewport, [], null); |
|
} |
|
startParse() { |
|
return this.parser.startParse(new DocInput(this.state.doc), this.fragments); |
|
} |
|
|
|
|
|
|
|
work(until, upto) { |
|
if (upto != null && upto >= this.state.doc.length) |
|
upto = undefined; |
|
if (this.tree != Tree.empty && this.isDone(upto !== null && upto !== void 0 ? upto : this.state.doc.length)) { |
|
this.takeTree(); |
|
return true; |
|
} |
|
return this.withContext(() => { |
|
var _a; |
|
if (typeof until == "number") { |
|
let endTime = Date.now() + until; |
|
until = () => Date.now() > endTime; |
|
} |
|
if (!this.parse) |
|
this.parse = this.startParse(); |
|
if (upto != null && (this.parse.stoppedAt == null || this.parse.stoppedAt > upto) && |
|
upto < this.state.doc.length) |
|
this.parse.stopAt(upto); |
|
for (;;) { |
|
let done = this.parse.advance(); |
|
if (done) { |
|
this.fragments = this.withoutTempSkipped(TreeFragment.addTree(done, this.fragments, this.parse.stoppedAt != null)); |
|
this.treeLen = (_a = this.parse.stoppedAt) !== null && _a !== void 0 ? _a : this.state.doc.length; |
|
this.tree = done; |
|
this.parse = null; |
|
if (this.treeLen < (upto !== null && upto !== void 0 ? upto : this.state.doc.length)) |
|
this.parse = this.startParse(); |
|
else |
|
return true; |
|
} |
|
if (until()) |
|
return false; |
|
} |
|
}); |
|
} |
|
|
|
|
|
|
|
takeTree() { |
|
let pos, tree; |
|
if (this.parse && (pos = this.parse.parsedPos) >= this.treeLen) { |
|
if (this.parse.stoppedAt == null || this.parse.stoppedAt > pos) |
|
this.parse.stopAt(pos); |
|
this.withContext(() => { while (!(tree = this.parse.advance())) { } }); |
|
this.treeLen = pos; |
|
this.tree = tree; |
|
this.fragments = this.withoutTempSkipped(TreeFragment.addTree(this.tree, this.fragments, true)); |
|
this.parse = null; |
|
} |
|
} |
|
withContext(f) { |
|
let prev = currentContext; |
|
currentContext = this; |
|
try { |
|
return f(); |
|
} |
|
finally { |
|
currentContext = prev; |
|
} |
|
} |
|
withoutTempSkipped(fragments) { |
|
for (let r; r = this.tempSkipped.pop();) |
|
fragments = cutFragments(fragments, r.from, r.to); |
|
return fragments; |
|
} |
|
|
|
|
|
|
|
changes(changes, newState) { |
|
let { fragments, tree, treeLen, viewport, skipped } = this; |
|
this.takeTree(); |
|
if (!changes.empty) { |
|
let ranges = []; |
|
changes.iterChangedRanges((fromA, toA, fromB, toB) => ranges.push({ fromA, toA, fromB, toB })); |
|
fragments = TreeFragment.applyChanges(fragments, ranges); |
|
tree = Tree.empty; |
|
treeLen = 0; |
|
viewport = { from: changes.mapPos(viewport.from, -1), to: changes.mapPos(viewport.to, 1) }; |
|
if (this.skipped.length) { |
|
skipped = []; |
|
for (let r of this.skipped) { |
|
let from = changes.mapPos(r.from, 1), to = changes.mapPos(r.to, -1); |
|
if (from < to) |
|
skipped.push({ from, to }); |
|
} |
|
} |
|
} |
|
return new ParseContext(this.parser, newState, fragments, tree, treeLen, viewport, skipped, this.scheduleOn); |
|
} |
|
|
|
|
|
|
|
updateViewport(viewport) { |
|
if (this.viewport.from == viewport.from && this.viewport.to == viewport.to) |
|
return false; |
|
this.viewport = viewport; |
|
let startLen = this.skipped.length; |
|
for (let i = 0; i < this.skipped.length; i++) { |
|
let { from, to } = this.skipped[i]; |
|
if (from < viewport.to && to > viewport.from) { |
|
this.fragments = cutFragments(this.fragments, from, to); |
|
this.skipped.splice(i--, 1); |
|
} |
|
} |
|
if (this.skipped.length >= startLen) |
|
return false; |
|
this.reset(); |
|
return true; |
|
} |
|
|
|
|
|
|
|
reset() { |
|
if (this.parse) { |
|
this.takeTree(); |
|
this.parse = null; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
skipUntilInView(from, to) { |
|
this.skipped.push({ from, to }); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static getSkippingParser(until) { |
|
return new class extends Parser { |
|
createParse(input, fragments, ranges) { |
|
let from = ranges[0].from, to = ranges[ranges.length - 1].to; |
|
let parser = { |
|
parsedPos: from, |
|
advance() { |
|
let cx = currentContext; |
|
if (cx) { |
|
for (let r of ranges) |
|
cx.tempSkipped.push(r); |
|
if (until) |
|
cx.scheduleOn = cx.scheduleOn ? Promise.all([cx.scheduleOn, until]) : until; |
|
} |
|
this.parsedPos = to; |
|
return new Tree(NodeType.none, [], [], to - from); |
|
}, |
|
stoppedAt: null, |
|
stopAt() { } |
|
}; |
|
return parser; |
|
} |
|
}; |
|
} |
|
|
|
|
|
|
|
isDone(upto) { |
|
upto = Math.min(upto, this.state.doc.length); |
|
let frags = this.fragments; |
|
return this.treeLen >= upto && frags.length && frags[0].from == 0 && frags[0].to >= upto; |
|
} |
|
|
|
|
|
|
|
|
|
static get() { return currentContext; } |
|
} |
|
function cutFragments(fragments, from, to) { |
|
return TreeFragment.applyChanges(fragments, [{ fromA: from, toA: to, fromB: from, toB: to }]); |
|
} |
|
class LanguageState { |
|
constructor( |
|
|
|
|
|
context) { |
|
this.context = context; |
|
this.tree = context.tree; |
|
} |
|
apply(tr) { |
|
if (!tr.docChanged && this.tree == this.context.tree) |
|
return this; |
|
let newCx = this.context.changes(tr.changes, tr.state); |
|
|
|
|
|
|
|
let upto = this.context.treeLen == tr.startState.doc.length ? undefined |
|
: Math.max(tr.changes.mapPos(this.context.treeLen), newCx.viewport.to); |
|
if (!newCx.work(20 , upto)) |
|
newCx.takeTree(); |
|
return new LanguageState(newCx); |
|
} |
|
static init(state) { |
|
let vpTo = Math.min(3000 , state.doc.length); |
|
let parseState = ParseContext.create(state.facet(language).parser, state, { from: 0, to: vpTo }); |
|
if (!parseState.work(20 , vpTo)) |
|
parseState.takeTree(); |
|
return new LanguageState(parseState); |
|
} |
|
} |
|
Language.state = StateField.define({ |
|
create: LanguageState.init, |
|
update(value, tr) { |
|
for (let e of tr.effects) |
|
if (e.is(Language.setState)) |
|
return e.value; |
|
if (tr.startState.facet(language) != tr.state.facet(language)) |
|
return LanguageState.init(tr.state); |
|
return value.apply(tr); |
|
} |
|
}); |
|
let requestIdle = (callback) => { |
|
let timeout = setTimeout(() => callback(), 500 ); |
|
return () => clearTimeout(timeout); |
|
}; |
|
if (typeof requestIdleCallback != "undefined") |
|
requestIdle = (callback) => { |
|
let idle = -1, timeout = setTimeout(() => { |
|
idle = requestIdleCallback(callback, { timeout: 500 - 100 }); |
|
}, 100 ); |
|
return () => idle < 0 ? clearTimeout(timeout) : cancelIdleCallback(idle); |
|
}; |
|
const isInputPending = typeof navigator != "undefined" && ((_a = navigator.scheduling) === null || _a === void 0 ? void 0 : _a.isInputPending) |
|
? () => navigator.scheduling.isInputPending() : null; |
|
const parseWorker = ViewPlugin.fromClass(class ParseWorker { |
|
constructor(view) { |
|
this.view = view; |
|
this.working = null; |
|
this.workScheduled = 0; |
|
|
|
this.chunkEnd = -1; |
|
|
|
this.chunkBudget = -1; |
|
this.work = this.work.bind(this); |
|
this.scheduleWork(); |
|
} |
|
update(update) { |
|
let cx = this.view.state.field(Language.state).context; |
|
if (cx.updateViewport(update.view.viewport) || this.view.viewport.to > cx.treeLen) |
|
this.scheduleWork(); |
|
if (update.docChanged || update.selectionSet) { |
|
if (this.view.hasFocus) |
|
this.chunkBudget += 50 ; |
|
this.scheduleWork(); |
|
} |
|
this.checkAsyncSchedule(cx); |
|
} |
|
scheduleWork() { |
|
if (this.working) |
|
return; |
|
let { state } = this.view, field = state.field(Language.state); |
|
if (field.tree != field.context.tree || !field.context.isDone(state.doc.length)) |
|
this.working = requestIdle(this.work); |
|
} |
|
work(deadline) { |
|
this.working = null; |
|
let now = Date.now(); |
|
if (this.chunkEnd < now && (this.chunkEnd < 0 || this.view.hasFocus)) { |
|
this.chunkEnd = now + 30000 ; |
|
this.chunkBudget = 3000 ; |
|
} |
|
if (this.chunkBudget <= 0) |
|
return; |
|
let { state, viewport: { to: vpTo } } = this.view, field = state.field(Language.state); |
|
if (field.tree == field.context.tree && field.context.isDone(vpTo + 100000 )) |
|
return; |
|
let endTime = Date.now() + Math.min(this.chunkBudget, 100 , deadline && !isInputPending ? Math.max(25 , deadline.timeRemaining() - 5) : 1e9); |
|
let viewportFirst = field.context.treeLen < vpTo && state.doc.length > vpTo + 1000; |
|
let done = field.context.work(() => { |
|
return isInputPending && isInputPending() || Date.now() > endTime; |
|
}, vpTo + (viewportFirst ? 0 : 100000 )); |
|
this.chunkBudget -= Date.now() - now; |
|
if (done || this.chunkBudget <= 0) { |
|
field.context.takeTree(); |
|
this.view.dispatch({ effects: Language.setState.of(new LanguageState(field.context)) }); |
|
} |
|
if (this.chunkBudget > 0 && !(done && !viewportFirst)) |
|
this.scheduleWork(); |
|
this.checkAsyncSchedule(field.context); |
|
} |
|
checkAsyncSchedule(cx) { |
|
if (cx.scheduleOn) { |
|
this.workScheduled++; |
|
cx.scheduleOn |
|
.then(() => this.scheduleWork()) |
|
.catch(err => logException(this.view.state, err)) |
|
.then(() => this.workScheduled--); |
|
cx.scheduleOn = null; |
|
} |
|
} |
|
destroy() { |
|
if (this.working) |
|
this.working(); |
|
} |
|
isWorking() { |
|
return !!(this.working || this.workScheduled > 0); |
|
} |
|
}, { |
|
eventHandlers: { focus() { this.scheduleWork(); } } |
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
const language = Facet.define({ |
|
combine(languages) { return languages.length ? languages[0] : null; }, |
|
enables: language => [ |
|
Language.state, |
|
parseWorker, |
|
EditorView.contentAttributes.compute([language], state => { |
|
let lang = state.facet(language); |
|
return lang && lang.name ? { "data-language": lang.name } : {}; |
|
}) |
|
] |
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LanguageSupport { |
|
|
|
|
|
|
|
constructor( |
|
|
|
|
|
|
|
language, |
|
|
|
|
|
|
|
|
|
|
|
|
|
support = []) { |
|
this.language = language; |
|
this.support = support; |
|
this.extension = [language, support]; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
class LanguageDescription { |
|
constructor( |
|
|
|
|
|
|
|
name, |
|
|
|
|
|
|
|
alias, |
|
|
|
|
|
|
|
extensions, |
|
|
|
|
|
|
|
|
|
filename, loadFunc, |
|
|
|
|
|
|
|
support = undefined) { |
|
this.name = name; |
|
this.alias = alias; |
|
this.extensions = extensions; |
|
this.filename = filename; |
|
this.loadFunc = loadFunc; |
|
this.support = support; |
|
this.loading = null; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
load() { |
|
return this.loading || (this.loading = this.loadFunc().then(support => this.support = support, err => { this.loading = null; throw err; })); |
|
} |
|
|
|
|
|
|
|
static of(spec) { |
|
let { load, support } = spec; |
|
if (!load) { |
|
if (!support) |
|
throw new RangeError("Must pass either 'load' or 'support' to LanguageDescription.of"); |
|
load = () => Promise.resolve(support); |
|
} |
|
return new LanguageDescription(spec.name, (spec.alias || []).concat(spec.name).map(s => s.toLowerCase()), spec.extensions || [], spec.filename, load, support); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static matchFilename(descs, filename) { |
|
for (let d of descs) |
|
if (d.filename && d.filename.test(filename)) |
|
return d; |
|
let ext = /\.([^.]+)$/.exec(filename); |
|
if (ext) |
|
for (let d of descs) |
|
if (d.extensions.indexOf(ext[1]) > -1) |
|
return d; |
|
return null; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static matchLanguageName(descs, name, fuzzy = true) { |
|
name = name.toLowerCase(); |
|
for (let d of descs) |
|
if (d.alias.some(a => a == name)) |
|
return d; |
|
if (fuzzy) |
|
for (let d of descs) |
|
for (let a of d.alias) { |
|
let found = name.indexOf(a); |
|
if (found > -1 && (a.length > 2 || !/\w/.test(name[found - 1]) && !/\w/.test(name[found + a.length]))) |
|
return d; |
|
} |
|
return null; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const indentService = Facet.define(); |
|
|
|
|
|
|
|
|
|
|
|
const indentUnit = Facet.define({ |
|
combine: values => { |
|
if (!values.length) |
|
return " "; |
|
let unit = values[0]; |
|
if (!unit || /\S/.test(unit) || Array.from(unit).some(e => e != unit[0])) |
|
throw new Error("Invalid indent unit: " + JSON.stringify(values[0])); |
|
return unit; |
|
} |
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
function getIndentUnit(state) { |
|
let unit = state.facet(indentUnit); |
|
return unit.charCodeAt(0) == 9 ? state.tabSize * unit.length : unit.length; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function indentString(state, cols) { |
|
let result = "", ts = state.tabSize, ch = state.facet(indentUnit)[0]; |
|
if (ch == "\t") { |
|
while (cols >= ts) { |
|
result += "\t"; |
|
cols -= ts; |
|
} |
|
ch = " "; |
|
} |
|
for (let i = 0; i < cols; i++) |
|
result += ch; |
|
return result; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getIndentation(context, pos) { |
|
if (context instanceof EditorState) |
|
context = new IndentContext(context); |
|
for (let service of context.state.facet(indentService)) { |
|
let result = service(context, pos); |
|
if (result !== undefined) |
|
return result; |
|
} |
|
let tree = syntaxTree(context.state); |
|
return tree.length >= pos ? syntaxIndentation(context, tree, pos) : null; |
|
} |
|
|
|
|
|
|
|
|
|
function indentRange(state, from, to) { |
|
let updated = Object.create(null); |
|
let context = new IndentContext(state, { overrideIndentation: start => { var _a; return (_a = updated[start]) !== null && _a !== void 0 ? _a : -1; } }); |
|
let changes = []; |
|
for (let pos = from; pos <= to;) { |
|
let line = state.doc.lineAt(pos); |
|
pos = line.to + 1; |
|
let indent = getIndentation(context, line.from); |
|
if (indent == null) |
|
continue; |
|
if (!/\S/.test(line.text)) |
|
indent = 0; |
|
let cur = /^\s*/.exec(line.text)[0]; |
|
let norm = indentString(state, indent); |
|
if (cur != norm) { |
|
updated[line.from] = indent; |
|
changes.push({ from: line.from, to: line.from + cur.length, insert: norm }); |
|
} |
|
} |
|
return state.changes(changes); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
class IndentContext { |
|
|
|
|
|
|
|
constructor( |
|
|
|
|
|
|
|
state, |
|
|
|
|
|
|
|
options = {}) { |
|
this.state = state; |
|
this.options = options; |
|
this.unit = getIndentUnit(state); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lineAt(pos, bias = 1) { |
|
let line = this.state.doc.lineAt(pos); |
|
let { simulateBreak, simulateDoubleBreak } = this.options; |
|
if (simulateBreak != null && simulateBreak >= line.from && simulateBreak <= line.to) { |
|
if (simulateDoubleBreak && simulateBreak == pos) |
|
return { text: "", from: pos }; |
|
else if (bias < 0 ? simulateBreak < pos : simulateBreak <= pos) |
|
return { text: line.text.slice(simulateBreak - line.from), from: simulateBreak }; |
|
else |
|
return { text: line.text.slice(0, simulateBreak - line.from), from: line.from }; |
|
} |
|
return line; |
|
} |
|
|
|
|
|
|
|
|
|
textAfterPos(pos, bias = 1) { |
|
if (this.options.simulateDoubleBreak && pos == this.options.simulateBreak) |
|
return ""; |
|
let { text, from } = this.lineAt(pos, bias); |
|
return text.slice(pos - from, Math.min(text.length, pos + 100 - from)); |
|
} |
|
|
|
|
|
|
|
column(pos, bias = 1) { |
|
let { text, from } = this.lineAt(pos, bias); |
|
let result = this.countColumn(text, pos - from); |
|
let override = this.options.overrideIndentation ? this.options.overrideIndentation(from) : -1; |
|
if (override > -1) |
|
result += override - this.countColumn(text, text.search(/\S|$/)); |
|
return result; |
|
} |
|
|
|
|
|
|
|
|
|
countColumn(line, pos = line.length) { |
|
return countColumn(line, this.state.tabSize, pos); |
|
} |
|
|
|
|
|
|
|
lineIndent(pos, bias = 1) { |
|
let { text, from } = this.lineAt(pos, bias); |
|
let override = this.options.overrideIndentation; |
|
if (override) { |
|
let overriden = override(from); |
|
if (overriden > -1) |
|
return overriden; |
|
} |
|
return this.countColumn(text, text.search(/\S|$/)); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
get simulatedBreak() { |
|
return this.options.simulateBreak || null; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const indentNodeProp = new NodeProp(); |
|
|
|
function syntaxIndentation(cx, ast, pos) { |
|
let stack = ast.resolveStack(pos); |
|
let inner = stack.node.enterUnfinishedNodesBefore(pos); |
|
if (inner != stack.node) { |
|
let add = []; |
|
for (let cur = inner; cur != stack.node; cur = cur.parent) |
|
add.push(cur); |
|
for (let i = add.length - 1; i >= 0; i--) |
|
stack = { node: add[i], next: stack }; |
|
} |
|
return indentFor(stack, cx, pos); |
|
} |
|
function indentFor(stack, cx, pos) { |
|
for (let cur = stack; cur; cur = cur.next) { |
|
let strategy = indentStrategy(cur.node); |
|
if (strategy) |
|
return strategy(TreeIndentContext.create(cx, pos, cur)); |
|
} |
|
return 0; |
|
} |
|
function ignoreClosed(cx) { |
|
return cx.pos == cx.options.simulateBreak && cx.options.simulateDoubleBreak; |
|
} |
|
function indentStrategy(tree) { |
|
let strategy = tree.type.prop(indentNodeProp); |
|
if (strategy) |
|
return strategy; |
|
let first = tree.firstChild, close; |
|
if (first && (close = first.type.prop(NodeProp.closedBy))) { |
|
let last = tree.lastChild, closed = last && close.indexOf(last.name) > -1; |
|
return cx => delimitedStrategy(cx, true, 1, undefined, closed && !ignoreClosed(cx) ? last.from : undefined); |
|
} |
|
return tree.parent == null ? topIndent : null; |
|
} |
|
function topIndent() { return 0; } |
|
|
|
|
|
|
|
|
|
class TreeIndentContext extends IndentContext { |
|
constructor(base, |
|
|
|
|
|
|
|
pos, |
|
|
|
|
|
|
|
context) { |
|
super(base.state, base.options); |
|
this.base = base; |
|
this.pos = pos; |
|
this.context = context; |
|
} |
|
|
|
|
|
|
|
|
|
get node() { return this.context.node; } |
|
|
|
|
|
|
|
static create(base, pos, context) { |
|
return new TreeIndentContext(base, pos, context); |
|
} |
|
|
|
|
|
|
|
|
|
get textAfter() { |
|
return this.textAfterPos(this.pos); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get baseIndent() { |
|
return this.baseIndentFor(this.node); |
|
} |
|
|
|
|
|
|
|
|
|
baseIndentFor(node) { |
|
let line = this.state.doc.lineAt(node.from); |
|
|
|
for (;;) { |
|
let atBreak = node.resolve(line.from); |
|
while (atBreak.parent && atBreak.parent.from == atBreak.from) |
|
atBreak = atBreak.parent; |
|
if (isParent(atBreak, node)) |
|
break; |
|
line = this.state.doc.lineAt(atBreak.from); |
|
} |
|
return this.lineIndent(line.from); |
|
} |
|
|
|
|
|
|
|
|
|
continue() { |
|
return indentFor(this.context.next, this.base, this.pos); |
|
} |
|
} |
|
function isParent(parent, of) { |
|
for (let cur = of; cur; cur = cur.parent) |
|
if (parent == cur) |
|
return true; |
|
return false; |
|
} |
|
|
|
|
|
|
|
function bracketedAligned(context) { |
|
let tree = context.node; |
|
let openToken = tree.childAfter(tree.from), last = tree.lastChild; |
|
if (!openToken) |
|
return null; |
|
let sim = context.options.simulateBreak; |
|
let openLine = context.state.doc.lineAt(openToken.from); |
|
let lineEnd = sim == null || sim <= openLine.from ? openLine.to : Math.min(openLine.to, sim); |
|
for (let pos = openToken.to;;) { |
|
let next = tree.childAfter(pos); |
|
if (!next || next == last) |
|
return null; |
|
if (!next.type.isSkipped) |
|
return next.from < lineEnd ? openToken : null; |
|
pos = next.to; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function delimitedIndent({ closing, align = true, units = 1 }) { |
|
return (context) => delimitedStrategy(context, align, units, closing); |
|
} |
|
function delimitedStrategy(context, align, units, closing, closedAt) { |
|
let after = context.textAfter, space = after.match(/^\s*/)[0].length; |
|
let closed = closing && after.slice(space, space + closing.length) == closing || closedAt == context.pos + space; |
|
let aligned = align ? bracketedAligned(context) : null; |
|
if (aligned) |
|
return closed ? context.column(aligned.from) : context.column(aligned.to); |
|
return context.baseIndent + (closed ? 0 : context.unit * units); |
|
} |
|
|
|
|
|
|
|
|
|
const flatIndent = (context) => context.baseIndent; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function continuedIndent({ except, units = 1 } = {}) { |
|
return (context) => { |
|
let matchExcept = except && except.test(context.textAfter); |
|
return context.baseIndent + (matchExcept ? 0 : units * context.unit); |
|
}; |
|
} |
|
const DontIndentBeyond = 200; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function indentOnInput() { |
|
return EditorState.transactionFilter.of(tr => { |
|
if (!tr.docChanged || !tr.isUserEvent("input.type") && !tr.isUserEvent("input.complete")) |
|
return tr; |
|
let rules = tr.startState.languageDataAt("indentOnInput", tr.startState.selection.main.head); |
|
if (!rules.length) |
|
return tr; |
|
let doc = tr.newDoc, { head } = tr.newSelection.main, line = doc.lineAt(head); |
|
if (head > line.from + DontIndentBeyond) |
|
return tr; |
|
let lineStart = doc.sliceString(line.from, head); |
|
if (!rules.some(r => r.test(lineStart))) |
|
return tr; |
|
let { state } = tr, last = -1, changes = []; |
|
for (let { head } of state.selection.ranges) { |
|
let line = state.doc.lineAt(head); |
|
if (line.from == last) |
|
continue; |
|
last = line.from; |
|
let indent = getIndentation(state, line.from); |
|
if (indent == null) |
|
continue; |
|
let cur = /^\s*/.exec(line.text)[0]; |
|
let norm = indentString(state, indent); |
|
if (cur != norm) |
|
changes.push({ from: line.from, to: line.from + cur.length, insert: norm }); |
|
} |
|
return changes.length ? [tr, { changes, sequential: true }] : tr; |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const foldService = Facet.define(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
const foldNodeProp = new NodeProp(); |
|
|
|
|
|
|
|
|
|
|
|
function foldInside(node) { |
|
let first = node.firstChild, last = node.lastChild; |
|
return first && first.to < last.from ? { from: first.to, to: last.type.isError ? node.to : last.from } : null; |
|
} |
|
function syntaxFolding(state, start, end) { |
|
let tree = syntaxTree(state); |
|
if (tree.length < end) |
|
return null; |
|
let stack = tree.resolveStack(end, 1); |
|
let found = null; |
|
for (let iter = stack; iter; iter = iter.next) { |
|
let cur = iter.node; |
|
if (cur.to <= end || cur.from > end) |
|
continue; |
|
if (found && cur.from < start) |
|
break; |
|
let prop = cur.type.prop(foldNodeProp); |
|
if (prop && (cur.to < tree.length - 50 || tree.length == state.doc.length || !isUnfinished(cur))) { |
|
let value = prop(cur, state); |
|
if (value && value.from <= end && value.from >= start && value.to > end) |
|
found = value; |
|
} |
|
} |
|
return found; |
|
} |
|
function isUnfinished(node) { |
|
let ch = node.lastChild; |
|
return ch && ch.to == node.to && ch.type.isError; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function foldable(state, lineStart, lineEnd) { |
|
for (let service of state.facet(foldService)) { |
|
let result = service(state, lineStart, lineEnd); |
|
if (result) |
|
return result; |
|
} |
|
return syntaxFolding(state, lineStart, lineEnd); |
|
} |
|
function mapRange(range, mapping) { |
|
let from = mapping.mapPos(range.from, 1), to = mapping.mapPos(range.to, -1); |
|
return from >= to ? undefined : { from, to }; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const foldEffect = StateEffect.define({ map: mapRange }); |
|
|
|
|
|
|
|
const unfoldEffect = StateEffect.define({ map: mapRange }); |
|
function selectedLines(view) { |
|
let lines = []; |
|
for (let { head } of view.state.selection.ranges) { |
|
if (lines.some(l => l.from <= head && l.to >= head)) |
|
continue; |
|
lines.push(view.lineBlockAt(head)); |
|
} |
|
return lines; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const foldState = StateField.define({ |
|
create() { |
|
return Decoration.none; |
|
}, |
|
update(folded, tr) { |
|
folded = folded.map(tr.changes); |
|
for (let e of tr.effects) { |
|
if (e.is(foldEffect) && !foldExists(folded, e.value.from, e.value.to)) { |
|
let { preparePlaceholder } = tr.state.facet(foldConfig); |
|
let widget = !preparePlaceholder ? foldWidget : |
|
Decoration.replace({ widget: new PreparedFoldWidget(preparePlaceholder(tr.state, e.value)) }); |
|
folded = folded.update({ add: [widget.range(e.value.from, e.value.to)] }); |
|
} |
|
else if (e.is(unfoldEffect)) { |
|
folded = folded.update({ filter: (from, to) => e.value.from != from || e.value.to != to, |
|
filterFrom: e.value.from, filterTo: e.value.to }); |
|
} |
|
} |
|
|
|
if (tr.selection) { |
|
let onSelection = false, { head } = tr.selection.main; |
|
folded.between(head, head, (a, b) => { if (a < head && b > head) |
|
onSelection = true; }); |
|
if (onSelection) |
|
folded = folded.update({ |
|
filterFrom: head, |
|
filterTo: head, |
|
filter: (a, b) => b <= head || a >= head |
|
}); |
|
} |
|
return folded; |
|
}, |
|
provide: f => EditorView.decorations.from(f), |
|
toJSON(folded, state) { |
|
let ranges = []; |
|
folded.between(0, state.doc.length, (from, to) => { ranges.push(from, to); }); |
|
return ranges; |
|
}, |
|
fromJSON(value) { |
|
if (!Array.isArray(value) || value.length % 2) |
|
throw new RangeError("Invalid JSON for fold state"); |
|
let ranges = []; |
|
for (let i = 0; i < value.length;) { |
|
let from = value[i++], to = value[i++]; |
|
if (typeof from != "number" || typeof to != "number") |
|
throw new RangeError("Invalid JSON for fold state"); |
|
ranges.push(foldWidget.range(from, to)); |
|
} |
|
return Decoration.set(ranges, true); |
|
} |
|
}); |
|
|
|
|
|
|
|
|
|
function foldedRanges(state) { |
|
return state.field(foldState, false) || RangeSet.empty; |
|
} |
|
function findFold(state, from, to) { |
|
var _a; |
|
let found = null; |
|
(_a = state.field(foldState, false)) === null || _a === void 0 ? void 0 : _a.between(from, to, (from, to) => { |
|
if (!found || found.from > from) |
|
found = { from, to }; |
|
}); |
|
return found; |
|
} |
|
function foldExists(folded, from, to) { |
|
let found = false; |
|
folded.between(from, from, (a, b) => { if (a == from && b == to) |
|
found = true; }); |
|
return found; |
|
} |
|
function maybeEnable(state, other) { |
|
return state.field(foldState, false) ? other : other.concat(StateEffect.appendConfig.of(codeFolding())); |
|
} |
|
|
|
|
|
|
|
const foldCode = view => { |
|
for (let line of selectedLines(view)) { |
|
let range = foldable(view.state, line.from, line.to); |
|
if (range) { |
|
view.dispatch({ effects: maybeEnable(view.state, [foldEffect.of(range), announceFold(view, range)]) }); |
|
return true; |
|
} |
|
} |
|
return false; |
|
}; |
|
|
|
|
|
|
|
const unfoldCode = view => { |
|
if (!view.state.field(foldState, false)) |
|
return false; |
|
let effects = []; |
|
for (let line of selectedLines(view)) { |
|
let folded = findFold(view.state, line.from, line.to); |
|
if (folded) |
|
effects.push(unfoldEffect.of(folded), announceFold(view, folded, false)); |
|
} |
|
if (effects.length) |
|
view.dispatch({ effects }); |
|
return effects.length > 0; |
|
}; |
|
function announceFold(view, range, fold = true) { |
|
let lineFrom = view.state.doc.lineAt(range.from).number, lineTo = view.state.doc.lineAt(range.to).number; |
|
return EditorView.announce.of(`${view.state.phrase(fold ? "Folded lines" : "Unfolded lines")} ${lineFrom} ${view.state.phrase("to")} ${lineTo}.`); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const foldAll = view => { |
|
let { state } = view, effects = []; |
|
for (let pos = 0; pos < state.doc.length;) { |
|
let line = view.lineBlockAt(pos), range = foldable(state, line.from, line.to); |
|
if (range) |
|
effects.push(foldEffect.of(range)); |
|
pos = (range ? view.lineBlockAt(range.to) : line).to + 1; |
|
} |
|
if (effects.length) |
|
view.dispatch({ effects: maybeEnable(view.state, effects) }); |
|
return !!effects.length; |
|
}; |
|
|
|
|
|
|
|
const unfoldAll = view => { |
|
let field = view.state.field(foldState, false); |
|
if (!field || !field.size) |
|
return false; |
|
let effects = []; |
|
field.between(0, view.state.doc.length, (from, to) => { effects.push(unfoldEffect.of({ from, to })); }); |
|
view.dispatch({ effects }); |
|
return true; |
|
}; |
|
|
|
function foldableContainer(view, lineBlock) { |
|
|
|
|
|
for (let line = lineBlock;;) { |
|
let foldableRegion = foldable(view.state, line.from, line.to); |
|
if (foldableRegion && foldableRegion.to > lineBlock.from) |
|
return foldableRegion; |
|
if (!line.from) |
|
return null; |
|
line = view.lineBlockAt(line.from - 1); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
const toggleFold = (view) => { |
|
let effects = []; |
|
for (let line of selectedLines(view)) { |
|
let folded = findFold(view.state, line.from, line.to); |
|
if (folded) { |
|
effects.push(unfoldEffect.of(folded), announceFold(view, folded, false)); |
|
} |
|
else { |
|
let foldRange = foldableContainer(view, line); |
|
if (foldRange) |
|
effects.push(foldEffect.of(foldRange), announceFold(view, foldRange)); |
|
} |
|
} |
|
if (effects.length > 0) |
|
view.dispatch({ effects: maybeEnable(view.state, effects) }); |
|
return !!effects.length; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const foldKeymap = [ |
|
{ key: "Ctrl-Shift-[", mac: "Cmd-Alt-[", run: foldCode }, |
|
{ key: "Ctrl-Shift-]", mac: "Cmd-Alt-]", run: unfoldCode }, |
|
{ key: "Ctrl-Alt-[", run: foldAll }, |
|
{ key: "Ctrl-Alt-]", run: unfoldAll } |
|
]; |
|
const defaultConfig = { |
|
placeholderDOM: null, |
|
preparePlaceholder: null, |
|
placeholderText: "…" |
|
}; |
|
const foldConfig = Facet.define({ |
|
combine(values) { return combineConfig(values, defaultConfig); } |
|
}); |
|
|
|
|
|
|
|
function codeFolding(config) { |
|
let result = [foldState, baseTheme$1]; |
|
if (config) |
|
result.push(foldConfig.of(config)); |
|
return result; |
|
} |
|
function widgetToDOM(view, prepared) { |
|
let { state } = view, conf = state.facet(foldConfig); |
|
let onclick = (event) => { |
|
let line = view.lineBlockAt(view.posAtDOM(event.target)); |
|
let folded = findFold(view.state, line.from, line.to); |
|
if (folded) |
|
view.dispatch({ effects: unfoldEffect.of(folded) }); |
|
event.preventDefault(); |
|
}; |
|
if (conf.placeholderDOM) |
|
return conf.placeholderDOM(view, onclick, prepared); |
|
let element = document.createElement("span"); |
|
element.textContent = conf.placeholderText; |
|
element.setAttribute("aria-label", state.phrase("folded code")); |
|
element.title = state.phrase("unfold"); |
|
element.className = "cm-foldPlaceholder"; |
|
element.onclick = onclick; |
|
return element; |
|
} |
|
const foldWidget = Decoration.replace({ widget: new class extends WidgetType { |
|
toDOM(view) { return widgetToDOM(view, null); } |
|
} }); |
|
class PreparedFoldWidget extends WidgetType { |
|
constructor(value) { |
|
super(); |
|
this.value = value; |
|
} |
|
eq(other) { return this.value == other.value; } |
|
toDOM(view) { return widgetToDOM(view, this.value); } |
|
} |
|
const foldGutterDefaults = { |
|
openText: "⌄", |
|
closedText: "›", |
|
markerDOM: null, |
|
domEventHandlers: {}, |
|
foldingChanged: () => false |
|
}; |
|
class FoldMarker extends GutterMarker { |
|
constructor(config, open) { |
|
super(); |
|
this.config = config; |
|
this.open = open; |
|
} |
|
eq(other) { return this.config == other.config && this.open == other.open; } |
|
toDOM(view) { |
|
if (this.config.markerDOM) |
|
return this.config.markerDOM(this.open); |
|
let span = document.createElement("span"); |
|
span.textContent = this.open ? this.config.openText : this.config.closedText; |
|
span.title = view.state.phrase(this.open ? "Fold line" : "Unfold line"); |
|
return span; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function foldGutter(config = {}) { |
|
let fullConfig = Object.assign(Object.assign({}, foldGutterDefaults), config); |
|
let canFold = new FoldMarker(fullConfig, true), canUnfold = new FoldMarker(fullConfig, false); |
|
let markers = ViewPlugin.fromClass(class { |
|
constructor(view) { |
|
this.from = view.viewport.from; |
|
this.markers = this.buildMarkers(view); |
|
} |
|
update(update) { |
|
if (update.docChanged || update.viewportChanged || |
|
update.startState.facet(language) != update.state.facet(language) || |
|
update.startState.field(foldState, false) != update.state.field(foldState, false) || |
|
syntaxTree(update.startState) != syntaxTree(update.state) || |
|
fullConfig.foldingChanged(update)) |
|
this.markers = this.buildMarkers(update.view); |
|
} |
|
buildMarkers(view) { |
|
let builder = new RangeSetBuilder(); |
|
for (let line of view.viewportLineBlocks) { |
|
let mark = findFold(view.state, line.from, line.to) ? canUnfold |
|
: foldable(view.state, line.from, line.to) ? canFold : null; |
|
if (mark) |
|
builder.add(line.from, line.from, mark); |
|
} |
|
return builder.finish(); |
|
} |
|
}); |
|
let { domEventHandlers } = fullConfig; |
|
return [ |
|
markers, |
|
gutter({ |
|
class: "cm-foldGutter", |
|
markers(view) { var _a; return ((_a = view.plugin(markers)) === null || _a === void 0 ? void 0 : _a.markers) || RangeSet.empty; }, |
|
initialSpacer() { |
|
return new FoldMarker(fullConfig, false); |
|
}, |
|
domEventHandlers: Object.assign(Object.assign({}, domEventHandlers), { click: (view, line, event) => { |
|
if (domEventHandlers.click && domEventHandlers.click(view, line, event)) |
|
return true; |
|
let folded = findFold(view.state, line.from, line.to); |
|
if (folded) { |
|
view.dispatch({ effects: unfoldEffect.of(folded) }); |
|
return true; |
|
} |
|
let range = foldable(view.state, line.from, line.to); |
|
if (range) { |
|
view.dispatch({ effects: foldEffect.of(range) }); |
|
return true; |
|
} |
|
return false; |
|
} }) |
|
}), |
|
codeFolding() |
|
]; |
|
} |
|
const baseTheme$1 = EditorView.baseTheme({ |
|
".cm-foldPlaceholder": { |
|
backgroundColor: "#eee", |
|
border: "1px solid #ddd", |
|
color: "#888", |
|
borderRadius: ".2em", |
|
margin: "0 1px", |
|
padding: "0 1px", |
|
cursor: "pointer" |
|
}, |
|
".cm-foldGutter span": { |
|
padding: "0 1px", |
|
cursor: "pointer" |
|
} |
|
}); |
|
|
|
|
|
|
|
|
|
|
|
class HighlightStyle { |
|
constructor( |
|
|
|
|
|
|
|
specs, options) { |
|
this.specs = specs; |
|
let modSpec; |
|
function def(spec) { |
|
let cls = StyleModule.newName(); |
|
(modSpec || (modSpec = Object.create(null)))["." + cls] = spec; |
|
return cls; |
|
} |
|
const all = typeof options.all == "string" ? options.all : options.all ? def(options.all) : undefined; |
|
const scopeOpt = options.scope; |
|
this.scope = scopeOpt instanceof Language ? (type) => type.prop(languageDataProp) == scopeOpt.data |
|
: scopeOpt ? (type) => type == scopeOpt : undefined; |
|
this.style = tagHighlighter(specs.map(style => ({ |
|
tag: style.tag, |
|
class: style.class || def(Object.assign({}, style, { tag: null })) |
|
})), { |
|
all, |
|
}).style; |
|
this.module = modSpec ? new StyleModule(modSpec) : null; |
|
this.themeType = options.themeType; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static define(specs, options) { |
|
return new HighlightStyle(specs, options || {}); |
|
} |
|
} |
|
const highlighterFacet = Facet.define(); |
|
const fallbackHighlighter = Facet.define({ |
|
combine(values) { return values.length ? [values[0]] : null; } |
|
}); |
|
function getHighlighters(state) { |
|
let main = state.facet(highlighterFacet); |
|
return main.length ? main : state.facet(fallbackHighlighter); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function syntaxHighlighting(highlighter, options) { |
|
let ext = [treeHighlighter], themeType; |
|
if (highlighter instanceof HighlightStyle) { |
|
if (highlighter.module) |
|
ext.push(EditorView.styleModule.of(highlighter.module)); |
|
themeType = highlighter.themeType; |
|
} |
|
if (options === null || options === void 0 ? void 0 : options.fallback) |
|
ext.push(fallbackHighlighter.of(highlighter)); |
|
else if (themeType) |
|
ext.push(highlighterFacet.computeN([EditorView.darkTheme], state => { |
|
return state.facet(EditorView.darkTheme) == (themeType == "dark") ? [highlighter] : []; |
|
})); |
|
else |
|
ext.push(highlighterFacet.of(highlighter)); |
|
return ext; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function highlightingFor(state, tags, scope) { |
|
let highlighters = getHighlighters(state); |
|
let result = null; |
|
if (highlighters) |
|
for (let highlighter of highlighters) { |
|
if (!highlighter.scope || scope && highlighter.scope(scope)) { |
|
let cls = highlighter.style(tags); |
|
if (cls) |
|
result = result ? result + " " + cls : cls; |
|
} |
|
} |
|
return result; |
|
} |
|
class TreeHighlighter { |
|
constructor(view) { |
|
this.markCache = Object.create(null); |
|
this.tree = syntaxTree(view.state); |
|
this.decorations = this.buildDeco(view, getHighlighters(view.state)); |
|
this.decoratedTo = view.viewport.to; |
|
} |
|
update(update) { |
|
let tree = syntaxTree(update.state), highlighters = getHighlighters(update.state); |
|
let styleChange = highlighters != getHighlighters(update.startState); |
|
let { viewport } = update.view, decoratedToMapped = update.changes.mapPos(this.decoratedTo, 1); |
|
if (tree.length < viewport.to && !styleChange && tree.type == this.tree.type && decoratedToMapped >= viewport.to) { |
|
this.decorations = this.decorations.map(update.changes); |
|
this.decoratedTo = decoratedToMapped; |
|
} |
|
else if (tree != this.tree || update.viewportChanged || styleChange) { |
|
this.tree = tree; |
|
this.decorations = this.buildDeco(update.view, highlighters); |
|
this.decoratedTo = viewport.to; |
|
} |
|
} |
|
buildDeco(view, highlighters) { |
|
if (!highlighters || !this.tree.length) |
|
return Decoration.none; |
|
let builder = new RangeSetBuilder(); |
|
for (let { from, to } of view.visibleRanges) { |
|
highlightTree(this.tree, highlighters, (from, to, style) => { |
|
builder.add(from, to, this.markCache[style] || (this.markCache[style] = Decoration.mark({ class: style }))); |
|
}, from, to); |
|
} |
|
return builder.finish(); |
|
} |
|
} |
|
const treeHighlighter = Prec.high(ViewPlugin.fromClass(TreeHighlighter, { |
|
decorations: v => v.decorations |
|
})); |
|
|
|
|
|
|
|
const defaultHighlightStyle = HighlightStyle.define([ |
|
{ tag: tags.meta, |
|
color: "#404740" }, |
|
{ tag: tags.link, |
|
textDecoration: "underline" }, |
|
{ tag: tags.heading, |
|
textDecoration: "underline", |
|
fontWeight: "bold" }, |
|
{ tag: tags.emphasis, |
|
fontStyle: "italic" }, |
|
{ tag: tags.strong, |
|
fontWeight: "bold" }, |
|
{ tag: tags.strikethrough, |
|
textDecoration: "line-through" }, |
|
{ tag: tags.keyword, |
|
color: "#708" }, |
|
{ tag: [tags.atom, tags.bool, tags.url, tags.contentSeparator, tags.labelName], |
|
color: "#219" }, |
|
{ tag: [tags.literal, tags.inserted], |
|
color: "#164" }, |
|
{ tag: [tags.string, tags.deleted], |
|
color: "#a11" }, |
|
{ tag: [tags.regexp, tags.escape, tags.special(tags.string)], |
|
color: "#e40" }, |
|
{ tag: tags.definition(tags.variableName), |
|
color: "#00f" }, |
|
{ tag: tags.local(tags.variableName), |
|
color: "#30a" }, |
|
{ tag: [tags.typeName, tags.namespace], |
|
color: "#085" }, |
|
{ tag: tags.className, |
|
color: "#167" }, |
|
{ tag: [tags.special(tags.variableName), tags.macroName], |
|
color: "#256" }, |
|
{ tag: tags.definition(tags.propertyName), |
|
color: "#00c" }, |
|
{ tag: tags.comment, |
|
color: "#940" }, |
|
{ tag: tags.invalid, |
|
color: "#f00" } |
|
]); |
|
|
|
const baseTheme = EditorView.baseTheme({ |
|
"&.cm-focused .cm-matchingBracket": { backgroundColor: "#328c8252" }, |
|
"&.cm-focused .cm-nonmatchingBracket": { backgroundColor: "#bb555544" } |
|
}); |
|
const DefaultScanDist = 10000, DefaultBrackets = "()[]{}"; |
|
const bracketMatchingConfig = Facet.define({ |
|
combine(configs) { |
|
return combineConfig(configs, { |
|
afterCursor: true, |
|
brackets: DefaultBrackets, |
|
maxScanDistance: DefaultScanDist, |
|
renderMatch: defaultRenderMatch |
|
}); |
|
} |
|
}); |
|
const matchingMark = Decoration.mark({ class: "cm-matchingBracket" }), nonmatchingMark = Decoration.mark({ class: "cm-nonmatchingBracket" }); |
|
function defaultRenderMatch(match) { |
|
let decorations = []; |
|
let mark = match.matched ? matchingMark : nonmatchingMark; |
|
decorations.push(mark.range(match.start.from, match.start.to)); |
|
if (match.end) |
|
decorations.push(mark.range(match.end.from, match.end.to)); |
|
return decorations; |
|
} |
|
const bracketMatchingState = StateField.define({ |
|
create() { return Decoration.none; }, |
|
update(deco, tr) { |
|
if (!tr.docChanged && !tr.selection) |
|
return deco; |
|
let decorations = []; |
|
let config = tr.state.facet(bracketMatchingConfig); |
|
for (let range of tr.state.selection.ranges) { |
|
if (!range.empty) |
|
continue; |
|
let match = matchBrackets(tr.state, range.head, -1, config) |
|
|| (range.head > 0 && matchBrackets(tr.state, range.head - 1, 1, config)) |
|
|| (config.afterCursor && |
|
(matchBrackets(tr.state, range.head, 1, config) || |
|
(range.head < tr.state.doc.length && matchBrackets(tr.state, range.head + 1, -1, config)))); |
|
if (match) |
|
decorations = decorations.concat(config.renderMatch(match, tr.state)); |
|
} |
|
return Decoration.set(decorations, true); |
|
}, |
|
provide: f => EditorView.decorations.from(f) |
|
}); |
|
const bracketMatchingUnique = [ |
|
bracketMatchingState, |
|
baseTheme |
|
]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
function bracketMatching(config = {}) { |
|
return [bracketMatchingConfig.of(config), bracketMatchingUnique]; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const bracketMatchingHandle = new NodeProp(); |
|
function matchingNodes(node, dir, brackets) { |
|
let byProp = node.prop(dir < 0 ? NodeProp.openedBy : NodeProp.closedBy); |
|
if (byProp) |
|
return byProp; |
|
if (node.name.length == 1) { |
|
let index = brackets.indexOf(node.name); |
|
if (index > -1 && index % 2 == (dir < 0 ? 1 : 0)) |
|
return [brackets[index + dir]]; |
|
} |
|
return null; |
|
} |
|
function findHandle(node) { |
|
let hasHandle = node.type.prop(bracketMatchingHandle); |
|
return hasHandle ? hasHandle(node.node) : node; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function matchBrackets(state, pos, dir, config = {}) { |
|
let maxScanDistance = config.maxScanDistance || DefaultScanDist, brackets = config.brackets || DefaultBrackets; |
|
let tree = syntaxTree(state), node = tree.resolveInner(pos, dir); |
|
for (let cur = node; cur; cur = cur.parent) { |
|
let matches = matchingNodes(cur.type, dir, brackets); |
|
if (matches && cur.from < cur.to) { |
|
let handle = findHandle(cur); |
|
if (handle && (dir > 0 ? pos >= handle.from && pos < handle.to : pos > handle.from && pos <= handle.to)) |
|
return matchMarkedBrackets(state, pos, dir, cur, handle, matches, brackets); |
|
} |
|
} |
|
return matchPlainBrackets(state, pos, dir, tree, node.type, maxScanDistance, brackets); |
|
} |
|
function matchMarkedBrackets(_state, _pos, dir, token, handle, matching, brackets) { |
|
let parent = token.parent, firstToken = { from: handle.from, to: handle.to }; |
|
let depth = 0, cursor = parent === null || parent === void 0 ? void 0 : parent.cursor(); |
|
if (cursor && (dir < 0 ? cursor.childBefore(token.from) : cursor.childAfter(token.to))) |
|
do { |
|
if (dir < 0 ? cursor.to <= token.from : cursor.from >= token.to) { |
|
if (depth == 0 && matching.indexOf(cursor.type.name) > -1 && cursor.from < cursor.to) { |
|
let endHandle = findHandle(cursor); |
|
return { start: firstToken, end: endHandle ? { from: endHandle.from, to: endHandle.to } : undefined, matched: true }; |
|
} |
|
else if (matchingNodes(cursor.type, dir, brackets)) { |
|
depth++; |
|
} |
|
else if (matchingNodes(cursor.type, -dir, brackets)) { |
|
if (depth == 0) { |
|
let endHandle = findHandle(cursor); |
|
return { |
|
start: firstToken, |
|
end: endHandle && endHandle.from < endHandle.to ? { from: endHandle.from, to: endHandle.to } : undefined, |
|
matched: false |
|
}; |
|
} |
|
depth--; |
|
} |
|
} |
|
} while (dir < 0 ? cursor.prevSibling() : cursor.nextSibling()); |
|
return { start: firstToken, matched: false }; |
|
} |
|
function matchPlainBrackets(state, pos, dir, tree, tokenType, maxScanDistance, brackets) { |
|
let startCh = dir < 0 ? state.sliceDoc(pos - 1, pos) : state.sliceDoc(pos, pos + 1); |
|
let bracket = brackets.indexOf(startCh); |
|
if (bracket < 0 || (bracket % 2 == 0) != (dir > 0)) |
|
return null; |
|
let startToken = { from: dir < 0 ? pos - 1 : pos, to: dir > 0 ? pos + 1 : pos }; |
|
let iter = state.doc.iterRange(pos, dir > 0 ? state.doc.length : 0), depth = 0; |
|
for (let distance = 0; !(iter.next()).done && distance <= maxScanDistance;) { |
|
let text = iter.value; |
|
if (dir < 0) |
|
distance += text.length; |
|
let basePos = pos + distance * dir; |
|
for (let pos = dir > 0 ? 0 : text.length - 1, end = dir > 0 ? text.length : -1; pos != end; pos += dir) { |
|
let found = brackets.indexOf(text[pos]); |
|
if (found < 0 || tree.resolveInner(basePos + pos, 1).type != tokenType) |
|
continue; |
|
if ((found % 2 == 0) == (dir > 0)) { |
|
depth++; |
|
} |
|
else if (depth == 1) { |
|
return { start: startToken, end: { from: basePos + pos, to: basePos + pos + 1 }, matched: (found >> 1) == (bracket >> 1) }; |
|
} |
|
else { |
|
depth--; |
|
} |
|
} |
|
if (dir > 0) |
|
distance += text.length; |
|
} |
|
return iter.done ? { start: startToken, matched: false } : null; |
|
} |
|
|
|
|
|
|
|
function countCol(string, end, tabSize, startIndex = 0, startValue = 0) { |
|
if (end == null) { |
|
end = string.search(/[^\s\u00a0]/); |
|
if (end == -1) |
|
end = string.length; |
|
} |
|
let n = startValue; |
|
for (let i = startIndex; i < end; i++) { |
|
if (string.charCodeAt(i) == 9) |
|
n += tabSize - (n % tabSize); |
|
else |
|
n++; |
|
} |
|
return n; |
|
} |
|
|
|
|
|
|
|
|
|
class StringStream { |
|
|
|
|
|
|
|
constructor( |
|
|
|
|
|
|
|
string, tabSize, |
|
|
|
|
|
|
|
indentUnit, overrideIndent) { |
|
this.string = string; |
|
this.tabSize = tabSize; |
|
this.indentUnit = indentUnit; |
|
this.overrideIndent = overrideIndent; |
|
|
|
|
|
|
|
this.pos = 0; |
|
|
|
|
|
|
|
this.start = 0; |
|
this.lastColumnPos = 0; |
|
this.lastColumnValue = 0; |
|
} |
|
|
|
|
|
|
|
eol() { return this.pos >= this.string.length; } |
|
|
|
|
|
|
|
sol() { return this.pos == 0; } |
|
|
|
|
|
|
|
|
|
peek() { return this.string.charAt(this.pos) || undefined; } |
|
|
|
|
|
|
|
next() { |
|
if (this.pos < this.string.length) |
|
return this.string.charAt(this.pos++); |
|
} |
|
|
|
|
|
|
|
|
|
eat(match) { |
|
let ch = this.string.charAt(this.pos); |
|
let ok; |
|
if (typeof match == "string") |
|
ok = ch == match; |
|
else |
|
ok = ch && (match instanceof RegExp ? match.test(ch) : match(ch)); |
|
if (ok) { |
|
++this.pos; |
|
return ch; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
eatWhile(match) { |
|
let start = this.pos; |
|
while (this.eat(match)) { } |
|
return this.pos > start; |
|
} |
|
|
|
|
|
|
|
|
|
eatSpace() { |
|
let start = this.pos; |
|
while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) |
|
++this.pos; |
|
return this.pos > start; |
|
} |
|
|
|
|
|
|
|
skipToEnd() { this.pos = this.string.length; } |
|
|
|
|
|
|
|
|
|
skipTo(ch) { |
|
let found = this.string.indexOf(ch, this.pos); |
|
if (found > -1) { |
|
this.pos = found; |
|
return true; |
|
} |
|
} |
|
|
|
|
|
|
|
backUp(n) { this.pos -= n; } |
|
|
|
|
|
|
|
column() { |
|
if (this.lastColumnPos < this.start) { |
|
this.lastColumnValue = countCol(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); |
|
this.lastColumnPos = this.start; |
|
} |
|
return this.lastColumnValue; |
|
} |
|
|
|
|
|
|
|
indentation() { |
|
var _a; |
|
return (_a = this.overrideIndent) !== null && _a !== void 0 ? _a : countCol(this.string, null, this.tabSize); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
match(pattern, consume, caseInsensitive) { |
|
if (typeof pattern == "string") { |
|
let cased = (str) => caseInsensitive ? str.toLowerCase() : str; |
|
let substr = this.string.substr(this.pos, pattern.length); |
|
if (cased(substr) == cased(pattern)) { |
|
if (consume !== false) |
|
this.pos += pattern.length; |
|
return true; |
|
} |
|
else |
|
return null; |
|
} |
|
else { |
|
let match = this.string.slice(this.pos).match(pattern); |
|
if (match && match.index > 0) |
|
return null; |
|
if (match && consume !== false) |
|
this.pos += match[0].length; |
|
return match; |
|
} |
|
} |
|
|
|
|
|
|
|
current() { return this.string.slice(this.start, this.pos); } |
|
} |
|
|
|
function fullParser(spec) { |
|
return { |
|
name: spec.name || "", |
|
token: spec.token, |
|
blankLine: spec.blankLine || (() => { }), |
|
startState: spec.startState || (() => true), |
|
copyState: spec.copyState || defaultCopyState, |
|
indent: spec.indent || (() => null), |
|
languageData: spec.languageData || {}, |
|
tokenTable: spec.tokenTable || noTokens |
|
}; |
|
} |
|
function defaultCopyState(state) { |
|
if (typeof state != "object") |
|
return state; |
|
let newState = {}; |
|
for (let prop in state) { |
|
let val = state[prop]; |
|
newState[prop] = (val instanceof Array ? val.slice() : val); |
|
} |
|
return newState; |
|
} |
|
const IndentedFrom = new WeakMap(); |
|
|
|
|
|
|
|
|
|
class StreamLanguage extends Language { |
|
constructor(parser) { |
|
let data = defineLanguageFacet(parser.languageData); |
|
let p = fullParser(parser), self; |
|
let impl = new class extends Parser { |
|
createParse(input, fragments, ranges) { |
|
return new Parse(self, input, fragments, ranges); |
|
} |
|
}; |
|
super(data, impl, [indentService.of((cx, pos) => this.getIndent(cx, pos))], parser.name); |
|
this.topNode = docID(data); |
|
self = this; |
|
this.streamParser = p; |
|
this.stateAfter = new NodeProp({ perNode: true }); |
|
this.tokenTable = parser.tokenTable ? new TokenTable(p.tokenTable) : defaultTokenTable; |
|
} |
|
|
|
|
|
|
|
static define(spec) { return new StreamLanguage(spec); } |
|
getIndent(cx, pos) { |
|
let tree = syntaxTree(cx.state), at = tree.resolve(pos); |
|
while (at && at.type != this.topNode) |
|
at = at.parent; |
|
if (!at) |
|
return null; |
|
let from = undefined; |
|
let { overrideIndentation } = cx.options; |
|
if (overrideIndentation) { |
|
from = IndentedFrom.get(cx.state); |
|
if (from != null && from < pos - 1e4) |
|
from = undefined; |
|
} |
|
let start = findState(this, tree, 0, at.from, from !== null && from !== void 0 ? from : pos), statePos, state; |
|
if (start) { |
|
state = start.state; |
|
statePos = start.pos + 1; |
|
} |
|
else { |
|
state = this.streamParser.startState(cx.unit); |
|
statePos = 0; |
|
} |
|
if (pos - statePos > 10000 ) |
|
return null; |
|
while (statePos < pos) { |
|
let line = cx.state.doc.lineAt(statePos), end = Math.min(pos, line.to); |
|
if (line.length) { |
|
let indentation = overrideIndentation ? overrideIndentation(line.from) : -1; |
|
let stream = new StringStream(line.text, cx.state.tabSize, cx.unit, indentation < 0 ? undefined : indentation); |
|
while (stream.pos < end - line.from) |
|
readToken(this.streamParser.token, stream, state); |
|
} |
|
else { |
|
this.streamParser.blankLine(state, cx.unit); |
|
} |
|
if (end == pos) |
|
break; |
|
statePos = line.to + 1; |
|
} |
|
let line = cx.lineAt(pos); |
|
if (overrideIndentation && from == null) |
|
IndentedFrom.set(cx.state, line.from); |
|
return this.streamParser.indent(state, /^\s*(.*)/.exec(line.text)[1], cx); |
|
} |
|
get allowsNesting() { return false; } |
|
} |
|
function findState(lang, tree, off, startPos, before) { |
|
let state = off >= startPos && off + tree.length <= before && tree.prop(lang.stateAfter); |
|
if (state) |
|
return { state: lang.streamParser.copyState(state), pos: off + tree.length }; |
|
for (let i = tree.children.length - 1; i >= 0; i--) { |
|
let child = tree.children[i], pos = off + tree.positions[i]; |
|
let found = child instanceof Tree && pos < before && findState(lang, child, pos, startPos, before); |
|
if (found) |
|
return found; |
|
} |
|
return null; |
|
} |
|
function cutTree(lang, tree, from, to, inside) { |
|
if (inside && from <= 0 && to >= tree.length) |
|
return tree; |
|
if (!inside && tree.type == lang.topNode) |
|
inside = true; |
|
for (let i = tree.children.length - 1; i >= 0; i--) { |
|
let pos = tree.positions[i], child = tree.children[i], inner; |
|
if (pos < to && child instanceof Tree) { |
|
if (!(inner = cutTree(lang, child, from - pos, to - pos, inside))) |
|
break; |
|
return !inside ? inner |
|
: new Tree(tree.type, tree.children.slice(0, i).concat(inner), tree.positions.slice(0, i + 1), pos + inner.length); |
|
} |
|
} |
|
return null; |
|
} |
|
function findStartInFragments(lang, fragments, startPos, editorState) { |
|
for (let f of fragments) { |
|
let from = f.from + (f.openStart ? 25 : 0), to = f.to - (f.openEnd ? 25 : 0); |
|
let found = from <= startPos && to > startPos && findState(lang, f.tree, 0 - f.offset, startPos, to), tree; |
|
if (found && (tree = cutTree(lang, f.tree, startPos + f.offset, found.pos + f.offset, false))) |
|
return { state: found.state, tree }; |
|
} |
|
return { state: lang.streamParser.startState(editorState ? getIndentUnit(editorState) : 4), tree: Tree.empty }; |
|
} |
|
class Parse { |
|
constructor(lang, input, fragments, ranges) { |
|
this.lang = lang; |
|
this.input = input; |
|
this.fragments = fragments; |
|
this.ranges = ranges; |
|
this.stoppedAt = null; |
|
this.chunks = []; |
|
this.chunkPos = []; |
|
this.chunk = []; |
|
this.chunkReused = undefined; |
|
this.rangeIndex = 0; |
|
this.to = ranges[ranges.length - 1].to; |
|
let context = ParseContext.get(), from = ranges[0].from; |
|
let { state, tree } = findStartInFragments(lang, fragments, from, context === null || context === void 0 ? void 0 : context.state); |
|
this.state = state; |
|
this.parsedPos = this.chunkStart = from + tree.length; |
|
for (let i = 0; i < tree.children.length; i++) { |
|
this.chunks.push(tree.children[i]); |
|
this.chunkPos.push(tree.positions[i]); |
|
} |
|
if (context && this.parsedPos < context.viewport.from - 100000 ) { |
|
this.state = this.lang.streamParser.startState(getIndentUnit(context.state)); |
|
context.skipUntilInView(this.parsedPos, context.viewport.from); |
|
this.parsedPos = context.viewport.from; |
|
} |
|
this.moveRangeIndex(); |
|
} |
|
advance() { |
|
let context = ParseContext.get(); |
|
let parseEnd = this.stoppedAt == null ? this.to : Math.min(this.to, this.stoppedAt); |
|
let end = Math.min(parseEnd, this.chunkStart + 2048 ); |
|
if (context) |
|
end = Math.min(end, context.viewport.to); |
|
while (this.parsedPos < end) |
|
this.parseLine(context); |
|
if (this.chunkStart < this.parsedPos) |
|
this.finishChunk(); |
|
if (this.parsedPos >= parseEnd) |
|
return this.finish(); |
|
if (context && this.parsedPos >= context.viewport.to) { |
|
context.skipUntilInView(this.parsedPos, parseEnd); |
|
return this.finish(); |
|
} |
|
return null; |
|
} |
|
stopAt(pos) { |
|
this.stoppedAt = pos; |
|
} |
|
lineAfter(pos) { |
|
let chunk = this.input.chunk(pos); |
|
if (!this.input.lineChunks) { |
|
let eol = chunk.indexOf("\n"); |
|
if (eol > -1) |
|
chunk = chunk.slice(0, eol); |
|
} |
|
else if (chunk == "\n") { |
|
chunk = ""; |
|
} |
|
return pos + chunk.length <= this.to ? chunk : chunk.slice(0, this.to - pos); |
|
} |
|
nextLine() { |
|
let from = this.parsedPos, line = this.lineAfter(from), end = from + line.length; |
|
for (let index = this.rangeIndex;;) { |
|
let rangeEnd = this.ranges[index].to; |
|
if (rangeEnd >= end) |
|
break; |
|
line = line.slice(0, rangeEnd - (end - line.length)); |
|
index++; |
|
if (index == this.ranges.length) |
|
break; |
|
let rangeStart = this.ranges[index].from; |
|
let after = this.lineAfter(rangeStart); |
|
line += after; |
|
end = rangeStart + after.length; |
|
} |
|
return { line, end }; |
|
} |
|
skipGapsTo(pos, offset, side) { |
|
for (;;) { |
|
let end = this.ranges[this.rangeIndex].to, offPos = pos + offset; |
|
if (side > 0 ? end > offPos : end >= offPos) |
|
break; |
|
let start = this.ranges[++this.rangeIndex].from; |
|
offset += start - end; |
|
} |
|
return offset; |
|
} |
|
moveRangeIndex() { |
|
while (this.ranges[this.rangeIndex].to < this.parsedPos) |
|
this.rangeIndex++; |
|
} |
|
emitToken(id, from, to, size, offset) { |
|
if (this.ranges.length > 1) { |
|
offset = this.skipGapsTo(from, offset, 1); |
|
from += offset; |
|
let len0 = this.chunk.length; |
|
offset = this.skipGapsTo(to, offset, -1); |
|
to += offset; |
|
size += this.chunk.length - len0; |
|
} |
|
this.chunk.push(id, from, to, size); |
|
return offset; |
|
} |
|
parseLine(context) { |
|
let { line, end } = this.nextLine(), offset = 0, { streamParser } = this.lang; |
|
let stream = new StringStream(line, context ? context.state.tabSize : 4, context ? getIndentUnit(context.state) : 2); |
|
if (stream.eol()) { |
|
streamParser.blankLine(this.state, stream.indentUnit); |
|
} |
|
else { |
|
while (!stream.eol()) { |
|
let token = readToken(streamParser.token, stream, this.state); |
|
if (token) |
|
offset = this.emitToken(this.lang.tokenTable.resolve(token), this.parsedPos + stream.start, this.parsedPos + stream.pos, 4, offset); |
|
if (stream.start > 10000 ) |
|
break; |
|
} |
|
} |
|
this.parsedPos = end; |
|
this.moveRangeIndex(); |
|
if (this.parsedPos < this.to) |
|
this.parsedPos++; |
|
} |
|
finishChunk() { |
|
let tree = Tree.build({ |
|
buffer: this.chunk, |
|
start: this.chunkStart, |
|
length: this.parsedPos - this.chunkStart, |
|
nodeSet, |
|
topID: 0, |
|
maxBufferLength: 2048 , |
|
reused: this.chunkReused |
|
}); |
|
tree = new Tree(tree.type, tree.children, tree.positions, tree.length, [[this.lang.stateAfter, this.lang.streamParser.copyState(this.state)]]); |
|
this.chunks.push(tree); |
|
this.chunkPos.push(this.chunkStart - this.ranges[0].from); |
|
this.chunk = []; |
|
this.chunkReused = undefined; |
|
this.chunkStart = this.parsedPos; |
|
} |
|
finish() { |
|
return new Tree(this.lang.topNode, this.chunks, this.chunkPos, this.parsedPos - this.ranges[0].from).balance(); |
|
} |
|
} |
|
function readToken(token, stream, state) { |
|
stream.start = stream.pos; |
|
for (let i = 0; i < 10; i++) { |
|
let result = token(stream, state); |
|
if (stream.pos > stream.start) |
|
return result; |
|
} |
|
throw new Error("Stream parser failed to advance stream."); |
|
} |
|
const noTokens = Object.create(null); |
|
const typeArray = [NodeType.none]; |
|
const nodeSet = new NodeSet(typeArray); |
|
const warned = []; |
|
|
|
const byTag = Object.create(null); |
|
const defaultTable = Object.create(null); |
|
for (let [legacyName, name] of [ |
|
["variable", "variableName"], |
|
["variable-2", "variableName.special"], |
|
["string-2", "string.special"], |
|
["def", "variableName.definition"], |
|
["tag", "tagName"], |
|
["attribute", "attributeName"], |
|
["type", "typeName"], |
|
["builtin", "variableName.standard"], |
|
["qualifier", "modifier"], |
|
["error", "invalid"], |
|
["header", "heading"], |
|
["property", "propertyName"] |
|
]) |
|
defaultTable[legacyName] = createTokenType(noTokens, name); |
|
class TokenTable { |
|
constructor(extra) { |
|
this.extra = extra; |
|
this.table = Object.assign(Object.create(null), defaultTable); |
|
} |
|
resolve(tag) { |
|
return !tag ? 0 : this.table[tag] || (this.table[tag] = createTokenType(this.extra, tag)); |
|
} |
|
} |
|
const defaultTokenTable = new TokenTable(noTokens); |
|
function warnForPart(part, msg) { |
|
if (warned.indexOf(part) > -1) |
|
return; |
|
warned.push(part); |
|
console.warn(msg); |
|
} |
|
function createTokenType(extra, tagStr) { |
|
let tags$1 = []; |
|
for (let name of tagStr.split(" ")) { |
|
let found = []; |
|
for (let part of name.split(".")) { |
|
let value = (extra[part] || tags[part]); |
|
if (!value) { |
|
warnForPart(part, `Unknown highlighting tag ${part}`); |
|
} |
|
else if (typeof value == "function") { |
|
if (!found.length) |
|
warnForPart(part, `Modifier ${part} used at start of tag`); |
|
else |
|
found = found.map(value); |
|
} |
|
else { |
|
if (found.length) |
|
warnForPart(part, `Tag ${part} used as modifier`); |
|
else |
|
found = Array.isArray(value) ? value : [value]; |
|
} |
|
} |
|
for (let tag of found) |
|
tags$1.push(tag); |
|
} |
|
if (!tags$1.length) |
|
return 0; |
|
let name = tagStr.replace(/ /g, "_"), key = name + " " + tags$1.map(t => t.id); |
|
let known = byTag[key]; |
|
if (known) |
|
return known.id; |
|
let type = byTag[key] = NodeType.define({ |
|
id: typeArray.length, |
|
name, |
|
props: [styleTags({ [name]: tags$1 })] |
|
}); |
|
typeArray.push(type); |
|
return type.id; |
|
} |
|
function docID(data) { |
|
let type = NodeType.define({ id: typeArray.length, name: "Document", props: [languageDataProp.add(() => data)], top: true }); |
|
typeArray.push(type); |
|
return type; |
|
} |
|
|
|
function buildForLine(line) { |
|
return line.length <= 4096 && /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac\ufb50-\ufdff]/.test(line); |
|
} |
|
function textHasRTL(text) { |
|
for (let i = text.iter(); !i.next().done;) |
|
if (buildForLine(i.value)) |
|
return true; |
|
return false; |
|
} |
|
function changeAddsRTL(change) { |
|
let added = false; |
|
change.iterChanges((fA, tA, fB, tB, ins) => { |
|
if (!added && textHasRTL(ins)) |
|
added = true; |
|
}); |
|
return added; |
|
} |
|
const alwaysIsolate = Facet.define({ combine: values => values.some(x => x) }); |
|
|
|
|
|
|
|
|
|
|
|
|
|
function bidiIsolates(options = {}) { |
|
let extensions = [isolateMarks]; |
|
if (options.alwaysIsolate) |
|
extensions.push(alwaysIsolate.of(true)); |
|
return extensions; |
|
} |
|
const isolateMarks = ViewPlugin.fromClass(class { |
|
constructor(view) { |
|
this.always = view.state.facet(alwaysIsolate) || |
|
view.textDirection != Direction.LTR || |
|
view.state.facet(EditorView.perLineTextDirection); |
|
this.hasRTL = !this.always && textHasRTL(view.state.doc); |
|
this.tree = syntaxTree(view.state); |
|
this.decorations = this.always || this.hasRTL ? buildDeco(view, this.tree, this.always) : Decoration.none; |
|
} |
|
update(update) { |
|
let always = update.state.facet(alwaysIsolate) || |
|
update.view.textDirection != Direction.LTR || |
|
update.state.facet(EditorView.perLineTextDirection); |
|
if (!always && !this.hasRTL && changeAddsRTL(update.changes)) |
|
this.hasRTL = true; |
|
if (!always && !this.hasRTL) |
|
return; |
|
let tree = syntaxTree(update.state); |
|
if (always != this.always || tree != this.tree || update.docChanged || update.viewportChanged) { |
|
this.tree = tree; |
|
this.always = always; |
|
this.decorations = buildDeco(update.view, tree, always); |
|
} |
|
} |
|
}, { |
|
provide: plugin => { |
|
function access(view) { |
|
var _a, _b; |
|
return (_b = (_a = view.plugin(plugin)) === null || _a === void 0 ? void 0 : _a.decorations) !== null && _b !== void 0 ? _b : Decoration.none; |
|
} |
|
return [EditorView.outerDecorations.of(access), |
|
Prec.lowest(EditorView.bidiIsolatedRanges.of(access))]; |
|
} |
|
}); |
|
function buildDeco(view, tree, always) { |
|
let deco = new RangeSetBuilder(); |
|
let ranges = view.visibleRanges; |
|
if (!always) |
|
ranges = clipRTLLines(ranges, view.state.doc); |
|
for (let { from, to } of ranges) { |
|
tree.iterate({ |
|
enter: node => { |
|
let iso = node.type.prop(NodeProp.isolate); |
|
if (iso) |
|
deco.add(node.from, node.to, marks[iso]); |
|
}, |
|
from, to |
|
}); |
|
} |
|
return deco.finish(); |
|
} |
|
function clipRTLLines(ranges, doc) { |
|
let cur = doc.iter(), pos = 0, result = [], last = null; |
|
for (let { from, to } of ranges) { |
|
if (last && last.to > from) { |
|
from = last.to; |
|
if (from >= to) |
|
continue; |
|
} |
|
if (pos + cur.value.length < from) { |
|
cur.next(from - (pos + cur.value.length)); |
|
pos = from; |
|
} |
|
for (;;) { |
|
let start = pos, end = pos + cur.value.length; |
|
if (!cur.lineBreak && buildForLine(cur.value)) { |
|
if (last && last.to > start - 10) |
|
last.to = Math.min(to, end); |
|
else |
|
result.push(last = { from: start, to: Math.min(to, end) }); |
|
} |
|
if (end >= to) |
|
break; |
|
pos = end; |
|
cur.next(); |
|
} |
|
} |
|
return result; |
|
} |
|
const marks = { |
|
rtl: Decoration.mark({ class: "cm-iso", inclusive: true, attributes: { dir: "rtl" }, bidiIsolate: Direction.RTL }), |
|
ltr: Decoration.mark({ class: "cm-iso", inclusive: true, attributes: { dir: "ltr" }, bidiIsolate: Direction.LTR }), |
|
auto: Decoration.mark({ class: "cm-iso", inclusive: true, attributes: { dir: "auto" }, bidiIsolate: null }) |
|
}; |
|
|
|
export { DocInput, HighlightStyle, IndentContext, LRLanguage, Language, LanguageDescription, LanguageSupport, ParseContext, StreamLanguage, StringStream, TreeIndentContext, bidiIsolates, bracketMatching, bracketMatchingHandle, codeFolding, continuedIndent, defaultHighlightStyle, defineLanguageFacet, delimitedIndent, ensureSyntaxTree, flatIndent, foldAll, foldCode, foldEffect, foldGutter, foldInside, foldKeymap, foldNodeProp, foldService, foldState, foldable, foldedRanges, forceParsing, getIndentUnit, getIndentation, highlightingFor, indentNodeProp, indentOnInput, indentRange, indentService, indentString, indentUnit, language, languageDataProp, matchBrackets, sublanguageProp, syntaxHighlighting, syntaxParserRunning, syntaxTree, syntaxTreeAvailable, toggleFold, unfoldAll, unfoldCode, unfoldEffect }; |
|
|