You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2726 lines
101 KiB
2726 lines
101 KiB
'use strict'; |
|
|
|
var common = require('@lezer/common'); |
|
var state = require('@codemirror/state'); |
|
var view = require('@codemirror/view'); |
|
var highlight = require('@lezer/highlight'); |
|
var styleMod = require('style-mod'); |
|
|
|
var _a; |
|
/** |
|
Node prop stored in a parser's top syntax node to provide the |
|
facet that stores language-specific data for that language. |
|
*/ |
|
const languageDataProp = new common.NodeProp(); |
|
/** |
|
Helper function to define a facet (to be added to the top syntax |
|
node(s) for a language via |
|
[`languageDataProp`](https://codemirror.net/6/docs/ref/#language.languageDataProp)), that will be |
|
used to associate language data with the language. You |
|
probably only need this when subclassing |
|
[`Language`](https://codemirror.net/6/docs/ref/#language.Language). |
|
*/ |
|
function defineLanguageFacet(baseData) { |
|
return state.Facet.define({ |
|
combine: baseData ? values => values.concat(baseData) : undefined |
|
}); |
|
} |
|
/** |
|
Syntax node prop used to register sublanguages. Should be added to |
|
the top level node type for the language. |
|
*/ |
|
const sublanguageProp = new common.NodeProp(); |
|
/** |
|
A language object manages parsing and per-language |
|
[metadata](https://codemirror.net/6/docs/ref/#state.EditorState.languageDataAt). Parse data is |
|
managed as a [Lezer](https://lezer.codemirror.net) tree. The class |
|
can be used directly, via the [`LRLanguage`](https://codemirror.net/6/docs/ref/#language.LRLanguage) |
|
subclass for [Lezer](https://lezer.codemirror.net/) LR parsers, or |
|
via the [`StreamLanguage`](https://codemirror.net/6/docs/ref/#language.StreamLanguage) subclass |
|
for stream parsers. |
|
*/ |
|
class Language { |
|
/** |
|
Construct a language object. If you need to invoke this |
|
directly, first define a data facet with |
|
[`defineLanguageFacet`](https://codemirror.net/6/docs/ref/#language.defineLanguageFacet), and then |
|
configure your parser to [attach](https://codemirror.net/6/docs/ref/#language.languageDataProp) it |
|
to the language's outer syntax node. |
|
*/ |
|
constructor( |
|
/** |
|
The [language data](https://codemirror.net/6/docs/ref/#state.EditorState.languageDataAt) facet |
|
used for this language. |
|
*/ |
|
data, parser, extraExtensions = [], |
|
/** |
|
A language name. |
|
*/ |
|
name = "") { |
|
this.data = data; |
|
this.name = name; |
|
// Kludge to define EditorState.tree as a debugging helper, |
|
// without the EditorState package actually knowing about |
|
// languages and lezer trees. |
|
if (!state.EditorState.prototype.hasOwnProperty("tree")) |
|
Object.defineProperty(state.EditorState.prototype, "tree", { get() { return syntaxTree(this); } }); |
|
this.parser = parser; |
|
this.extension = [ |
|
language.of(this), |
|
state.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); |
|
} |
|
/** |
|
Query whether this language is active at the given position. |
|
*/ |
|
isActiveAt(state, pos, side = -1) { |
|
return topNodeAt(state, pos, side).type.prop(languageDataProp) == this.data; |
|
} |
|
/** |
|
Find the document regions that were parsed using this language. |
|
The returned regions will _include_ any nested languages rooted |
|
in this language, when those exist. |
|
*/ |
|
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(common.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 common.Tree) |
|
explore(ch, tree.positions[i] + from); |
|
} |
|
}; |
|
explore(syntaxTree(state), 0); |
|
return result; |
|
} |
|
/** |
|
Indicates whether this language allows nested languages. The |
|
default implementation returns true. |
|
*/ |
|
get allowsNesting() { return true; } |
|
} |
|
/** |
|
@internal |
|
*/ |
|
Language.setState = state.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, common.IterMode.ExcludeBuffers | common.IterMode.EnterBracketed)) |
|
if (node.type.isTop) |
|
tree = node; |
|
} |
|
return tree; |
|
} |
|
/** |
|
A subclass of [`Language`](https://codemirror.net/6/docs/ref/#language.Language) for use with Lezer |
|
[LR parsers](https://lezer.codemirror.net/docs/ref#lr.LRParser) |
|
parsers. |
|
*/ |
|
class LRLanguage extends Language { |
|
constructor(data, parser, name) { |
|
super(data, parser, [], name); |
|
this.parser = parser; |
|
} |
|
/** |
|
Define a language from a 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); |
|
} |
|
/** |
|
Create a new instance of this language with a reconfigured |
|
version of its parser and optionally a new name. |
|
*/ |
|
configure(options, name) { |
|
return new LRLanguage(this.data, this.parser.configure(options), name || this.name); |
|
} |
|
get allowsNesting() { return this.parser.hasWrappers(); } |
|
} |
|
/** |
|
Get the syntax tree for a state, which is the current (possibly |
|
incomplete) parse tree of the active |
|
[language](https://codemirror.net/6/docs/ref/#language.Language), or the empty tree if there is no |
|
language available. |
|
*/ |
|
function syntaxTree(state) { |
|
let field = state.field(Language.state, false); |
|
return field ? field.tree : common.Tree.empty; |
|
} |
|
/** |
|
Try to get a parse tree that spans at least up to `upto`. The |
|
method will do at most `timeout` milliseconds of work to parse |
|
up to that point if the tree isn't already available. |
|
*/ |
|
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; |
|
} |
|
/** |
|
Queries whether there is a full syntax tree available up to the |
|
given document position. If there isn't, the background parse |
|
process _might_ still be working and update the tree further, but |
|
there is no guarantee of that—the parser will [stop |
|
working](https://codemirror.net/6/docs/ref/#language.syntaxParserRunning) when it has spent a |
|
certain amount of time or has moved beyond the visible viewport. |
|
Always returns false if no language has been enabled. |
|
*/ |
|
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; |
|
} |
|
/** |
|
Move parsing forward, and update the editor state afterwards to |
|
reflect the new tree. Will work for at most `timeout` |
|
milliseconds. Returns true if the parser managed get to the given |
|
position in that time. |
|
*/ |
|
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; |
|
} |
|
/** |
|
Tells you whether the language parser is planning to do more |
|
parsing work (in a `requestIdleCallback` pseudo-thread) or has |
|
stopped running, either because it parsed the entire document, |
|
because it spent too much time and was cut off, or because there |
|
is no language parser enabled. |
|
*/ |
|
function syntaxParserRunning(view) { |
|
var _a; |
|
return ((_a = view.plugin(parseWorker)) === null || _a === void 0 ? void 0 : _a.isWorking()) || false; |
|
} |
|
/** |
|
Lezer-style |
|
[`Input`](https://lezer.codemirror.net/docs/ref#common.Input) |
|
object for a [`Text`](https://codemirror.net/6/docs/ref/#state.Text) object. |
|
*/ |
|
class DocInput { |
|
/** |
|
Create an input object for the given document. |
|
*/ |
|
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; |
|
/** |
|
A parse context provided to parsers working on the editor content. |
|
*/ |
|
class ParseContext { |
|
constructor(parser, |
|
/** |
|
The current editor state. |
|
*/ |
|
state, |
|
/** |
|
Tree fragments that can be reused by incremental re-parses. |
|
*/ |
|
fragments = [], |
|
/** |
|
@internal |
|
*/ |
|
tree, |
|
/** |
|
@internal |
|
*/ |
|
treeLen, |
|
/** |
|
The current editor viewport (or some overapproximation |
|
thereof). Intended to be used for opportunistically avoiding |
|
work (in which case |
|
[`skipUntilInView`](https://codemirror.net/6/docs/ref/#language.ParseContext.skipUntilInView) |
|
should be called to make sure the parser is restarted when the |
|
skipped region becomes visible). |
|
*/ |
|
viewport, |
|
/** |
|
@internal |
|
*/ |
|
skipped, |
|
/** |
|
This is where skipping parsers can register a promise that, |
|
when resolved, will schedule a new parse. It is cleared when |
|
the parse worker picks up the promise. @internal |
|
*/ |
|
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; |
|
/** |
|
@internal |
|
*/ |
|
this.tempSkipped = []; |
|
} |
|
/** |
|
@internal |
|
*/ |
|
static create(parser, state, viewport) { |
|
return new ParseContext(parser, state, [], common.Tree.empty, 0, viewport, [], null); |
|
} |
|
startParse() { |
|
return this.parser.startParse(new DocInput(this.state.doc), this.fragments); |
|
} |
|
/** |
|
@internal |
|
*/ |
|
work(until, upto) { |
|
if (upto != null && upto >= this.state.doc.length) |
|
upto = undefined; |
|
if (this.tree != common.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(common.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; |
|
} |
|
}); |
|
} |
|
/** |
|
@internal |
|
*/ |
|
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(common.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; |
|
} |
|
/** |
|
@internal |
|
*/ |
|
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 = common.TreeFragment.applyChanges(fragments, ranges); |
|
tree = common.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); |
|
} |
|
/** |
|
@internal |
|
*/ |
|
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; |
|
} |
|
/** |
|
@internal |
|
*/ |
|
reset() { |
|
if (this.parse) { |
|
this.takeTree(); |
|
this.parse = null; |
|
} |
|
} |
|
/** |
|
Notify the parse scheduler that the given region was skipped |
|
because it wasn't in view, and the parse should be restarted |
|
when it comes into view. |
|
*/ |
|
skipUntilInView(from, to) { |
|
this.skipped.push({ from, to }); |
|
} |
|
/** |
|
Returns a parser intended to be used as placeholder when |
|
asynchronously loading a nested parser. It'll skip its input and |
|
mark it as not-really-parsed, so that the next update will parse |
|
it again. |
|
|
|
When `until` is given, a reparse will be scheduled when that |
|
promise resolves. |
|
*/ |
|
static getSkippingParser(until) { |
|
return new class extends common.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 common.Tree(common.NodeType.none, [], [], to - from); |
|
}, |
|
stoppedAt: null, |
|
stopAt() { } |
|
}; |
|
return parser; |
|
} |
|
}; |
|
} |
|
/** |
|
@internal |
|
*/ |
|
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; |
|
} |
|
/** |
|
Get the context for the current parse, or `null` if no editor |
|
parse is in progress. |
|
*/ |
|
static get() { return currentContext; } |
|
} |
|
function cutFragments(fragments, from, to) { |
|
return common.TreeFragment.applyChanges(fragments, [{ fromA: from, toA: to, fromB: from, toB: to }]); |
|
} |
|
class LanguageState { |
|
constructor( |
|
// A mutable parse state that is used to preserve work done during |
|
// the lifetime of a state when moving to the next state. |
|
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); |
|
// If the previous parse wasn't done, go forward only up to its |
|
// end position or the end of the viewport, to avoid slowing down |
|
// state updates with parse work beyond the viewport. |
|
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 /* Work.Apply */, upto)) |
|
newCx.takeTree(); |
|
return new LanguageState(newCx); |
|
} |
|
static init(state) { |
|
let vpTo = Math.min(3000 /* Work.InitViewport */, state.doc.length); |
|
let parseState = ParseContext.create(state.facet(language).parser, state, { from: 0, to: vpTo }); |
|
if (!parseState.work(20 /* Work.Apply */, vpTo)) |
|
parseState.takeTree(); |
|
return new LanguageState(parseState); |
|
} |
|
} |
|
Language.state = 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 /* Work.MaxPause */); |
|
return () => clearTimeout(timeout); |
|
}; |
|
if (typeof requestIdleCallback != "undefined") |
|
requestIdle = (callback) => { |
|
let idle = -1, timeout = setTimeout(() => { |
|
idle = requestIdleCallback(callback, { timeout: 500 /* Work.MaxPause */ - 100 /* Work.MinPause */ }); |
|
}, 100 /* Work.MinPause */); |
|
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 = view.ViewPlugin.fromClass(class ParseWorker { |
|
constructor(view) { |
|
this.view = view; |
|
this.working = null; |
|
this.workScheduled = 0; |
|
// End of the current time chunk |
|
this.chunkEnd = -1; |
|
// Milliseconds of budget left for this chunk |
|
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 /* Work.ChangeBonus */; |
|
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)) { // Start a new chunk |
|
this.chunkEnd = now + 30000 /* Work.ChunkTime */; |
|
this.chunkBudget = 3000 /* Work.ChunkBudget */; |
|
} |
|
if (this.chunkBudget <= 0) |
|
return; // No more budget |
|
let { state, viewport: { to: vpTo } } = this.view, field = state.field(Language.state); |
|
if (field.tree == field.context.tree && field.context.isDone(vpTo + 100000 /* Work.MaxParseAhead */)) |
|
return; |
|
let endTime = Date.now() + Math.min(this.chunkBudget, 100 /* Work.Slice */, deadline && !isInputPending ? Math.max(25 /* Work.MinSlice */, 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 /* Work.MaxParseAhead */)); |
|
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 => view.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(); } } |
|
}); |
|
/** |
|
The facet used to associate a language with an editor state. Used |
|
by `Language` object's `extension` property (so you don't need to |
|
manually wrap your languages in this). Can be used to access the |
|
current language on a state. |
|
*/ |
|
const language = state.Facet.define({ |
|
combine(languages) { return languages.length ? languages[0] : null; }, |
|
enables: language => [ |
|
Language.state, |
|
parseWorker, |
|
view.EditorView.contentAttributes.compute([language], state => { |
|
let lang = state.facet(language); |
|
return lang && lang.name ? { "data-language": lang.name } : {}; |
|
}) |
|
] |
|
}); |
|
/** |
|
This class bundles a [language](https://codemirror.net/6/docs/ref/#language.Language) with an |
|
optional set of supporting extensions. Language packages are |
|
encouraged to export a function that optionally takes a |
|
configuration object and returns a `LanguageSupport` instance, as |
|
the main way for client code to use the package. |
|
*/ |
|
class LanguageSupport { |
|
/** |
|
Create a language support object. |
|
*/ |
|
constructor( |
|
/** |
|
The language object. |
|
*/ |
|
language, |
|
/** |
|
An optional set of supporting extensions. When nesting a |
|
language in another language, the outer language is encouraged |
|
to include the supporting extensions for its inner languages |
|
in its own set of support extensions. |
|
*/ |
|
support = []) { |
|
this.language = language; |
|
this.support = support; |
|
this.extension = [language, support]; |
|
} |
|
} |
|
/** |
|
Language descriptions are used to store metadata about languages |
|
and to dynamically load them. Their main role is finding the |
|
appropriate language for a filename or dynamically loading nested |
|
parsers. |
|
*/ |
|
class LanguageDescription { |
|
constructor( |
|
/** |
|
The name of this language. |
|
*/ |
|
name, |
|
/** |
|
Alternative names for the mode (lowercased, includes `this.name`). |
|
*/ |
|
alias, |
|
/** |
|
File extensions associated with this language. |
|
*/ |
|
extensions, |
|
/** |
|
Optional filename pattern that should be associated with this |
|
language. |
|
*/ |
|
filename, loadFunc, |
|
/** |
|
If the language has been loaded, this will hold its value. |
|
*/ |
|
support = undefined) { |
|
this.name = name; |
|
this.alias = alias; |
|
this.extensions = extensions; |
|
this.filename = filename; |
|
this.loadFunc = loadFunc; |
|
this.support = support; |
|
this.loading = null; |
|
} |
|
/** |
|
Start loading the the language. Will return a promise that |
|
resolves to a [`LanguageSupport`](https://codemirror.net/6/docs/ref/#language.LanguageSupport) |
|
object when the language successfully loads. |
|
*/ |
|
load() { |
|
return this.loading || (this.loading = this.loadFunc().then(support => this.support = support, err => { this.loading = null; throw err; })); |
|
} |
|
/** |
|
Create a language description. |
|
*/ |
|
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); |
|
} |
|
/** |
|
Look for a language in the given array of descriptions that |
|
matches the filename. Will first match |
|
[`filename`](https://codemirror.net/6/docs/ref/#language.LanguageDescription.filename) patterns, |
|
and then [extensions](https://codemirror.net/6/docs/ref/#language.LanguageDescription.extensions), |
|
and return the first language that matches. |
|
*/ |
|
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; |
|
} |
|
/** |
|
Look for a language whose name or alias matches the the given |
|
name (case-insensitively). If `fuzzy` is true, and no direct |
|
matchs is found, this'll also search for a language whose name |
|
or alias occurs in the string (for names shorter than three |
|
characters, only when surrounded by non-word characters). |
|
*/ |
|
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; |
|
} |
|
} |
|
|
|
/** |
|
Facet that defines a way to provide a function that computes the |
|
appropriate indentation depth, as a column number (see |
|
[`indentString`](https://codemirror.net/6/docs/ref/#language.indentString)), at the start of a given |
|
line. A return value of `null` indicates no indentation can be |
|
determined, and the line should inherit the indentation of the one |
|
above it. A return value of `undefined` defers to the next indent |
|
service. |
|
*/ |
|
const indentService = state.Facet.define(); |
|
/** |
|
Facet for overriding the unit by which indentation happens. Should |
|
be a string consisting entirely of the same whitespace character. |
|
When not set, this defaults to 2 spaces. |
|
*/ |
|
const indentUnit = state.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; |
|
} |
|
}); |
|
/** |
|
Return the _column width_ of an indent unit in the state. |
|
Determined by the [`indentUnit`](https://codemirror.net/6/docs/ref/#language.indentUnit) |
|
facet, and [`tabSize`](https://codemirror.net/6/docs/ref/#state.EditorState^tabSize) when that |
|
contains tabs. |
|
*/ |
|
function getIndentUnit(state) { |
|
let unit = state.facet(indentUnit); |
|
return unit.charCodeAt(0) == 9 ? state.tabSize * unit.length : unit.length; |
|
} |
|
/** |
|
Create an indentation string that covers columns 0 to `cols`. |
|
Will use tabs for as much of the columns as possible when the |
|
[`indentUnit`](https://codemirror.net/6/docs/ref/#language.indentUnit) facet contains |
|
tabs. |
|
*/ |
|
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; |
|
} |
|
/** |
|
Get the indentation, as a column number, at the given position. |
|
Will first consult any [indent services](https://codemirror.net/6/docs/ref/#language.indentService) |
|
that are registered, and if none of those return an indentation, |
|
this will check the syntax tree for the [indent node |
|
prop](https://codemirror.net/6/docs/ref/#language.indentNodeProp) and use that if found. Returns a |
|
number when an indentation could be determined, and null |
|
otherwise. |
|
*/ |
|
function getIndentation(context, pos) { |
|
if (context instanceof state.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; |
|
} |
|
/** |
|
Create a change set that auto-indents all lines touched by the |
|
given document range. |
|
*/ |
|
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); |
|
} |
|
/** |
|
Indentation contexts are used when calling [indentation |
|
services](https://codemirror.net/6/docs/ref/#language.indentService). They provide helper utilities |
|
useful in indentation logic, and can selectively override the |
|
indentation reported for some lines. |
|
*/ |
|
class IndentContext { |
|
/** |
|
Create an indent context. |
|
*/ |
|
constructor( |
|
/** |
|
The editor state. |
|
*/ |
|
state, |
|
/** |
|
@internal |
|
*/ |
|
options = {}) { |
|
this.state = state; |
|
this.options = options; |
|
this.unit = getIndentUnit(state); |
|
} |
|
/** |
|
Get a description of the line at the given position, taking |
|
[simulated line |
|
breaks](https://codemirror.net/6/docs/ref/#language.IndentContext.constructor^options.simulateBreak) |
|
into account. If there is such a break at `pos`, the `bias` |
|
argument determines whether the part of the line line before or |
|
after the break is used. |
|
*/ |
|
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; |
|
} |
|
/** |
|
Get the text directly after `pos`, either the entire line |
|
or the next 100 characters, whichever is shorter. |
|
*/ |
|
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)); |
|
} |
|
/** |
|
Find the column for the given position. |
|
*/ |
|
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; |
|
} |
|
/** |
|
Find the column position (taking tabs into account) of the given |
|
position in the given string. |
|
*/ |
|
countColumn(line, pos = line.length) { |
|
return state.countColumn(line, this.state.tabSize, pos); |
|
} |
|
/** |
|
Find the indentation column of the line at the given point. |
|
*/ |
|
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|$/)); |
|
} |
|
/** |
|
Returns the [simulated line |
|
break](https://codemirror.net/6/docs/ref/#language.IndentContext.constructor^options.simulateBreak) |
|
for this context, if any. |
|
*/ |
|
get simulatedBreak() { |
|
return this.options.simulateBreak || null; |
|
} |
|
} |
|
/** |
|
A syntax tree node prop used to associate indentation strategies |
|
with node types. Such a strategy is a function from an indentation |
|
context to a column number (see also |
|
[`indentString`](https://codemirror.net/6/docs/ref/#language.indentString)) or null, where null |
|
indicates that no definitive indentation can be determined. |
|
*/ |
|
const indentNodeProp = new common.NodeProp(); |
|
// Compute the indentation for a given position from the syntax tree. |
|
function syntaxIndentation(cx, ast, pos) { |
|
let stack = ast.resolveStack(pos); |
|
let inner = ast.resolveInner(pos, -1).resolve(pos, 0).enterUnfinishedNodesBefore(pos); |
|
if (inner != stack.node) { |
|
let add = []; |
|
for (let cur = inner; cur && !(cur.from < stack.node.from || cur.to > stack.node.to || |
|
cur.from == stack.node.from && cur.type == stack.node.type); 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(common.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; } |
|
/** |
|
Objects of this type provide context information and helper |
|
methods to indentation functions registered on syntax nodes. |
|
*/ |
|
class TreeIndentContext extends IndentContext { |
|
constructor(base, |
|
/** |
|
The position at which indentation is being computed. |
|
*/ |
|
pos, |
|
/** |
|
@internal |
|
*/ |
|
context) { |
|
super(base.state, base.options); |
|
this.base = base; |
|
this.pos = pos; |
|
this.context = context; |
|
} |
|
/** |
|
The syntax tree node to which the indentation strategy |
|
applies. |
|
*/ |
|
get node() { return this.context.node; } |
|
/** |
|
@internal |
|
*/ |
|
static create(base, pos, context) { |
|
return new TreeIndentContext(base, pos, context); |
|
} |
|
/** |
|
Get the text directly after `this.pos`, either the entire line |
|
or the next 100 characters, whichever is shorter. |
|
*/ |
|
get textAfter() { |
|
return this.textAfterPos(this.pos); |
|
} |
|
/** |
|
Get the indentation at the reference line for `this.node`, which |
|
is the line on which it starts, unless there is a node that is |
|
_not_ a parent of this node covering the start of that line. If |
|
so, the line at the start of that node is tried, again skipping |
|
on if it is covered by another such node. |
|
*/ |
|
get baseIndent() { |
|
return this.baseIndentFor(this.node); |
|
} |
|
/** |
|
Get the indentation for the reference line of the given node |
|
(see [`baseIndent`](https://codemirror.net/6/docs/ref/#language.TreeIndentContext.baseIndent)). |
|
*/ |
|
baseIndentFor(node) { |
|
let line = this.state.doc.lineAt(node.from); |
|
// Skip line starts that are covered by a sibling (or cousin, etc) |
|
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 looking for indentations in the node's parent nodes, |
|
and return the result of that. |
|
*/ |
|
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; |
|
} |
|
// Check whether a delimited node is aligned (meaning there are |
|
// non-skipped nodes on the same line as the opening delimiter). And |
|
// if so, return the opening token. |
|
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) { |
|
if (next.from >= lineEnd) |
|
return null; |
|
let space = /^ */.exec(openLine.text.slice(openToken.to - openLine.from))[0].length; |
|
return { from: openToken.from, to: openToken.to + space }; |
|
} |
|
pos = next.to; |
|
} |
|
} |
|
/** |
|
An indentation strategy for delimited (usually bracketed) nodes. |
|
Will, by default, indent one unit more than the parent's base |
|
indent unless the line starts with a closing token. When `align` |
|
is true and there are non-skipped nodes on the node's opening |
|
line, the content of the node will be aligned with the end of the |
|
opening node, like this: |
|
|
|
foo(bar, |
|
baz) |
|
*/ |
|
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); |
|
} |
|
/** |
|
An indentation strategy that aligns a node's content to its base |
|
indentation. |
|
*/ |
|
const flatIndent = (context) => context.baseIndent; |
|
/** |
|
Creates an indentation strategy that, by default, indents |
|
continued lines one unit more than the node's base indentation. |
|
You can provide `except` to prevent indentation of lines that |
|
match a pattern (for example `/^else\b/` in `if`/`else` |
|
constructs), and you can change the amount of units used with the |
|
`units` option. |
|
*/ |
|
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; |
|
/** |
|
Enables reindentation on input. When a language defines an |
|
`indentOnInput` field in its [language |
|
data](https://codemirror.net/6/docs/ref/#state.EditorState.languageDataAt), which must hold a regular |
|
expression, the line at the cursor will be reindented whenever new |
|
text is typed and the input from the start of the line up to the |
|
cursor matches that regexp. |
|
|
|
To avoid unneccesary reindents, it is recommended to start the |
|
regexp with `^` (usually followed by `\s*`), and end it with `$`. |
|
For example, `/^\s*\}$/` will reindent when a closing brace is |
|
added at the start of a line. |
|
*/ |
|
function indentOnInput() { |
|
return state.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; |
|
}); |
|
} |
|
|
|
/** |
|
A facet that registers a code folding service. When called with |
|
the extent of a line, such a function should return a foldable |
|
range that starts on that line (but continues beyond it), if one |
|
can be found. |
|
*/ |
|
const foldService = state.Facet.define(); |
|
/** |
|
This node prop is used to associate folding information with |
|
syntax node types. Given a syntax node, it should check whether |
|
that tree is foldable and return the range that can be collapsed |
|
when it is. |
|
*/ |
|
const foldNodeProp = new common.NodeProp(); |
|
/** |
|
[Fold](https://codemirror.net/6/docs/ref/#language.foldNodeProp) function that folds everything but |
|
the first and the last child of a syntax node. Useful for nodes |
|
that start and end with delimiters. |
|
*/ |
|
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; |
|
} |
|
/** |
|
Check whether the given line is foldable. First asks any fold |
|
services registered through |
|
[`foldService`](https://codemirror.net/6/docs/ref/#language.foldService), and if none of them return |
|
a result, tries to query the [fold node |
|
prop](https://codemirror.net/6/docs/ref/#language.foldNodeProp) of syntax nodes that cover the end |
|
of the line. |
|
*/ |
|
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 }; |
|
} |
|
/** |
|
State effect that can be attached to a transaction to fold the |
|
given range. (You probably only need this in exceptional |
|
circumstances—usually you'll just want to let |
|
[`foldCode`](https://codemirror.net/6/docs/ref/#language.foldCode) and the [fold |
|
gutter](https://codemirror.net/6/docs/ref/#language.foldGutter) create the transactions.) |
|
*/ |
|
const foldEffect = state.StateEffect.define({ map: mapRange }); |
|
/** |
|
State effect that unfolds the given range (if it was folded). |
|
*/ |
|
const unfoldEffect = state.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; |
|
} |
|
/** |
|
The state field that stores the folded ranges (as a [decoration |
|
set](https://codemirror.net/6/docs/ref/#view.DecorationSet)). Can be passed to |
|
[`EditorState.toJSON`](https://codemirror.net/6/docs/ref/#state.EditorState.toJSON) and |
|
[`fromJSON`](https://codemirror.net/6/docs/ref/#state.EditorState^fromJSON) to serialize the fold |
|
state. |
|
*/ |
|
const foldState = state.StateField.define({ |
|
create() { |
|
return view.Decoration.none; |
|
}, |
|
update(folded, tr) { |
|
if (tr.isUserEvent("delete")) |
|
tr.changes.iterChangedRanges((fromA, toA) => folded = clearTouchedFolds(folded, fromA, toA)); |
|
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 : |
|
view.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 }); |
|
} |
|
} |
|
// Clear folded ranges that cover the selection head |
|
if (tr.selection) |
|
folded = clearTouchedFolds(folded, tr.selection.main.head); |
|
return folded; |
|
}, |
|
provide: f => view.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 view.Decoration.set(ranges, true); |
|
} |
|
}); |
|
function clearTouchedFolds(folded, from, to = from) { |
|
let touched = false; |
|
folded.between(from, to, (a, b) => { if (a < to && b > from) |
|
touched = true; }); |
|
return !touched ? folded : folded.update({ |
|
filterFrom: from, |
|
filterTo: to, |
|
filter: (a, b) => a >= to || b <= from |
|
}); |
|
} |
|
/** |
|
Get a [range set](https://codemirror.net/6/docs/ref/#state.RangeSet) containing the folded ranges |
|
in the given state. |
|
*/ |
|
function foldedRanges(state$1) { |
|
return state$1.field(foldState, false) || state.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$1, other) { |
|
return state$1.field(foldState, false) ? other : other.concat(state.StateEffect.appendConfig.of(codeFolding())); |
|
} |
|
/** |
|
Fold the lines that are selected, if possible. |
|
*/ |
|
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; |
|
}; |
|
/** |
|
Unfold folded ranges on selected lines. |
|
*/ |
|
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$1, range, fold = true) { |
|
let lineFrom = view$1.state.doc.lineAt(range.from).number, lineTo = view$1.state.doc.lineAt(range.to).number; |
|
return view.EditorView.announce.of(`${view$1.state.phrase(fold ? "Folded lines" : "Unfolded lines")} ${lineFrom} ${view$1.state.phrase("to")} ${lineTo}.`); |
|
} |
|
/** |
|
Fold all top-level foldable ranges. Note that, in most cases, |
|
folding information will depend on the [syntax |
|
tree](https://codemirror.net/6/docs/ref/#language.syntaxTree), and folding everything may not work |
|
reliably when the document hasn't been fully parsed (either |
|
because the editor state was only just initialized, or because the |
|
document is so big that the parser decided not to parse it |
|
entirely). |
|
*/ |
|
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; |
|
}; |
|
/** |
|
Unfold all folded code. |
|
*/ |
|
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; |
|
}; |
|
// Find the foldable region containing the given line, if one exists |
|
function foldableContainer(view, lineBlock) { |
|
// Look backwards through line blocks until we find a foldable region that |
|
// intersects with the line |
|
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); |
|
} |
|
} |
|
/** |
|
Toggle folding at cursors. Unfolds if there is an existing fold |
|
starting in that line, tries to find a foldable range around it |
|
otherwise. |
|
*/ |
|
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; |
|
}; |
|
/** |
|
Default fold-related key bindings. |
|
|
|
- Ctrl-Shift-[ (Cmd-Alt-[ on macOS): [`foldCode`](https://codemirror.net/6/docs/ref/#language.foldCode). |
|
- Ctrl-Shift-] (Cmd-Alt-] on macOS): [`unfoldCode`](https://codemirror.net/6/docs/ref/#language.unfoldCode). |
|
- Ctrl-Alt-[: [`foldAll`](https://codemirror.net/6/docs/ref/#language.foldAll). |
|
- Ctrl-Alt-]: [`unfoldAll`](https://codemirror.net/6/docs/ref/#language.unfoldAll). |
|
*/ |
|
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 = state.Facet.define({ |
|
combine(values) { return state.combineConfig(values, defaultConfig); } |
|
}); |
|
/** |
|
Create an extension that configures code folding. |
|
*/ |
|
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 = view.Decoration.replace({ widget: new class extends view.WidgetType { |
|
toDOM(view) { return widgetToDOM(view, null); } |
|
} }); |
|
class PreparedFoldWidget extends view.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 view.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; |
|
} |
|
} |
|
/** |
|
Create an extension that registers a fold gutter, which shows a |
|
fold status indicator before foldable lines (which can be clicked |
|
to fold or unfold the line). |
|
*/ |
|
function foldGutter(config = {}) { |
|
let fullConfig = { ...foldGutterDefaults, ...config }; |
|
let canFold = new FoldMarker(fullConfig, true), canUnfold = new FoldMarker(fullConfig, false); |
|
let markers = view.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 state.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, |
|
view.gutter({ |
|
class: "cm-foldGutter", |
|
markers(view) { var _a; return ((_a = view.plugin(markers)) === null || _a === void 0 ? void 0 : _a.markers) || state.RangeSet.empty; }, |
|
initialSpacer() { |
|
return new FoldMarker(fullConfig, false); |
|
}, |
|
domEventHandlers: { |
|
...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 = view.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" |
|
} |
|
}); |
|
|
|
/** |
|
A highlight style associates CSS styles with highlighting |
|
[tags](https://lezer.codemirror.net/docs/ref#highlight.Tag). |
|
*/ |
|
class HighlightStyle { |
|
constructor( |
|
/** |
|
The tag styles used to create this highlight style. |
|
*/ |
|
specs, options) { |
|
this.specs = specs; |
|
let modSpec; |
|
function def(spec) { |
|
let cls = styleMod.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 = highlight.tagHighlighter(specs.map(style => ({ |
|
tag: style.tag, |
|
class: style.class || def(Object.assign({}, style, { tag: null })) |
|
})), { |
|
all, |
|
}).style; |
|
this.module = modSpec ? new styleMod.StyleModule(modSpec) : null; |
|
this.themeType = options.themeType; |
|
} |
|
/** |
|
Create a highlighter style that associates the given styles to |
|
the given tags. The specs must be objects that hold a style tag |
|
or array of tags in their `tag` property, and either a single |
|
`class` property providing a static CSS class (for highlighter |
|
that rely on external styling), or a |
|
[`style-mod`](https://github.com/marijnh/style-mod#documentation)-style |
|
set of CSS properties (which define the styling for those tags). |
|
|
|
The CSS rules created for a highlighter will be emitted in the |
|
order of the spec's properties. That means that for elements that |
|
have multiple tags associated with them, styles defined further |
|
down in the list will have a higher CSS precedence than styles |
|
defined earlier. |
|
*/ |
|
static define(specs, options) { |
|
return new HighlightStyle(specs, options || {}); |
|
} |
|
} |
|
const highlighterFacet = state.Facet.define(); |
|
const fallbackHighlighter = state.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); |
|
} |
|
/** |
|
Wrap a highlighter in an editor extension that uses it to apply |
|
syntax highlighting to the editor content. |
|
|
|
When multiple (non-fallback) styles are provided, the styling |
|
applied is the union of the classes they emit. |
|
*/ |
|
function syntaxHighlighting(highlighter, options) { |
|
let ext = [treeHighlighter], themeType; |
|
if (highlighter instanceof HighlightStyle) { |
|
if (highlighter.module) |
|
ext.push(view.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([view.EditorView.darkTheme], state => { |
|
return state.facet(view.EditorView.darkTheme) == (themeType == "dark") ? [highlighter] : []; |
|
})); |
|
else |
|
ext.push(highlighterFacet.of(highlighter)); |
|
return ext; |
|
} |
|
/** |
|
Returns the CSS classes (if any) that the highlighters active in |
|
the state would assign to the given style |
|
[tags](https://lezer.codemirror.net/docs/ref#highlight.Tag) and |
|
(optional) language |
|
[scope](https://codemirror.net/6/docs/ref/#language.HighlightStyle^define^options.scope). |
|
*/ |
|
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$1, highlighters) { |
|
if (!highlighters || !this.tree.length) |
|
return view.Decoration.none; |
|
let builder = new state.RangeSetBuilder(); |
|
for (let { from, to } of view$1.visibleRanges) { |
|
highlight.highlightTree(this.tree, highlighters, (from, to, style) => { |
|
builder.add(from, to, this.markCache[style] || (this.markCache[style] = view.Decoration.mark({ class: style }))); |
|
}, from, to); |
|
} |
|
return builder.finish(); |
|
} |
|
} |
|
const treeHighlighter = state.Prec.high(view.ViewPlugin.fromClass(TreeHighlighter, { |
|
decorations: v => v.decorations |
|
})); |
|
/** |
|
A default highlight style (works well with light themes). |
|
*/ |
|
const defaultHighlightStyle = HighlightStyle.define([ |
|
{ tag: highlight.tags.meta, |
|
color: "#404740" }, |
|
{ tag: highlight.tags.link, |
|
textDecoration: "underline" }, |
|
{ tag: highlight.tags.heading, |
|
textDecoration: "underline", |
|
fontWeight: "bold" }, |
|
{ tag: highlight.tags.emphasis, |
|
fontStyle: "italic" }, |
|
{ tag: highlight.tags.strong, |
|
fontWeight: "bold" }, |
|
{ tag: highlight.tags.strikethrough, |
|
textDecoration: "line-through" }, |
|
{ tag: highlight.tags.keyword, |
|
color: "#708" }, |
|
{ tag: [highlight.tags.atom, highlight.tags.bool, highlight.tags.url, highlight.tags.contentSeparator, highlight.tags.labelName], |
|
color: "#219" }, |
|
{ tag: [highlight.tags.literal, highlight.tags.inserted], |
|
color: "#164" }, |
|
{ tag: [highlight.tags.string, highlight.tags.deleted], |
|
color: "#a11" }, |
|
{ tag: [highlight.tags.regexp, highlight.tags.escape, highlight.tags.special(highlight.tags.string)], |
|
color: "#e40" }, |
|
{ tag: highlight.tags.definition(highlight.tags.variableName), |
|
color: "#00f" }, |
|
{ tag: highlight.tags.local(highlight.tags.variableName), |
|
color: "#30a" }, |
|
{ tag: [highlight.tags.typeName, highlight.tags.namespace], |
|
color: "#085" }, |
|
{ tag: highlight.tags.className, |
|
color: "#167" }, |
|
{ tag: [highlight.tags.special(highlight.tags.variableName), highlight.tags.macroName], |
|
color: "#256" }, |
|
{ tag: highlight.tags.definition(highlight.tags.propertyName), |
|
color: "#00c" }, |
|
{ tag: highlight.tags.comment, |
|
color: "#940" }, |
|
{ tag: highlight.tags.invalid, |
|
color: "#f00" } |
|
]); |
|
|
|
const baseTheme = view.EditorView.baseTheme({ |
|
"&.cm-focused .cm-matchingBracket": { backgroundColor: "#328c8252" }, |
|
"&.cm-focused .cm-nonmatchingBracket": { backgroundColor: "#bb555544" } |
|
}); |
|
const DefaultScanDist = 10000, DefaultBrackets = "()[]{}"; |
|
const bracketMatchingConfig = state.Facet.define({ |
|
combine(configs) { |
|
return state.combineConfig(configs, { |
|
afterCursor: true, |
|
brackets: DefaultBrackets, |
|
maxScanDistance: DefaultScanDist, |
|
renderMatch: defaultRenderMatch |
|
}); |
|
} |
|
}); |
|
const matchingMark = view.Decoration.mark({ class: "cm-matchingBracket" }), nonmatchingMark = view.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 = state.StateField.define({ |
|
create() { return view.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 view.Decoration.set(decorations, true); |
|
}, |
|
provide: f => view.EditorView.decorations.from(f) |
|
}); |
|
const bracketMatchingUnique = [ |
|
bracketMatchingState, |
|
baseTheme |
|
]; |
|
/** |
|
Create an extension that enables bracket matching. Whenever the |
|
cursor is next to a bracket, that bracket and the one it matches |
|
are highlighted. Or, when no matching bracket is found, another |
|
highlighting style is used to indicate this. |
|
*/ |
|
function bracketMatching(config = {}) { |
|
return [bracketMatchingConfig.of(config), bracketMatchingUnique]; |
|
} |
|
/** |
|
When larger syntax nodes, such as HTML tags, are marked as |
|
opening/closing, it can be a bit messy to treat the whole node as |
|
a matchable bracket. This node prop allows you to define, for such |
|
a node, a ‘handle’—the part of the node that is highlighted, and |
|
that the cursor must be on to activate highlighting in the first |
|
place. |
|
*/ |
|
const bracketMatchingHandle = new common.NodeProp(); |
|
function matchingNodes(node, dir, brackets) { |
|
let byProp = node.prop(dir < 0 ? common.NodeProp.openedBy : common.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; |
|
} |
|
/** |
|
Find the matching bracket for the token at `pos`, scanning |
|
direction `dir`. Only the `brackets` and `maxScanDistance` |
|
properties are used from `config`, if given. Returns null if no |
|
bracket was found at `pos`, or a match result otherwise. |
|
*/ |
|
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) { // Closing |
|
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; |
|
} |
|
|
|
// Counts the column offset in a string, taking tabs into account. |
|
// Used mostly to find indentation. |
|
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; |
|
} |
|
/** |
|
Encapsulates a single line of input. Given to stream syntax code, |
|
which uses it to tokenize the content. |
|
*/ |
|
class StringStream { |
|
/** |
|
Create a stream. |
|
*/ |
|
constructor( |
|
/** |
|
The line. |
|
*/ |
|
string, tabSize, |
|
/** |
|
The current indent unit size. |
|
*/ |
|
indentUnit, overrideIndent) { |
|
this.string = string; |
|
this.tabSize = tabSize; |
|
this.indentUnit = indentUnit; |
|
this.overrideIndent = overrideIndent; |
|
/** |
|
The current position on the line. |
|
*/ |
|
this.pos = 0; |
|
/** |
|
The start position of the current token. |
|
*/ |
|
this.start = 0; |
|
this.lastColumnPos = 0; |
|
this.lastColumnValue = 0; |
|
} |
|
/** |
|
True if we are at the end of the line. |
|
*/ |
|
eol() { return this.pos >= this.string.length; } |
|
/** |
|
True if we are at the start of the line. |
|
*/ |
|
sol() { return this.pos == 0; } |
|
/** |
|
Get the next code unit after the current position, or undefined |
|
if we're at the end of the line. |
|
*/ |
|
peek() { return this.string.charAt(this.pos) || undefined; } |
|
/** |
|
Read the next code unit and advance `this.pos`. |
|
*/ |
|
next() { |
|
if (this.pos < this.string.length) |
|
return this.string.charAt(this.pos++); |
|
} |
|
/** |
|
Match the next character against the given string, regular |
|
expression, or predicate. Consume and return it if it matches. |
|
*/ |
|
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; |
|
} |
|
} |
|
/** |
|
Continue matching characters that match the given string, |
|
regular expression, or predicate function. Return true if any |
|
characters were consumed. |
|
*/ |
|
eatWhile(match) { |
|
let start = this.pos; |
|
while (this.eat(match)) { } |
|
return this.pos > start; |
|
} |
|
/** |
|
Consume whitespace ahead of `this.pos`. Return true if any was |
|
found. |
|
*/ |
|
eatSpace() { |
|
let start = this.pos; |
|
while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) |
|
++this.pos; |
|
return this.pos > start; |
|
} |
|
/** |
|
Move to the end of the line. |
|
*/ |
|
skipToEnd() { this.pos = this.string.length; } |
|
/** |
|
Move to directly before the given character, if found on the |
|
current line. |
|
*/ |
|
skipTo(ch) { |
|
let found = this.string.indexOf(ch, this.pos); |
|
if (found > -1) { |
|
this.pos = found; |
|
return true; |
|
} |
|
} |
|
/** |
|
Move back `n` characters. |
|
*/ |
|
backUp(n) { this.pos -= n; } |
|
/** |
|
Get the column position at `this.pos`. |
|
*/ |
|
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; |
|
} |
|
/** |
|
Get the indentation column of the current line. |
|
*/ |
|
indentation() { |
|
var _a; |
|
return (_a = this.overrideIndent) !== null && _a !== void 0 ? _a : countCol(this.string, null, this.tabSize); |
|
} |
|
/** |
|
Match the input against the given string or regular expression |
|
(which should start with a `^`). Return true or the regexp match |
|
if it matches. |
|
|
|
Unless `consume` is set to `false`, this will move `this.pos` |
|
past the matched text. |
|
|
|
When matching a string `caseInsensitive` can be set to true to |
|
make the match case-insensitive. |
|
*/ |
|
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; |
|
} |
|
} |
|
/** |
|
Get the current token. |
|
*/ |
|
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, |
|
mergeTokens: spec.mergeTokens !== false |
|
}; |
|
} |
|
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(); |
|
/** |
|
A [language](https://codemirror.net/6/docs/ref/#language.Language) class based on a CodeMirror |
|
5-style [streaming parser](https://codemirror.net/6/docs/ref/#language.StreamParser). |
|
*/ |
|
class StreamLanguage extends Language { |
|
constructor(parser) { |
|
let data = defineLanguageFacet(parser.languageData); |
|
let p = fullParser(parser), self; |
|
let impl = new class extends common.Parser { |
|
createParse(input, fragments, ranges) { |
|
return new Parse(self, input, fragments, ranges); |
|
} |
|
}; |
|
super(data, impl, [], parser.name); |
|
this.topNode = docID(data, this); |
|
self = this; |
|
this.streamParser = p; |
|
this.stateAfter = new common.NodeProp({ perNode: true }); |
|
this.tokenTable = parser.tokenTable ? new TokenTable(p.tokenTable) : defaultTokenTable; |
|
} |
|
/** |
|
Define a stream language. |
|
*/ |
|
static define(spec) { return new StreamLanguage(spec); } |
|
/** |
|
@internal |
|
*/ |
|
getIndent(cx) { |
|
let from = undefined; |
|
let { overrideIndentation } = cx.options; |
|
if (overrideIndentation) { |
|
from = IndentedFrom.get(cx.state); |
|
if (from != null && from < cx.pos - 1e4) |
|
from = undefined; |
|
} |
|
let start = findState(this, cx.node.tree, cx.node.from, cx.node.from, from !== null && from !== void 0 ? from : cx.pos), statePos, state; |
|
if (start) { |
|
state = start.state; |
|
statePos = start.pos + 1; |
|
} |
|
else { |
|
state = this.streamParser.startState(cx.unit); |
|
statePos = cx.node.from; |
|
} |
|
if (cx.pos - statePos > 10000 /* C.MaxIndentScanDist */) |
|
return null; |
|
while (statePos < cx.pos) { |
|
let line = cx.state.doc.lineAt(statePos), end = Math.min(cx.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 == cx.pos) |
|
break; |
|
statePos = line.to + 1; |
|
} |
|
let line = cx.lineAt(cx.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 common.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 && from == 0 && 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 common.Tree) { |
|
if (!(inner = cutTree(lang, child, from - pos, to - pos, inside))) |
|
break; |
|
return !inside ? inner |
|
: new common.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, endPos, 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 && found.pos <= endPos && (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: common.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, this.to, 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 /* C.MaxDistanceBeforeViewport */ && |
|
ranges.some(r => r.from <= context.viewport.from && r.to >= context.viewport.from)) { |
|
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 + 512 /* C.ChunkSize */); |
|
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, offset) { |
|
let size = 4; |
|
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; |
|
} |
|
let last = this.chunk.length - 4; |
|
if (this.lang.streamParser.mergeTokens && size == 4 && last >= 0 && |
|
this.chunk[last] == id && this.chunk[last + 2] == from) |
|
this.chunk[last + 2] = to; |
|
else |
|
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, offset); |
|
if (stream.start > 10000 /* C.MaxLineLength */) |
|
break; |
|
} |
|
} |
|
this.parsedPos = end; |
|
this.moveRangeIndex(); |
|
if (this.parsedPos < this.to) |
|
this.parsedPos++; |
|
} |
|
finishChunk() { |
|
let tree = common.Tree.build({ |
|
buffer: this.chunk, |
|
start: this.chunkStart, |
|
length: this.parsedPos - this.chunkStart, |
|
nodeSet, |
|
topID: 0, |
|
maxBufferLength: 512 /* C.ChunkSize */, |
|
reused: this.chunkReused |
|
}); |
|
tree = new common.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 common.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 = [common.NodeType.none]; |
|
const nodeSet = new common.NodeSet(typeArray); |
|
const warned = []; |
|
// Cache of node types by name and tags |
|
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 = []; |
|
for (let name of tagStr.split(" ")) { |
|
let found = []; |
|
for (let part of name.split(".")) { |
|
let value = (extra[part] || highlight.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.push(tag); |
|
} |
|
if (!tags.length) |
|
return 0; |
|
let name = tagStr.replace(/ /g, "_"), key = name + " " + tags.map(t => t.id); |
|
let known = byTag[key]; |
|
if (known) |
|
return known.id; |
|
let type = byTag[key] = common.NodeType.define({ |
|
id: typeArray.length, |
|
name, |
|
props: [highlight.styleTags({ [name]: tags })] |
|
}); |
|
typeArray.push(type); |
|
return type.id; |
|
} |
|
function docID(data, lang) { |
|
let type = common.NodeType.define({ id: typeArray.length, name: "Document", props: [ |
|
languageDataProp.add(() => data), |
|
indentNodeProp.add(() => cx => lang.getIndent(cx)) |
|
], 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 = state.Facet.define({ combine: values => values.some(x => x) }); |
|
/** |
|
Make sure nodes |
|
[marked](https://lezer.codemirror.net/docs/ref/#common.NodeProp^isolate) |
|
as isolating for bidirectional text are rendered in a way that |
|
isolates them from the surrounding text. |
|
*/ |
|
function bidiIsolates(options = {}) { |
|
let extensions = [isolateMarks]; |
|
if (options.alwaysIsolate) |
|
extensions.push(alwaysIsolate.of(true)); |
|
return extensions; |
|
} |
|
const isolateMarks = view.ViewPlugin.fromClass(class { |
|
constructor(view$1) { |
|
this.always = view$1.state.facet(alwaysIsolate) || |
|
view$1.textDirection != view.Direction.LTR || |
|
view$1.state.facet(view.EditorView.perLineTextDirection); |
|
this.hasRTL = !this.always && textHasRTL(view$1.state.doc); |
|
this.tree = syntaxTree(view$1.state); |
|
this.decorations = this.always || this.hasRTL ? buildDeco(view$1, this.tree, this.always) : view.Decoration.none; |
|
} |
|
update(update) { |
|
let always = update.state.facet(alwaysIsolate) || |
|
update.view.textDirection != view.Direction.LTR || |
|
update.state.facet(view.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$1) { |
|
var _a, _b; |
|
return (_b = (_a = view$1.plugin(plugin)) === null || _a === void 0 ? void 0 : _a.decorations) !== null && _b !== void 0 ? _b : view.Decoration.none; |
|
} |
|
return [view.EditorView.outerDecorations.of(access), |
|
state.Prec.lowest(view.EditorView.bidiIsolatedRanges.of(access))]; |
|
} |
|
}); |
|
function buildDeco(view, tree, always) { |
|
let deco = new state.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(common.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: view.Decoration.mark({ class: "cm-iso", inclusive: true, attributes: { dir: "rtl" }, bidiIsolate: view.Direction.RTL }), |
|
ltr: view.Decoration.mark({ class: "cm-iso", inclusive: true, attributes: { dir: "ltr" }, bidiIsolate: view.Direction.LTR }), |
|
auto: view.Decoration.mark({ class: "cm-iso", inclusive: true, attributes: { dir: "auto" }, bidiIsolate: null }) |
|
}; |
|
|
|
exports.DocInput = DocInput; |
|
exports.HighlightStyle = HighlightStyle; |
|
exports.IndentContext = IndentContext; |
|
exports.LRLanguage = LRLanguage; |
|
exports.Language = Language; |
|
exports.LanguageDescription = LanguageDescription; |
|
exports.LanguageSupport = LanguageSupport; |
|
exports.ParseContext = ParseContext; |
|
exports.StreamLanguage = StreamLanguage; |
|
exports.StringStream = StringStream; |
|
exports.TreeIndentContext = TreeIndentContext; |
|
exports.bidiIsolates = bidiIsolates; |
|
exports.bracketMatching = bracketMatching; |
|
exports.bracketMatchingHandle = bracketMatchingHandle; |
|
exports.codeFolding = codeFolding; |
|
exports.continuedIndent = continuedIndent; |
|
exports.defaultHighlightStyle = defaultHighlightStyle; |
|
exports.defineLanguageFacet = defineLanguageFacet; |
|
exports.delimitedIndent = delimitedIndent; |
|
exports.ensureSyntaxTree = ensureSyntaxTree; |
|
exports.flatIndent = flatIndent; |
|
exports.foldAll = foldAll; |
|
exports.foldCode = foldCode; |
|
exports.foldEffect = foldEffect; |
|
exports.foldGutter = foldGutter; |
|
exports.foldInside = foldInside; |
|
exports.foldKeymap = foldKeymap; |
|
exports.foldNodeProp = foldNodeProp; |
|
exports.foldService = foldService; |
|
exports.foldState = foldState; |
|
exports.foldable = foldable; |
|
exports.foldedRanges = foldedRanges; |
|
exports.forceParsing = forceParsing; |
|
exports.getIndentUnit = getIndentUnit; |
|
exports.getIndentation = getIndentation; |
|
exports.highlightingFor = highlightingFor; |
|
exports.indentNodeProp = indentNodeProp; |
|
exports.indentOnInput = indentOnInput; |
|
exports.indentRange = indentRange; |
|
exports.indentService = indentService; |
|
exports.indentString = indentString; |
|
exports.indentUnit = indentUnit; |
|
exports.language = language; |
|
exports.languageDataProp = languageDataProp; |
|
exports.matchBrackets = matchBrackets; |
|
exports.sublanguageProp = sublanguageProp; |
|
exports.syntaxHighlighting = syntaxHighlighting; |
|
exports.syntaxParserRunning = syntaxParserRunning; |
|
exports.syntaxTree = syntaxTree; |
|
exports.syntaxTreeAvailable = syntaxTreeAvailable; |
|
exports.toggleFold = toggleFold; |
|
exports.unfoldAll = unfoldAll; |
|
exports.unfoldCode = unfoldCode; |
|
exports.unfoldEffect = unfoldEffect;
|
|
|