mirror of
https://github.com/DanielnetoDotCom/YouPHPTube
synced 2025-10-04 10:19:24 +02:00
1675 lines
76 KiB
JavaScript
1675 lines
76 KiB
JavaScript
import { Annotation, Facet, combineConfig, StateField, Transaction, ChangeSet, ChangeDesc, EditorSelection, StateEffect, Text, findClusterBreak, countColumn, CharCategory } from '@codemirror/state';
|
|
import { EditorView, Direction } from '@codemirror/view';
|
|
import { IndentContext, getIndentation, indentString, matchBrackets, syntaxTree, getIndentUnit, indentUnit } from '@codemirror/language';
|
|
import { NodeProp } from '@lezer/common';
|
|
|
|
/**
|
|
Comment or uncomment the current selection. Will use line comments
|
|
if available, otherwise falling back to block comments.
|
|
*/
|
|
const toggleComment = target => {
|
|
let { state } = target, line = state.doc.lineAt(state.selection.main.from), config = getConfig(target.state, line.from);
|
|
return config.line ? toggleLineComment(target) : config.block ? toggleBlockCommentByLine(target) : false;
|
|
};
|
|
function command(f, option) {
|
|
return ({ state, dispatch }) => {
|
|
if (state.readOnly)
|
|
return false;
|
|
let tr = f(option, state);
|
|
if (!tr)
|
|
return false;
|
|
dispatch(state.update(tr));
|
|
return true;
|
|
};
|
|
}
|
|
/**
|
|
Comment or uncomment the current selection using line comments.
|
|
The line comment syntax is taken from the
|
|
[`commentTokens`](https://codemirror.net/6/docs/ref/#commands.CommentTokens) [language
|
|
data](https://codemirror.net/6/docs/ref/#state.EditorState.languageDataAt).
|
|
*/
|
|
const toggleLineComment = /*@__PURE__*/command(changeLineComment, 0 /* CommentOption.Toggle */);
|
|
/**
|
|
Comment the current selection using line comments.
|
|
*/
|
|
const lineComment = /*@__PURE__*/command(changeLineComment, 1 /* CommentOption.Comment */);
|
|
/**
|
|
Uncomment the current selection using line comments.
|
|
*/
|
|
const lineUncomment = /*@__PURE__*/command(changeLineComment, 2 /* CommentOption.Uncomment */);
|
|
/**
|
|
Comment or uncomment the current selection using block comments.
|
|
The block comment syntax is taken from the
|
|
[`commentTokens`](https://codemirror.net/6/docs/ref/#commands.CommentTokens) [language
|
|
data](https://codemirror.net/6/docs/ref/#state.EditorState.languageDataAt).
|
|
*/
|
|
const toggleBlockComment = /*@__PURE__*/command(changeBlockComment, 0 /* CommentOption.Toggle */);
|
|
/**
|
|
Comment the current selection using block comments.
|
|
*/
|
|
const blockComment = /*@__PURE__*/command(changeBlockComment, 1 /* CommentOption.Comment */);
|
|
/**
|
|
Uncomment the current selection using block comments.
|
|
*/
|
|
const blockUncomment = /*@__PURE__*/command(changeBlockComment, 2 /* CommentOption.Uncomment */);
|
|
/**
|
|
Comment or uncomment the lines around the current selection using
|
|
block comments.
|
|
*/
|
|
const toggleBlockCommentByLine = /*@__PURE__*/command((o, s) => changeBlockComment(o, s, selectedLineRanges(s)), 0 /* CommentOption.Toggle */);
|
|
function getConfig(state, pos) {
|
|
let data = state.languageDataAt("commentTokens", pos);
|
|
return data.length ? data[0] : {};
|
|
}
|
|
const SearchMargin = 50;
|
|
/**
|
|
Determines if the given range is block-commented in the given
|
|
state.
|
|
*/
|
|
function findBlockComment(state, { open, close }, from, to) {
|
|
let textBefore = state.sliceDoc(from - SearchMargin, from);
|
|
let textAfter = state.sliceDoc(to, to + SearchMargin);
|
|
let spaceBefore = /\s*$/.exec(textBefore)[0].length, spaceAfter = /^\s*/.exec(textAfter)[0].length;
|
|
let beforeOff = textBefore.length - spaceBefore;
|
|
if (textBefore.slice(beforeOff - open.length, beforeOff) == open &&
|
|
textAfter.slice(spaceAfter, spaceAfter + close.length) == close) {
|
|
return { open: { pos: from - spaceBefore, margin: spaceBefore && 1 },
|
|
close: { pos: to + spaceAfter, margin: spaceAfter && 1 } };
|
|
}
|
|
let startText, endText;
|
|
if (to - from <= 2 * SearchMargin) {
|
|
startText = endText = state.sliceDoc(from, to);
|
|
}
|
|
else {
|
|
startText = state.sliceDoc(from, from + SearchMargin);
|
|
endText = state.sliceDoc(to - SearchMargin, to);
|
|
}
|
|
let startSpace = /^\s*/.exec(startText)[0].length, endSpace = /\s*$/.exec(endText)[0].length;
|
|
let endOff = endText.length - endSpace - close.length;
|
|
if (startText.slice(startSpace, startSpace + open.length) == open &&
|
|
endText.slice(endOff, endOff + close.length) == close) {
|
|
return { open: { pos: from + startSpace + open.length,
|
|
margin: /\s/.test(startText.charAt(startSpace + open.length)) ? 1 : 0 },
|
|
close: { pos: to - endSpace - close.length,
|
|
margin: /\s/.test(endText.charAt(endOff - 1)) ? 1 : 0 } };
|
|
}
|
|
return null;
|
|
}
|
|
function selectedLineRanges(state) {
|
|
let ranges = [];
|
|
for (let r of state.selection.ranges) {
|
|
let fromLine = state.doc.lineAt(r.from);
|
|
let toLine = r.to <= fromLine.to ? fromLine : state.doc.lineAt(r.to);
|
|
let last = ranges.length - 1;
|
|
if (last >= 0 && ranges[last].to > fromLine.from)
|
|
ranges[last].to = toLine.to;
|
|
else
|
|
ranges.push({ from: fromLine.from + /^\s*/.exec(fromLine.text)[0].length, to: toLine.to });
|
|
}
|
|
return ranges;
|
|
}
|
|
// Performs toggle, comment and uncomment of block comments in
|
|
// languages that support them.
|
|
function changeBlockComment(option, state, ranges = state.selection.ranges) {
|
|
let tokens = ranges.map(r => getConfig(state, r.from).block);
|
|
if (!tokens.every(c => c))
|
|
return null;
|
|
let comments = ranges.map((r, i) => findBlockComment(state, tokens[i], r.from, r.to));
|
|
if (option != 2 /* CommentOption.Uncomment */ && !comments.every(c => c)) {
|
|
return { changes: state.changes(ranges.map((range, i) => {
|
|
if (comments[i])
|
|
return [];
|
|
return [{ from: range.from, insert: tokens[i].open + " " }, { from: range.to, insert: " " + tokens[i].close }];
|
|
})) };
|
|
}
|
|
else if (option != 1 /* CommentOption.Comment */ && comments.some(c => c)) {
|
|
let changes = [];
|
|
for (let i = 0, comment; i < comments.length; i++)
|
|
if (comment = comments[i]) {
|
|
let token = tokens[i], { open, close } = comment;
|
|
changes.push({ from: open.pos - token.open.length, to: open.pos + open.margin }, { from: close.pos - close.margin, to: close.pos + token.close.length });
|
|
}
|
|
return { changes };
|
|
}
|
|
return null;
|
|
}
|
|
// Performs toggle, comment and uncomment of line comments.
|
|
function changeLineComment(option, state, ranges = state.selection.ranges) {
|
|
let lines = [];
|
|
let prevLine = -1;
|
|
for (let { from, to } of ranges) {
|
|
let startI = lines.length, minIndent = 1e9;
|
|
let token = getConfig(state, from).line;
|
|
if (!token)
|
|
continue;
|
|
for (let pos = from; pos <= to;) {
|
|
let line = state.doc.lineAt(pos);
|
|
if (line.from > prevLine && (from == to || to > line.from)) {
|
|
prevLine = line.from;
|
|
let indent = /^\s*/.exec(line.text)[0].length;
|
|
let empty = indent == line.length;
|
|
let comment = line.text.slice(indent, indent + token.length) == token ? indent : -1;
|
|
if (indent < line.text.length && indent < minIndent)
|
|
minIndent = indent;
|
|
lines.push({ line, comment, token, indent, empty, single: false });
|
|
}
|
|
pos = line.to + 1;
|
|
}
|
|
if (minIndent < 1e9)
|
|
for (let i = startI; i < lines.length; i++)
|
|
if (lines[i].indent < lines[i].line.text.length)
|
|
lines[i].indent = minIndent;
|
|
if (lines.length == startI + 1)
|
|
lines[startI].single = true;
|
|
}
|
|
if (option != 2 /* CommentOption.Uncomment */ && lines.some(l => l.comment < 0 && (!l.empty || l.single))) {
|
|
let changes = [];
|
|
for (let { line, token, indent, empty, single } of lines)
|
|
if (single || !empty)
|
|
changes.push({ from: line.from + indent, insert: token + " " });
|
|
let changeSet = state.changes(changes);
|
|
return { changes: changeSet, selection: state.selection.map(changeSet, 1) };
|
|
}
|
|
else if (option != 1 /* CommentOption.Comment */ && lines.some(l => l.comment >= 0)) {
|
|
let changes = [];
|
|
for (let { line, comment, token } of lines)
|
|
if (comment >= 0) {
|
|
let from = line.from + comment, to = from + token.length;
|
|
if (line.text[to - line.from] == " ")
|
|
to++;
|
|
changes.push({ from, to });
|
|
}
|
|
return { changes };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const fromHistory = /*@__PURE__*/Annotation.define();
|
|
/**
|
|
Transaction annotation that will prevent that transaction from
|
|
being combined with other transactions in the undo history. Given
|
|
`"before"`, it'll prevent merging with previous transactions. With
|
|
`"after"`, subsequent transactions won't be combined with this
|
|
one. With `"full"`, the transaction is isolated on both sides.
|
|
*/
|
|
const isolateHistory = /*@__PURE__*/Annotation.define();
|
|
/**
|
|
This facet provides a way to register functions that, given a
|
|
transaction, provide a set of effects that the history should
|
|
store when inverting the transaction. This can be used to
|
|
integrate some kinds of effects in the history, so that they can
|
|
be undone (and redone again).
|
|
*/
|
|
const invertedEffects = /*@__PURE__*/Facet.define();
|
|
const historyConfig = /*@__PURE__*/Facet.define({
|
|
combine(configs) {
|
|
return combineConfig(configs, {
|
|
minDepth: 100,
|
|
newGroupDelay: 500,
|
|
joinToEvent: (_t, isAdjacent) => isAdjacent,
|
|
}, {
|
|
minDepth: Math.max,
|
|
newGroupDelay: Math.min,
|
|
joinToEvent: (a, b) => (tr, adj) => a(tr, adj) || b(tr, adj)
|
|
});
|
|
}
|
|
});
|
|
const historyField_ = /*@__PURE__*/StateField.define({
|
|
create() {
|
|
return HistoryState.empty;
|
|
},
|
|
update(state, tr) {
|
|
let config = tr.state.facet(historyConfig);
|
|
let fromHist = tr.annotation(fromHistory);
|
|
if (fromHist) {
|
|
let item = HistEvent.fromTransaction(tr, fromHist.selection), from = fromHist.side;
|
|
let other = from == 0 /* BranchName.Done */ ? state.undone : state.done;
|
|
if (item)
|
|
other = updateBranch(other, other.length, config.minDepth, item);
|
|
else
|
|
other = addSelection(other, tr.startState.selection);
|
|
return new HistoryState(from == 0 /* BranchName.Done */ ? fromHist.rest : other, from == 0 /* BranchName.Done */ ? other : fromHist.rest);
|
|
}
|
|
let isolate = tr.annotation(isolateHistory);
|
|
if (isolate == "full" || isolate == "before")
|
|
state = state.isolate();
|
|
if (tr.annotation(Transaction.addToHistory) === false)
|
|
return !tr.changes.empty ? state.addMapping(tr.changes.desc) : state;
|
|
let event = HistEvent.fromTransaction(tr);
|
|
let time = tr.annotation(Transaction.time), userEvent = tr.annotation(Transaction.userEvent);
|
|
if (event)
|
|
state = state.addChanges(event, time, userEvent, config, tr);
|
|
else if (tr.selection)
|
|
state = state.addSelection(tr.startState.selection, time, userEvent, config.newGroupDelay);
|
|
if (isolate == "full" || isolate == "after")
|
|
state = state.isolate();
|
|
return state;
|
|
},
|
|
toJSON(value) {
|
|
return { done: value.done.map(e => e.toJSON()), undone: value.undone.map(e => e.toJSON()) };
|
|
},
|
|
fromJSON(json) {
|
|
return new HistoryState(json.done.map(HistEvent.fromJSON), json.undone.map(HistEvent.fromJSON));
|
|
}
|
|
});
|
|
/**
|
|
Create a history extension with the given configuration.
|
|
*/
|
|
function history(config = {}) {
|
|
return [
|
|
historyField_,
|
|
historyConfig.of(config),
|
|
EditorView.domEventHandlers({
|
|
beforeinput(e, view) {
|
|
let command = e.inputType == "historyUndo" ? undo : e.inputType == "historyRedo" ? redo : null;
|
|
if (!command)
|
|
return false;
|
|
e.preventDefault();
|
|
return command(view);
|
|
}
|
|
})
|
|
];
|
|
}
|
|
/**
|
|
The state field used to store the history data. Should probably
|
|
only be used when you want to
|
|
[serialize](https://codemirror.net/6/docs/ref/#state.EditorState.toJSON) or
|
|
[deserialize](https://codemirror.net/6/docs/ref/#state.EditorState^fromJSON) state objects in a way
|
|
that preserves history.
|
|
*/
|
|
const historyField = historyField_;
|
|
function cmd(side, selection) {
|
|
return function ({ state, dispatch }) {
|
|
if (!selection && state.readOnly)
|
|
return false;
|
|
let historyState = state.field(historyField_, false);
|
|
if (!historyState)
|
|
return false;
|
|
let tr = historyState.pop(side, state, selection);
|
|
if (!tr)
|
|
return false;
|
|
dispatch(tr);
|
|
return true;
|
|
};
|
|
}
|
|
/**
|
|
Undo a single group of history events. Returns false if no group
|
|
was available.
|
|
*/
|
|
const undo = /*@__PURE__*/cmd(0 /* BranchName.Done */, false);
|
|
/**
|
|
Redo a group of history events. Returns false if no group was
|
|
available.
|
|
*/
|
|
const redo = /*@__PURE__*/cmd(1 /* BranchName.Undone */, false);
|
|
/**
|
|
Undo a change or selection change.
|
|
*/
|
|
const undoSelection = /*@__PURE__*/cmd(0 /* BranchName.Done */, true);
|
|
/**
|
|
Redo a change or selection change.
|
|
*/
|
|
const redoSelection = /*@__PURE__*/cmd(1 /* BranchName.Undone */, true);
|
|
function depth(side) {
|
|
return function (state) {
|
|
let histState = state.field(historyField_, false);
|
|
if (!histState)
|
|
return 0;
|
|
let branch = side == 0 /* BranchName.Done */ ? histState.done : histState.undone;
|
|
return branch.length - (branch.length && !branch[0].changes ? 1 : 0);
|
|
};
|
|
}
|
|
/**
|
|
The amount of undoable change events available in a given state.
|
|
*/
|
|
const undoDepth = /*@__PURE__*/depth(0 /* BranchName.Done */);
|
|
/**
|
|
The amount of redoable change events available in a given state.
|
|
*/
|
|
const redoDepth = /*@__PURE__*/depth(1 /* BranchName.Undone */);
|
|
// History events store groups of changes or effects that need to be
|
|
// undone/redone together.
|
|
class HistEvent {
|
|
constructor(
|
|
// The changes in this event. Normal events hold at least one
|
|
// change or effect. But it may be necessary to store selection
|
|
// events before the first change, in which case a special type of
|
|
// instance is created which doesn't hold any changes, with
|
|
// changes == startSelection == undefined
|
|
changes,
|
|
// The effects associated with this event
|
|
effects,
|
|
// Accumulated mapping (from addToHistory==false) that should be
|
|
// applied to events below this one.
|
|
mapped,
|
|
// The selection before this event
|
|
startSelection,
|
|
// Stores selection changes after this event, to be used for
|
|
// selection undo/redo.
|
|
selectionsAfter) {
|
|
this.changes = changes;
|
|
this.effects = effects;
|
|
this.mapped = mapped;
|
|
this.startSelection = startSelection;
|
|
this.selectionsAfter = selectionsAfter;
|
|
}
|
|
setSelAfter(after) {
|
|
return new HistEvent(this.changes, this.effects, this.mapped, this.startSelection, after);
|
|
}
|
|
toJSON() {
|
|
var _a, _b, _c;
|
|
return {
|
|
changes: (_a = this.changes) === null || _a === void 0 ? void 0 : _a.toJSON(),
|
|
mapped: (_b = this.mapped) === null || _b === void 0 ? void 0 : _b.toJSON(),
|
|
startSelection: (_c = this.startSelection) === null || _c === void 0 ? void 0 : _c.toJSON(),
|
|
selectionsAfter: this.selectionsAfter.map(s => s.toJSON())
|
|
};
|
|
}
|
|
static fromJSON(json) {
|
|
return new HistEvent(json.changes && ChangeSet.fromJSON(json.changes), [], json.mapped && ChangeDesc.fromJSON(json.mapped), json.startSelection && EditorSelection.fromJSON(json.startSelection), json.selectionsAfter.map(EditorSelection.fromJSON));
|
|
}
|
|
// This does not check `addToHistory` and such, it assumes the
|
|
// transaction needs to be converted to an item. Returns null when
|
|
// there are no changes or effects in the transaction.
|
|
static fromTransaction(tr, selection) {
|
|
let effects = none;
|
|
for (let invert of tr.startState.facet(invertedEffects)) {
|
|
let result = invert(tr);
|
|
if (result.length)
|
|
effects = effects.concat(result);
|
|
}
|
|
if (!effects.length && tr.changes.empty)
|
|
return null;
|
|
return new HistEvent(tr.changes.invert(tr.startState.doc), effects, undefined, selection || tr.startState.selection, none);
|
|
}
|
|
static selection(selections) {
|
|
return new HistEvent(undefined, none, undefined, undefined, selections);
|
|
}
|
|
}
|
|
function updateBranch(branch, to, maxLen, newEvent) {
|
|
let start = to + 1 > maxLen + 20 ? to - maxLen - 1 : 0;
|
|
let newBranch = branch.slice(start, to);
|
|
newBranch.push(newEvent);
|
|
return newBranch;
|
|
}
|
|
function isAdjacent(a, b) {
|
|
let ranges = [], isAdjacent = false;
|
|
a.iterChangedRanges((f, t) => ranges.push(f, t));
|
|
b.iterChangedRanges((_f, _t, f, t) => {
|
|
for (let i = 0; i < ranges.length;) {
|
|
let from = ranges[i++], to = ranges[i++];
|
|
if (t >= from && f <= to)
|
|
isAdjacent = true;
|
|
}
|
|
});
|
|
return isAdjacent;
|
|
}
|
|
function eqSelectionShape(a, b) {
|
|
return a.ranges.length == b.ranges.length &&
|
|
a.ranges.filter((r, i) => r.empty != b.ranges[i].empty).length === 0;
|
|
}
|
|
function conc(a, b) {
|
|
return !a.length ? b : !b.length ? a : a.concat(b);
|
|
}
|
|
const none = [];
|
|
const MaxSelectionsPerEvent = 200;
|
|
function addSelection(branch, selection) {
|
|
if (!branch.length) {
|
|
return [HistEvent.selection([selection])];
|
|
}
|
|
else {
|
|
let lastEvent = branch[branch.length - 1];
|
|
let sels = lastEvent.selectionsAfter.slice(Math.max(0, lastEvent.selectionsAfter.length - MaxSelectionsPerEvent));
|
|
if (sels.length && sels[sels.length - 1].eq(selection))
|
|
return branch;
|
|
sels.push(selection);
|
|
return updateBranch(branch, branch.length - 1, 1e9, lastEvent.setSelAfter(sels));
|
|
}
|
|
}
|
|
// Assumes the top item has one or more selectionAfter values
|
|
function popSelection(branch) {
|
|
let last = branch[branch.length - 1];
|
|
let newBranch = branch.slice();
|
|
newBranch[branch.length - 1] = last.setSelAfter(last.selectionsAfter.slice(0, last.selectionsAfter.length - 1));
|
|
return newBranch;
|
|
}
|
|
// Add a mapping to the top event in the given branch. If this maps
|
|
// away all the changes and effects in that item, drop it and
|
|
// propagate the mapping to the next item.
|
|
function addMappingToBranch(branch, mapping) {
|
|
if (!branch.length)
|
|
return branch;
|
|
let length = branch.length, selections = none;
|
|
while (length) {
|
|
let event = mapEvent(branch[length - 1], mapping, selections);
|
|
if (event.changes && !event.changes.empty || event.effects.length) { // Event survived mapping
|
|
let result = branch.slice(0, length);
|
|
result[length - 1] = event;
|
|
return result;
|
|
}
|
|
else { // Drop this event, since there's no changes or effects left
|
|
mapping = event.mapped;
|
|
length--;
|
|
selections = event.selectionsAfter;
|
|
}
|
|
}
|
|
return selections.length ? [HistEvent.selection(selections)] : none;
|
|
}
|
|
function mapEvent(event, mapping, extraSelections) {
|
|
let selections = conc(event.selectionsAfter.length ? event.selectionsAfter.map(s => s.map(mapping)) : none, extraSelections);
|
|
// Change-less events don't store mappings (they are always the last event in a branch)
|
|
if (!event.changes)
|
|
return HistEvent.selection(selections);
|
|
let mappedChanges = event.changes.map(mapping), before = mapping.mapDesc(event.changes, true);
|
|
let fullMapping = event.mapped ? event.mapped.composeDesc(before) : before;
|
|
return new HistEvent(mappedChanges, StateEffect.mapEffects(event.effects, mapping), fullMapping, event.startSelection.map(before), selections);
|
|
}
|
|
const joinableUserEvent = /^(input\.type|delete)($|\.)/;
|
|
class HistoryState {
|
|
constructor(done, undone, prevTime = 0, prevUserEvent = undefined) {
|
|
this.done = done;
|
|
this.undone = undone;
|
|
this.prevTime = prevTime;
|
|
this.prevUserEvent = prevUserEvent;
|
|
}
|
|
isolate() {
|
|
return this.prevTime ? new HistoryState(this.done, this.undone) : this;
|
|
}
|
|
addChanges(event, time, userEvent, config, tr) {
|
|
let done = this.done, lastEvent = done[done.length - 1];
|
|
if (lastEvent && lastEvent.changes && !lastEvent.changes.empty && event.changes &&
|
|
(!userEvent || joinableUserEvent.test(userEvent)) &&
|
|
((!lastEvent.selectionsAfter.length &&
|
|
time - this.prevTime < config.newGroupDelay &&
|
|
config.joinToEvent(tr, isAdjacent(lastEvent.changes, event.changes))) ||
|
|
// For compose (but not compose.start) events, always join with previous event
|
|
userEvent == "input.type.compose")) {
|
|
done = updateBranch(done, done.length - 1, config.minDepth, new HistEvent(event.changes.compose(lastEvent.changes), conc(event.effects, lastEvent.effects), lastEvent.mapped, lastEvent.startSelection, none));
|
|
}
|
|
else {
|
|
done = updateBranch(done, done.length, config.minDepth, event);
|
|
}
|
|
return new HistoryState(done, none, time, userEvent);
|
|
}
|
|
addSelection(selection, time, userEvent, newGroupDelay) {
|
|
let last = this.done.length ? this.done[this.done.length - 1].selectionsAfter : none;
|
|
if (last.length > 0 &&
|
|
time - this.prevTime < newGroupDelay &&
|
|
userEvent == this.prevUserEvent && userEvent && /^select($|\.)/.test(userEvent) &&
|
|
eqSelectionShape(last[last.length - 1], selection))
|
|
return this;
|
|
return new HistoryState(addSelection(this.done, selection), this.undone, time, userEvent);
|
|
}
|
|
addMapping(mapping) {
|
|
return new HistoryState(addMappingToBranch(this.done, mapping), addMappingToBranch(this.undone, mapping), this.prevTime, this.prevUserEvent);
|
|
}
|
|
pop(side, state, onlySelection) {
|
|
let branch = side == 0 /* BranchName.Done */ ? this.done : this.undone;
|
|
if (branch.length == 0)
|
|
return null;
|
|
let event = branch[branch.length - 1], selection = event.selectionsAfter[0] || state.selection;
|
|
if (onlySelection && event.selectionsAfter.length) {
|
|
return state.update({
|
|
selection: event.selectionsAfter[event.selectionsAfter.length - 1],
|
|
annotations: fromHistory.of({ side, rest: popSelection(branch), selection }),
|
|
userEvent: side == 0 /* BranchName.Done */ ? "select.undo" : "select.redo",
|
|
scrollIntoView: true
|
|
});
|
|
}
|
|
else if (!event.changes) {
|
|
return null;
|
|
}
|
|
else {
|
|
let rest = branch.length == 1 ? none : branch.slice(0, branch.length - 1);
|
|
if (event.mapped)
|
|
rest = addMappingToBranch(rest, event.mapped);
|
|
return state.update({
|
|
changes: event.changes,
|
|
selection: event.startSelection,
|
|
effects: event.effects,
|
|
annotations: fromHistory.of({ side, rest, selection }),
|
|
filter: false,
|
|
userEvent: side == 0 /* BranchName.Done */ ? "undo" : "redo",
|
|
scrollIntoView: true
|
|
});
|
|
}
|
|
}
|
|
}
|
|
HistoryState.empty = /*@__PURE__*/new HistoryState(none, none);
|
|
/**
|
|
Default key bindings for the undo history.
|
|
|
|
- Mod-z: [`undo`](https://codemirror.net/6/docs/ref/#commands.undo).
|
|
- Mod-y (Mod-Shift-z on macOS) + Ctrl-Shift-z on Linux: [`redo`](https://codemirror.net/6/docs/ref/#commands.redo).
|
|
- Mod-u: [`undoSelection`](https://codemirror.net/6/docs/ref/#commands.undoSelection).
|
|
- Alt-u (Mod-Shift-u on macOS): [`redoSelection`](https://codemirror.net/6/docs/ref/#commands.redoSelection).
|
|
*/
|
|
const historyKeymap = [
|
|
{ key: "Mod-z", run: undo, preventDefault: true },
|
|
{ key: "Mod-y", mac: "Mod-Shift-z", run: redo, preventDefault: true },
|
|
{ linux: "Ctrl-Shift-z", run: redo, preventDefault: true },
|
|
{ key: "Mod-u", run: undoSelection, preventDefault: true },
|
|
{ key: "Alt-u", mac: "Mod-Shift-u", run: redoSelection, preventDefault: true }
|
|
];
|
|
|
|
function updateSel(sel, by) {
|
|
return EditorSelection.create(sel.ranges.map(by), sel.mainIndex);
|
|
}
|
|
function setSel(state, selection) {
|
|
return state.update({ selection, scrollIntoView: true, userEvent: "select" });
|
|
}
|
|
function moveSel({ state, dispatch }, how) {
|
|
let selection = updateSel(state.selection, how);
|
|
if (selection.eq(state.selection, true))
|
|
return false;
|
|
dispatch(setSel(state, selection));
|
|
return true;
|
|
}
|
|
function rangeEnd(range, forward) {
|
|
return EditorSelection.cursor(forward ? range.to : range.from);
|
|
}
|
|
function cursorByChar(view, forward) {
|
|
return moveSel(view, range => range.empty ? view.moveByChar(range, forward) : rangeEnd(range, forward));
|
|
}
|
|
function ltrAtCursor(view) {
|
|
return view.textDirectionAt(view.state.selection.main.head) == Direction.LTR;
|
|
}
|
|
/**
|
|
Move the selection one character to the left (which is backward in
|
|
left-to-right text, forward in right-to-left text).
|
|
*/
|
|
const cursorCharLeft = view => cursorByChar(view, !ltrAtCursor(view));
|
|
/**
|
|
Move the selection one character to the right.
|
|
*/
|
|
const cursorCharRight = view => cursorByChar(view, ltrAtCursor(view));
|
|
/**
|
|
Move the selection one character forward.
|
|
*/
|
|
const cursorCharForward = view => cursorByChar(view, true);
|
|
/**
|
|
Move the selection one character backward.
|
|
*/
|
|
const cursorCharBackward = view => cursorByChar(view, false);
|
|
function cursorByGroup(view, forward) {
|
|
return moveSel(view, range => range.empty ? view.moveByGroup(range, forward) : rangeEnd(range, forward));
|
|
}
|
|
/**
|
|
Move the selection to the left across one group of word or
|
|
non-word (but also non-space) characters.
|
|
*/
|
|
const cursorGroupLeft = view => cursorByGroup(view, !ltrAtCursor(view));
|
|
/**
|
|
Move the selection one group to the right.
|
|
*/
|
|
const cursorGroupRight = view => cursorByGroup(view, ltrAtCursor(view));
|
|
/**
|
|
Move the selection one group forward.
|
|
*/
|
|
const cursorGroupForward = view => cursorByGroup(view, true);
|
|
/**
|
|
Move the selection one group backward.
|
|
*/
|
|
const cursorGroupBackward = view => cursorByGroup(view, false);
|
|
const segmenter = typeof Intl != "undefined" && Intl.Segmenter ?
|
|
/*@__PURE__*/new (Intl.Segmenter)(undefined, { granularity: "word" }) : null;
|
|
function moveBySubword(view, range, forward) {
|
|
let categorize = view.state.charCategorizer(range.from);
|
|
let cat = CharCategory.Space, pos = range.from, steps = 0;
|
|
let done = false, sawUpper = false, sawLower = false;
|
|
let step = (next) => {
|
|
if (done)
|
|
return false;
|
|
pos += forward ? next.length : -next.length;
|
|
let nextCat = categorize(next), ahead;
|
|
if (nextCat == CharCategory.Word && next.charCodeAt(0) < 128 && /[\W_]/.test(next))
|
|
nextCat = -1; // Treat word punctuation specially
|
|
if (cat == CharCategory.Space)
|
|
cat = nextCat;
|
|
if (cat != nextCat)
|
|
return false;
|
|
if (cat == CharCategory.Word) {
|
|
if (next.toLowerCase() == next) {
|
|
if (!forward && sawUpper)
|
|
return false;
|
|
sawLower = true;
|
|
}
|
|
else if (sawLower) {
|
|
if (forward)
|
|
return false;
|
|
done = true;
|
|
}
|
|
else {
|
|
if (sawUpper && forward && categorize(ahead = view.state.sliceDoc(pos, pos + 1)) == CharCategory.Word &&
|
|
ahead.toLowerCase() == ahead)
|
|
return false;
|
|
sawUpper = true;
|
|
}
|
|
}
|
|
steps++;
|
|
return true;
|
|
};
|
|
let end = view.moveByChar(range, forward, start => {
|
|
step(start);
|
|
return step;
|
|
});
|
|
if (segmenter && cat == CharCategory.Word && end.from == range.from + steps * (forward ? 1 : -1)) {
|
|
let from = Math.min(range.head, end.head), to = Math.max(range.head, end.head);
|
|
let skipped = view.state.sliceDoc(from, to);
|
|
if (skipped.length > 1 && /[\u4E00-\uffff]/.test(skipped)) {
|
|
let segments = Array.from(segmenter.segment(skipped));
|
|
if (segments.length > 1) {
|
|
if (forward)
|
|
return EditorSelection.cursor(range.head + segments[1].index, -1);
|
|
return EditorSelection.cursor(end.head + segments[segments.length - 1].index, 1);
|
|
}
|
|
}
|
|
}
|
|
return end;
|
|
}
|
|
function cursorBySubword(view, forward) {
|
|
return moveSel(view, range => range.empty ? moveBySubword(view, range, forward) : rangeEnd(range, forward));
|
|
}
|
|
/**
|
|
Move the selection one group or camel-case subword forward.
|
|
*/
|
|
const cursorSubwordForward = view => cursorBySubword(view, true);
|
|
/**
|
|
Move the selection one group or camel-case subword backward.
|
|
*/
|
|
const cursorSubwordBackward = view => cursorBySubword(view, false);
|
|
function interestingNode(state, node, bracketProp) {
|
|
if (node.type.prop(bracketProp))
|
|
return true;
|
|
let len = node.to - node.from;
|
|
return len && (len > 2 || /[^\s,.;:]/.test(state.sliceDoc(node.from, node.to))) || node.firstChild;
|
|
}
|
|
function moveBySyntax(state, start, forward) {
|
|
let pos = syntaxTree(state).resolveInner(start.head);
|
|
let bracketProp = forward ? NodeProp.closedBy : NodeProp.openedBy;
|
|
// Scan forward through child nodes to see if there's an interesting
|
|
// node ahead.
|
|
for (let at = start.head;;) {
|
|
let next = forward ? pos.childAfter(at) : pos.childBefore(at);
|
|
if (!next)
|
|
break;
|
|
if (interestingNode(state, next, bracketProp))
|
|
pos = next;
|
|
else
|
|
at = forward ? next.to : next.from;
|
|
}
|
|
let bracket = pos.type.prop(bracketProp), match, newPos;
|
|
if (bracket && (match = forward ? matchBrackets(state, pos.from, 1) : matchBrackets(state, pos.to, -1)) && match.matched)
|
|
newPos = forward ? match.end.to : match.end.from;
|
|
else
|
|
newPos = forward ? pos.to : pos.from;
|
|
return EditorSelection.cursor(newPos, forward ? -1 : 1);
|
|
}
|
|
/**
|
|
Move the cursor over the next syntactic element to the left.
|
|
*/
|
|
const cursorSyntaxLeft = view => moveSel(view, range => moveBySyntax(view.state, range, !ltrAtCursor(view)));
|
|
/**
|
|
Move the cursor over the next syntactic element to the right.
|
|
*/
|
|
const cursorSyntaxRight = view => moveSel(view, range => moveBySyntax(view.state, range, ltrAtCursor(view)));
|
|
function cursorByLine(view, forward) {
|
|
return moveSel(view, range => {
|
|
if (!range.empty)
|
|
return rangeEnd(range, forward);
|
|
let moved = view.moveVertically(range, forward);
|
|
return moved.head != range.head ? moved : view.moveToLineBoundary(range, forward);
|
|
});
|
|
}
|
|
/**
|
|
Move the selection one line up.
|
|
*/
|
|
const cursorLineUp = view => cursorByLine(view, false);
|
|
/**
|
|
Move the selection one line down.
|
|
*/
|
|
const cursorLineDown = view => cursorByLine(view, true);
|
|
function pageInfo(view) {
|
|
let selfScroll = view.scrollDOM.clientHeight < view.scrollDOM.scrollHeight - 2;
|
|
let marginTop = 0, marginBottom = 0, height;
|
|
if (selfScroll) {
|
|
for (let source of view.state.facet(EditorView.scrollMargins)) {
|
|
let margins = source(view);
|
|
if (margins === null || margins === void 0 ? void 0 : margins.top)
|
|
marginTop = Math.max(margins === null || margins === void 0 ? void 0 : margins.top, marginTop);
|
|
if (margins === null || margins === void 0 ? void 0 : margins.bottom)
|
|
marginBottom = Math.max(margins === null || margins === void 0 ? void 0 : margins.bottom, marginBottom);
|
|
}
|
|
height = view.scrollDOM.clientHeight - marginTop - marginBottom;
|
|
}
|
|
else {
|
|
height = (view.dom.ownerDocument.defaultView || window).innerHeight;
|
|
}
|
|
return { marginTop, marginBottom, selfScroll,
|
|
height: Math.max(view.defaultLineHeight, height - 5) };
|
|
}
|
|
function cursorByPage(view, forward) {
|
|
let page = pageInfo(view);
|
|
let { state } = view, selection = updateSel(state.selection, range => {
|
|
return range.empty ? view.moveVertically(range, forward, page.height)
|
|
: rangeEnd(range, forward);
|
|
});
|
|
if (selection.eq(state.selection))
|
|
return false;
|
|
let effect;
|
|
if (page.selfScroll) {
|
|
let startPos = view.coordsAtPos(state.selection.main.head);
|
|
let scrollRect = view.scrollDOM.getBoundingClientRect();
|
|
let scrollTop = scrollRect.top + page.marginTop, scrollBottom = scrollRect.bottom - page.marginBottom;
|
|
if (startPos && startPos.top > scrollTop && startPos.bottom < scrollBottom)
|
|
effect = EditorView.scrollIntoView(selection.main.head, { y: "start", yMargin: startPos.top - scrollTop });
|
|
}
|
|
view.dispatch(setSel(state, selection), { effects: effect });
|
|
return true;
|
|
}
|
|
/**
|
|
Move the selection one page up.
|
|
*/
|
|
const cursorPageUp = view => cursorByPage(view, false);
|
|
/**
|
|
Move the selection one page down.
|
|
*/
|
|
const cursorPageDown = view => cursorByPage(view, true);
|
|
function moveByLineBoundary(view, start, forward) {
|
|
let line = view.lineBlockAt(start.head), moved = view.moveToLineBoundary(start, forward);
|
|
if (moved.head == start.head && moved.head != (forward ? line.to : line.from))
|
|
moved = view.moveToLineBoundary(start, forward, false);
|
|
if (!forward && moved.head == line.from && line.length) {
|
|
let space = /^\s*/.exec(view.state.sliceDoc(line.from, Math.min(line.from + 100, line.to)))[0].length;
|
|
if (space && start.head != line.from + space)
|
|
moved = EditorSelection.cursor(line.from + space);
|
|
}
|
|
return moved;
|
|
}
|
|
/**
|
|
Move the selection to the next line wrap point, or to the end of
|
|
the line if there isn't one left on this line.
|
|
*/
|
|
const cursorLineBoundaryForward = view => moveSel(view, range => moveByLineBoundary(view, range, true));
|
|
/**
|
|
Move the selection to previous line wrap point, or failing that to
|
|
the start of the line. If the line is indented, and the cursor
|
|
isn't already at the end of the indentation, this will move to the
|
|
end of the indentation instead of the start of the line.
|
|
*/
|
|
const cursorLineBoundaryBackward = view => moveSel(view, range => moveByLineBoundary(view, range, false));
|
|
/**
|
|
Move the selection one line wrap point to the left.
|
|
*/
|
|
const cursorLineBoundaryLeft = view => moveSel(view, range => moveByLineBoundary(view, range, !ltrAtCursor(view)));
|
|
/**
|
|
Move the selection one line wrap point to the right.
|
|
*/
|
|
const cursorLineBoundaryRight = view => moveSel(view, range => moveByLineBoundary(view, range, ltrAtCursor(view)));
|
|
/**
|
|
Move the selection to the start of the line.
|
|
*/
|
|
const cursorLineStart = view => moveSel(view, range => EditorSelection.cursor(view.lineBlockAt(range.head).from, 1));
|
|
/**
|
|
Move the selection to the end of the line.
|
|
*/
|
|
const cursorLineEnd = view => moveSel(view, range => EditorSelection.cursor(view.lineBlockAt(range.head).to, -1));
|
|
function toMatchingBracket(state, dispatch, extend) {
|
|
let found = false, selection = updateSel(state.selection, range => {
|
|
let matching = matchBrackets(state, range.head, -1)
|
|
|| matchBrackets(state, range.head, 1)
|
|
|| (range.head > 0 && matchBrackets(state, range.head - 1, 1))
|
|
|| (range.head < state.doc.length && matchBrackets(state, range.head + 1, -1));
|
|
if (!matching || !matching.end)
|
|
return range;
|
|
found = true;
|
|
let head = matching.start.from == range.head ? matching.end.to : matching.end.from;
|
|
return extend ? EditorSelection.range(range.anchor, head) : EditorSelection.cursor(head);
|
|
});
|
|
if (!found)
|
|
return false;
|
|
dispatch(setSel(state, selection));
|
|
return true;
|
|
}
|
|
/**
|
|
Move the selection to the bracket matching the one it is currently
|
|
on, if any.
|
|
*/
|
|
const cursorMatchingBracket = ({ state, dispatch }) => toMatchingBracket(state, dispatch, false);
|
|
/**
|
|
Extend the selection to the bracket matching the one the selection
|
|
head is currently on, if any.
|
|
*/
|
|
const selectMatchingBracket = ({ state, dispatch }) => toMatchingBracket(state, dispatch, true);
|
|
function extendSel(view, how) {
|
|
let selection = updateSel(view.state.selection, range => {
|
|
let head = how(range);
|
|
return EditorSelection.range(range.anchor, head.head, head.goalColumn, head.bidiLevel || undefined);
|
|
});
|
|
if (selection.eq(view.state.selection))
|
|
return false;
|
|
view.dispatch(setSel(view.state, selection));
|
|
return true;
|
|
}
|
|
function selectByChar(view, forward) {
|
|
return extendSel(view, range => view.moveByChar(range, forward));
|
|
}
|
|
/**
|
|
Move the selection head one character to the left, while leaving
|
|
the anchor in place.
|
|
*/
|
|
const selectCharLeft = view => selectByChar(view, !ltrAtCursor(view));
|
|
/**
|
|
Move the selection head one character to the right.
|
|
*/
|
|
const selectCharRight = view => selectByChar(view, ltrAtCursor(view));
|
|
/**
|
|
Move the selection head one character forward.
|
|
*/
|
|
const selectCharForward = view => selectByChar(view, true);
|
|
/**
|
|
Move the selection head one character backward.
|
|
*/
|
|
const selectCharBackward = view => selectByChar(view, false);
|
|
function selectByGroup(view, forward) {
|
|
return extendSel(view, range => view.moveByGroup(range, forward));
|
|
}
|
|
/**
|
|
Move the selection head one [group](https://codemirror.net/6/docs/ref/#commands.cursorGroupLeft) to
|
|
the left.
|
|
*/
|
|
const selectGroupLeft = view => selectByGroup(view, !ltrAtCursor(view));
|
|
/**
|
|
Move the selection head one group to the right.
|
|
*/
|
|
const selectGroupRight = view => selectByGroup(view, ltrAtCursor(view));
|
|
/**
|
|
Move the selection head one group forward.
|
|
*/
|
|
const selectGroupForward = view => selectByGroup(view, true);
|
|
/**
|
|
Move the selection head one group backward.
|
|
*/
|
|
const selectGroupBackward = view => selectByGroup(view, false);
|
|
function selectBySubword(view, forward) {
|
|
return extendSel(view, range => moveBySubword(view, range, forward));
|
|
}
|
|
/**
|
|
Move the selection head one group or camel-case subword forward.
|
|
*/
|
|
const selectSubwordForward = view => selectBySubword(view, true);
|
|
/**
|
|
Move the selection head one group or subword backward.
|
|
*/
|
|
const selectSubwordBackward = view => selectBySubword(view, false);
|
|
/**
|
|
Move the selection head over the next syntactic element to the left.
|
|
*/
|
|
const selectSyntaxLeft = view => extendSel(view, range => moveBySyntax(view.state, range, !ltrAtCursor(view)));
|
|
/**
|
|
Move the selection head over the next syntactic element to the right.
|
|
*/
|
|
const selectSyntaxRight = view => extendSel(view, range => moveBySyntax(view.state, range, ltrAtCursor(view)));
|
|
function selectByLine(view, forward) {
|
|
return extendSel(view, range => view.moveVertically(range, forward));
|
|
}
|
|
/**
|
|
Move the selection head one line up.
|
|
*/
|
|
const selectLineUp = view => selectByLine(view, false);
|
|
/**
|
|
Move the selection head one line down.
|
|
*/
|
|
const selectLineDown = view => selectByLine(view, true);
|
|
function selectByPage(view, forward) {
|
|
return extendSel(view, range => view.moveVertically(range, forward, pageInfo(view).height));
|
|
}
|
|
/**
|
|
Move the selection head one page up.
|
|
*/
|
|
const selectPageUp = view => selectByPage(view, false);
|
|
/**
|
|
Move the selection head one page down.
|
|
*/
|
|
const selectPageDown = view => selectByPage(view, true);
|
|
/**
|
|
Move the selection head to the next line boundary.
|
|
*/
|
|
const selectLineBoundaryForward = view => extendSel(view, range => moveByLineBoundary(view, range, true));
|
|
/**
|
|
Move the selection head to the previous line boundary.
|
|
*/
|
|
const selectLineBoundaryBackward = view => extendSel(view, range => moveByLineBoundary(view, range, false));
|
|
/**
|
|
Move the selection head one line boundary to the left.
|
|
*/
|
|
const selectLineBoundaryLeft = view => extendSel(view, range => moveByLineBoundary(view, range, !ltrAtCursor(view)));
|
|
/**
|
|
Move the selection head one line boundary to the right.
|
|
*/
|
|
const selectLineBoundaryRight = view => extendSel(view, range => moveByLineBoundary(view, range, ltrAtCursor(view)));
|
|
/**
|
|
Move the selection head to the start of the line.
|
|
*/
|
|
const selectLineStart = view => extendSel(view, range => EditorSelection.cursor(view.lineBlockAt(range.head).from));
|
|
/**
|
|
Move the selection head to the end of the line.
|
|
*/
|
|
const selectLineEnd = view => extendSel(view, range => EditorSelection.cursor(view.lineBlockAt(range.head).to));
|
|
/**
|
|
Move the selection to the start of the document.
|
|
*/
|
|
const cursorDocStart = ({ state, dispatch }) => {
|
|
dispatch(setSel(state, { anchor: 0 }));
|
|
return true;
|
|
};
|
|
/**
|
|
Move the selection to the end of the document.
|
|
*/
|
|
const cursorDocEnd = ({ state, dispatch }) => {
|
|
dispatch(setSel(state, { anchor: state.doc.length }));
|
|
return true;
|
|
};
|
|
/**
|
|
Move the selection head to the start of the document.
|
|
*/
|
|
const selectDocStart = ({ state, dispatch }) => {
|
|
dispatch(setSel(state, { anchor: state.selection.main.anchor, head: 0 }));
|
|
return true;
|
|
};
|
|
/**
|
|
Move the selection head to the end of the document.
|
|
*/
|
|
const selectDocEnd = ({ state, dispatch }) => {
|
|
dispatch(setSel(state, { anchor: state.selection.main.anchor, head: state.doc.length }));
|
|
return true;
|
|
};
|
|
/**
|
|
Select the entire document.
|
|
*/
|
|
const selectAll = ({ state, dispatch }) => {
|
|
dispatch(state.update({ selection: { anchor: 0, head: state.doc.length }, userEvent: "select" }));
|
|
return true;
|
|
};
|
|
/**
|
|
Expand the selection to cover entire lines.
|
|
*/
|
|
const selectLine = ({ state, dispatch }) => {
|
|
let ranges = selectedLineBlocks(state).map(({ from, to }) => EditorSelection.range(from, Math.min(to + 1, state.doc.length)));
|
|
dispatch(state.update({ selection: EditorSelection.create(ranges), userEvent: "select" }));
|
|
return true;
|
|
};
|
|
/**
|
|
Select the next syntactic construct that is larger than the
|
|
selection. Note that this will only work insofar as the language
|
|
[provider](https://codemirror.net/6/docs/ref/#language.language) you use builds up a full
|
|
syntax tree.
|
|
*/
|
|
const selectParentSyntax = ({ state, dispatch }) => {
|
|
let selection = updateSel(state.selection, range => {
|
|
var _a;
|
|
let stack = syntaxTree(state).resolveStack(range.from, 1);
|
|
for (let cur = stack; cur; cur = cur.next) {
|
|
let { node } = cur;
|
|
if (((node.from < range.from && node.to >= range.to) ||
|
|
(node.to > range.to && node.from <= range.from)) &&
|
|
((_a = node.parent) === null || _a === void 0 ? void 0 : _a.parent))
|
|
return EditorSelection.range(node.to, node.from);
|
|
}
|
|
return range;
|
|
});
|
|
dispatch(setSel(state, selection));
|
|
return true;
|
|
};
|
|
/**
|
|
Simplify the current selection. When multiple ranges are selected,
|
|
reduce it to its main range. Otherwise, if the selection is
|
|
non-empty, convert it to a cursor selection.
|
|
*/
|
|
const simplifySelection = ({ state, dispatch }) => {
|
|
let cur = state.selection, selection = null;
|
|
if (cur.ranges.length > 1)
|
|
selection = EditorSelection.create([cur.main]);
|
|
else if (!cur.main.empty)
|
|
selection = EditorSelection.create([EditorSelection.cursor(cur.main.head)]);
|
|
if (!selection)
|
|
return false;
|
|
dispatch(setSel(state, selection));
|
|
return true;
|
|
};
|
|
function deleteBy(target, by) {
|
|
if (target.state.readOnly)
|
|
return false;
|
|
let event = "delete.selection", { state } = target;
|
|
let changes = state.changeByRange(range => {
|
|
let { from, to } = range;
|
|
if (from == to) {
|
|
let towards = by(range);
|
|
if (towards < from) {
|
|
event = "delete.backward";
|
|
towards = skipAtomic(target, towards, false);
|
|
}
|
|
else if (towards > from) {
|
|
event = "delete.forward";
|
|
towards = skipAtomic(target, towards, true);
|
|
}
|
|
from = Math.min(from, towards);
|
|
to = Math.max(to, towards);
|
|
}
|
|
else {
|
|
from = skipAtomic(target, from, false);
|
|
to = skipAtomic(target, to, true);
|
|
}
|
|
return from == to ? { range } : { changes: { from, to }, range: EditorSelection.cursor(from, from < range.head ? -1 : 1) };
|
|
});
|
|
if (changes.changes.empty)
|
|
return false;
|
|
target.dispatch(state.update(changes, {
|
|
scrollIntoView: true,
|
|
userEvent: event,
|
|
effects: event == "delete.selection" ? EditorView.announce.of(state.phrase("Selection deleted")) : undefined
|
|
}));
|
|
return true;
|
|
}
|
|
function skipAtomic(target, pos, forward) {
|
|
if (target instanceof EditorView)
|
|
for (let ranges of target.state.facet(EditorView.atomicRanges).map(f => f(target)))
|
|
ranges.between(pos, pos, (from, to) => {
|
|
if (from < pos && to > pos)
|
|
pos = forward ? to : from;
|
|
});
|
|
return pos;
|
|
}
|
|
const deleteByChar = (target, forward, byIndentUnit) => deleteBy(target, range => {
|
|
let pos = range.from, { state } = target, line = state.doc.lineAt(pos), before, targetPos;
|
|
if (byIndentUnit && !forward && pos > line.from && pos < line.from + 200 &&
|
|
!/[^ \t]/.test(before = line.text.slice(0, pos - line.from))) {
|
|
if (before[before.length - 1] == "\t")
|
|
return pos - 1;
|
|
let col = countColumn(before, state.tabSize), drop = col % getIndentUnit(state) || getIndentUnit(state);
|
|
for (let i = 0; i < drop && before[before.length - 1 - i] == " "; i++)
|
|
pos--;
|
|
targetPos = pos;
|
|
}
|
|
else {
|
|
targetPos = findClusterBreak(line.text, pos - line.from, forward, forward) + line.from;
|
|
if (targetPos == pos && line.number != (forward ? state.doc.lines : 1))
|
|
targetPos += forward ? 1 : -1;
|
|
else if (!forward && /[\ufe00-\ufe0f]/.test(line.text.slice(targetPos - line.from, pos - line.from)))
|
|
targetPos = findClusterBreak(line.text, targetPos - line.from, false, false) + line.from;
|
|
}
|
|
return targetPos;
|
|
});
|
|
/**
|
|
Delete the selection, or, for cursor selections, the character or
|
|
indentation unit before the cursor.
|
|
*/
|
|
const deleteCharBackward = view => deleteByChar(view, false, true);
|
|
/**
|
|
Delete the selection or the character before the cursor. Does not
|
|
implement any extended behavior like deleting whole indentation
|
|
units in one go.
|
|
*/
|
|
const deleteCharBackwardStrict = view => deleteByChar(view, false, false);
|
|
/**
|
|
Delete the selection or the character after the cursor.
|
|
*/
|
|
const deleteCharForward = view => deleteByChar(view, true, false);
|
|
const deleteByGroup = (target, forward) => deleteBy(target, range => {
|
|
let pos = range.head, { state } = target, line = state.doc.lineAt(pos);
|
|
let categorize = state.charCategorizer(pos);
|
|
for (let cat = null;;) {
|
|
if (pos == (forward ? line.to : line.from)) {
|
|
if (pos == range.head && line.number != (forward ? state.doc.lines : 1))
|
|
pos += forward ? 1 : -1;
|
|
break;
|
|
}
|
|
let next = findClusterBreak(line.text, pos - line.from, forward) + line.from;
|
|
let nextChar = line.text.slice(Math.min(pos, next) - line.from, Math.max(pos, next) - line.from);
|
|
let nextCat = categorize(nextChar);
|
|
if (cat != null && nextCat != cat)
|
|
break;
|
|
if (nextChar != " " || pos != range.head)
|
|
cat = nextCat;
|
|
pos = next;
|
|
}
|
|
return pos;
|
|
});
|
|
/**
|
|
Delete the selection or backward until the end of the next
|
|
[group](https://codemirror.net/6/docs/ref/#view.EditorView.moveByGroup), only skipping groups of
|
|
whitespace when they consist of a single space.
|
|
*/
|
|
const deleteGroupBackward = target => deleteByGroup(target, false);
|
|
/**
|
|
Delete the selection or forward until the end of the next group.
|
|
*/
|
|
const deleteGroupForward = target => deleteByGroup(target, true);
|
|
/**
|
|
Delete the selection, or, if it is a cursor selection, delete to
|
|
the end of the line. If the cursor is directly at the end of the
|
|
line, delete the line break after it.
|
|
*/
|
|
const deleteToLineEnd = view => deleteBy(view, range => {
|
|
let lineEnd = view.lineBlockAt(range.head).to;
|
|
return range.head < lineEnd ? lineEnd : Math.min(view.state.doc.length, range.head + 1);
|
|
});
|
|
/**
|
|
Delete the selection, or, if it is a cursor selection, delete to
|
|
the start of the line. If the cursor is directly at the start of the
|
|
line, delete the line break before it.
|
|
*/
|
|
const deleteToLineStart = view => deleteBy(view, range => {
|
|
let lineStart = view.lineBlockAt(range.head).from;
|
|
return range.head > lineStart ? lineStart : Math.max(0, range.head - 1);
|
|
});
|
|
/**
|
|
Delete the selection, or, if it is a cursor selection, delete to
|
|
the start of the line or the next line wrap before the cursor.
|
|
*/
|
|
const deleteLineBoundaryBackward = view => deleteBy(view, range => {
|
|
let lineStart = view.moveToLineBoundary(range, false).head;
|
|
return range.head > lineStart ? lineStart : Math.max(0, range.head - 1);
|
|
});
|
|
/**
|
|
Delete the selection, or, if it is a cursor selection, delete to
|
|
the end of the line or the next line wrap after the cursor.
|
|
*/
|
|
const deleteLineBoundaryForward = view => deleteBy(view, range => {
|
|
let lineStart = view.moveToLineBoundary(range, true).head;
|
|
return range.head < lineStart ? lineStart : Math.min(view.state.doc.length, range.head + 1);
|
|
});
|
|
/**
|
|
Delete all whitespace directly before a line end from the
|
|
document.
|
|
*/
|
|
const deleteTrailingWhitespace = ({ state, dispatch }) => {
|
|
if (state.readOnly)
|
|
return false;
|
|
let changes = [];
|
|
for (let pos = 0, prev = "", iter = state.doc.iter();;) {
|
|
iter.next();
|
|
if (iter.lineBreak || iter.done) {
|
|
let trailing = prev.search(/\s+$/);
|
|
if (trailing > -1)
|
|
changes.push({ from: pos - (prev.length - trailing), to: pos });
|
|
if (iter.done)
|
|
break;
|
|
prev = "";
|
|
}
|
|
else {
|
|
prev = iter.value;
|
|
}
|
|
pos += iter.value.length;
|
|
}
|
|
if (!changes.length)
|
|
return false;
|
|
dispatch(state.update({ changes, userEvent: "delete" }));
|
|
return true;
|
|
};
|
|
/**
|
|
Replace each selection range with a line break, leaving the cursor
|
|
on the line before the break.
|
|
*/
|
|
const splitLine = ({ state, dispatch }) => {
|
|
if (state.readOnly)
|
|
return false;
|
|
let changes = state.changeByRange(range => {
|
|
return { changes: { from: range.from, to: range.to, insert: Text.of(["", ""]) },
|
|
range: EditorSelection.cursor(range.from) };
|
|
});
|
|
dispatch(state.update(changes, { scrollIntoView: true, userEvent: "input" }));
|
|
return true;
|
|
};
|
|
/**
|
|
Flip the characters before and after the cursor(s).
|
|
*/
|
|
const transposeChars = ({ state, dispatch }) => {
|
|
if (state.readOnly)
|
|
return false;
|
|
let changes = state.changeByRange(range => {
|
|
if (!range.empty || range.from == 0 || range.from == state.doc.length)
|
|
return { range };
|
|
let pos = range.from, line = state.doc.lineAt(pos);
|
|
let from = pos == line.from ? pos - 1 : findClusterBreak(line.text, pos - line.from, false) + line.from;
|
|
let to = pos == line.to ? pos + 1 : findClusterBreak(line.text, pos - line.from, true) + line.from;
|
|
return { changes: { from, to, insert: state.doc.slice(pos, to).append(state.doc.slice(from, pos)) },
|
|
range: EditorSelection.cursor(to) };
|
|
});
|
|
if (changes.changes.empty)
|
|
return false;
|
|
dispatch(state.update(changes, { scrollIntoView: true, userEvent: "move.character" }));
|
|
return true;
|
|
};
|
|
function selectedLineBlocks(state) {
|
|
let blocks = [], upto = -1;
|
|
for (let range of state.selection.ranges) {
|
|
let startLine = state.doc.lineAt(range.from), endLine = state.doc.lineAt(range.to);
|
|
if (!range.empty && range.to == endLine.from)
|
|
endLine = state.doc.lineAt(range.to - 1);
|
|
if (upto >= startLine.number) {
|
|
let prev = blocks[blocks.length - 1];
|
|
prev.to = endLine.to;
|
|
prev.ranges.push(range);
|
|
}
|
|
else {
|
|
blocks.push({ from: startLine.from, to: endLine.to, ranges: [range] });
|
|
}
|
|
upto = endLine.number + 1;
|
|
}
|
|
return blocks;
|
|
}
|
|
function moveLine(state, dispatch, forward) {
|
|
if (state.readOnly)
|
|
return false;
|
|
let changes = [], ranges = [];
|
|
for (let block of selectedLineBlocks(state)) {
|
|
if (forward ? block.to == state.doc.length : block.from == 0)
|
|
continue;
|
|
let nextLine = state.doc.lineAt(forward ? block.to + 1 : block.from - 1);
|
|
let size = nextLine.length + 1;
|
|
if (forward) {
|
|
changes.push({ from: block.to, to: nextLine.to }, { from: block.from, insert: nextLine.text + state.lineBreak });
|
|
for (let r of block.ranges)
|
|
ranges.push(EditorSelection.range(Math.min(state.doc.length, r.anchor + size), Math.min(state.doc.length, r.head + size)));
|
|
}
|
|
else {
|
|
changes.push({ from: nextLine.from, to: block.from }, { from: block.to, insert: state.lineBreak + nextLine.text });
|
|
for (let r of block.ranges)
|
|
ranges.push(EditorSelection.range(r.anchor - size, r.head - size));
|
|
}
|
|
}
|
|
if (!changes.length)
|
|
return false;
|
|
dispatch(state.update({
|
|
changes,
|
|
scrollIntoView: true,
|
|
selection: EditorSelection.create(ranges, state.selection.mainIndex),
|
|
userEvent: "move.line"
|
|
}));
|
|
return true;
|
|
}
|
|
/**
|
|
Move the selected lines up one line.
|
|
*/
|
|
const moveLineUp = ({ state, dispatch }) => moveLine(state, dispatch, false);
|
|
/**
|
|
Move the selected lines down one line.
|
|
*/
|
|
const moveLineDown = ({ state, dispatch }) => moveLine(state, dispatch, true);
|
|
function copyLine(state, dispatch, forward) {
|
|
if (state.readOnly)
|
|
return false;
|
|
let changes = [];
|
|
for (let block of selectedLineBlocks(state)) {
|
|
if (forward)
|
|
changes.push({ from: block.from, insert: state.doc.slice(block.from, block.to) + state.lineBreak });
|
|
else
|
|
changes.push({ from: block.to, insert: state.lineBreak + state.doc.slice(block.from, block.to) });
|
|
}
|
|
dispatch(state.update({ changes, scrollIntoView: true, userEvent: "input.copyline" }));
|
|
return true;
|
|
}
|
|
/**
|
|
Create a copy of the selected lines. Keep the selection in the top copy.
|
|
*/
|
|
const copyLineUp = ({ state, dispatch }) => copyLine(state, dispatch, false);
|
|
/**
|
|
Create a copy of the selected lines. Keep the selection in the bottom copy.
|
|
*/
|
|
const copyLineDown = ({ state, dispatch }) => copyLine(state, dispatch, true);
|
|
/**
|
|
Delete selected lines.
|
|
*/
|
|
const deleteLine = view => {
|
|
if (view.state.readOnly)
|
|
return false;
|
|
let { state } = view, changes = state.changes(selectedLineBlocks(state).map(({ from, to }) => {
|
|
if (from > 0)
|
|
from--;
|
|
else if (to < state.doc.length)
|
|
to++;
|
|
return { from, to };
|
|
}));
|
|
let selection = updateSel(state.selection, range => {
|
|
let dist = undefined;
|
|
if (view.lineWrapping) {
|
|
let block = view.lineBlockAt(range.head), pos = view.coordsAtPos(range.head, range.assoc || 1);
|
|
if (pos)
|
|
dist = (block.bottom + view.documentTop) - pos.bottom + view.defaultLineHeight / 2;
|
|
}
|
|
return view.moveVertically(range, true, dist);
|
|
}).map(changes);
|
|
view.dispatch({ changes, selection, scrollIntoView: true, userEvent: "delete.line" });
|
|
return true;
|
|
};
|
|
/**
|
|
Replace the selection with a newline.
|
|
*/
|
|
const insertNewline = ({ state, dispatch }) => {
|
|
dispatch(state.update(state.replaceSelection(state.lineBreak), { scrollIntoView: true, userEvent: "input" }));
|
|
return true;
|
|
};
|
|
/**
|
|
Replace the selection with a newline and the same amount of
|
|
indentation as the line above.
|
|
*/
|
|
const insertNewlineKeepIndent = ({ state, dispatch }) => {
|
|
dispatch(state.update(state.changeByRange(range => {
|
|
let indent = /^\s*/.exec(state.doc.lineAt(range.from).text)[0];
|
|
return {
|
|
changes: { from: range.from, to: range.to, insert: state.lineBreak + indent },
|
|
range: EditorSelection.cursor(range.from + indent.length + 1)
|
|
};
|
|
}), { scrollIntoView: true, userEvent: "input" }));
|
|
return true;
|
|
};
|
|
function isBetweenBrackets(state, pos) {
|
|
if (/\(\)|\[\]|\{\}/.test(state.sliceDoc(pos - 1, pos + 1)))
|
|
return { from: pos, to: pos };
|
|
let context = syntaxTree(state).resolveInner(pos);
|
|
let before = context.childBefore(pos), after = context.childAfter(pos), closedBy;
|
|
if (before && after && before.to <= pos && after.from >= pos &&
|
|
(closedBy = before.type.prop(NodeProp.closedBy)) && closedBy.indexOf(after.name) > -1 &&
|
|
state.doc.lineAt(before.to).from == state.doc.lineAt(after.from).from &&
|
|
!/\S/.test(state.sliceDoc(before.to, after.from)))
|
|
return { from: before.to, to: after.from };
|
|
return null;
|
|
}
|
|
/**
|
|
Replace the selection with a newline and indent the newly created
|
|
line(s). If the current line consists only of whitespace, this
|
|
will also delete that whitespace. When the cursor is between
|
|
matching brackets, an additional newline will be inserted after
|
|
the cursor.
|
|
*/
|
|
const insertNewlineAndIndent = /*@__PURE__*/newlineAndIndent(false);
|
|
/**
|
|
Create a blank, indented line below the current line.
|
|
*/
|
|
const insertBlankLine = /*@__PURE__*/newlineAndIndent(true);
|
|
function newlineAndIndent(atEof) {
|
|
return ({ state, dispatch }) => {
|
|
if (state.readOnly)
|
|
return false;
|
|
let changes = state.changeByRange(range => {
|
|
let { from, to } = range, line = state.doc.lineAt(from);
|
|
let explode = !atEof && from == to && isBetweenBrackets(state, from);
|
|
if (atEof)
|
|
from = to = (to <= line.to ? line : state.doc.lineAt(to)).to;
|
|
let cx = new IndentContext(state, { simulateBreak: from, simulateDoubleBreak: !!explode });
|
|
let indent = getIndentation(cx, from);
|
|
if (indent == null)
|
|
indent = countColumn(/^\s*/.exec(state.doc.lineAt(from).text)[0], state.tabSize);
|
|
while (to < line.to && /\s/.test(line.text[to - line.from]))
|
|
to++;
|
|
if (explode)
|
|
({ from, to } = explode);
|
|
else if (from > line.from && from < line.from + 100 && !/\S/.test(line.text.slice(0, from)))
|
|
from = line.from;
|
|
let insert = ["", indentString(state, indent)];
|
|
if (explode)
|
|
insert.push(indentString(state, cx.lineIndent(line.from, -1)));
|
|
return { changes: { from, to, insert: Text.of(insert) },
|
|
range: EditorSelection.cursor(from + 1 + insert[1].length) };
|
|
});
|
|
dispatch(state.update(changes, { scrollIntoView: true, userEvent: "input" }));
|
|
return true;
|
|
};
|
|
}
|
|
function changeBySelectedLine(state, f) {
|
|
let atLine = -1;
|
|
return state.changeByRange(range => {
|
|
let changes = [];
|
|
for (let pos = range.from; pos <= range.to;) {
|
|
let line = state.doc.lineAt(pos);
|
|
if (line.number > atLine && (range.empty || range.to > line.from)) {
|
|
f(line, changes, range);
|
|
atLine = line.number;
|
|
}
|
|
pos = line.to + 1;
|
|
}
|
|
let changeSet = state.changes(changes);
|
|
return { changes,
|
|
range: EditorSelection.range(changeSet.mapPos(range.anchor, 1), changeSet.mapPos(range.head, 1)) };
|
|
});
|
|
}
|
|
/**
|
|
Auto-indent the selected lines. This uses the [indentation service
|
|
facet](https://codemirror.net/6/docs/ref/#language.indentService) as source for auto-indent
|
|
information.
|
|
*/
|
|
const indentSelection = ({ state, dispatch }) => {
|
|
if (state.readOnly)
|
|
return false;
|
|
let updated = Object.create(null);
|
|
let context = new IndentContext(state, { overrideIndentation: start => {
|
|
let found = updated[start];
|
|
return found == null ? -1 : found;
|
|
} });
|
|
let changes = changeBySelectedLine(state, (line, changes, range) => {
|
|
let indent = getIndentation(context, line.from);
|
|
if (indent == null)
|
|
return;
|
|
if (!/\S/.test(line.text))
|
|
indent = 0;
|
|
let cur = /^\s*/.exec(line.text)[0];
|
|
let norm = indentString(state, indent);
|
|
if (cur != norm || range.from < line.from + cur.length) {
|
|
updated[line.from] = indent;
|
|
changes.push({ from: line.from, to: line.from + cur.length, insert: norm });
|
|
}
|
|
});
|
|
if (!changes.changes.empty)
|
|
dispatch(state.update(changes, { userEvent: "indent" }));
|
|
return true;
|
|
};
|
|
/**
|
|
Add a [unit](https://codemirror.net/6/docs/ref/#language.indentUnit) of indentation to all selected
|
|
lines.
|
|
*/
|
|
const indentMore = ({ state, dispatch }) => {
|
|
if (state.readOnly)
|
|
return false;
|
|
dispatch(state.update(changeBySelectedLine(state, (line, changes) => {
|
|
changes.push({ from: line.from, insert: state.facet(indentUnit) });
|
|
}), { userEvent: "input.indent" }));
|
|
return true;
|
|
};
|
|
/**
|
|
Remove a [unit](https://codemirror.net/6/docs/ref/#language.indentUnit) of indentation from all
|
|
selected lines.
|
|
*/
|
|
const indentLess = ({ state, dispatch }) => {
|
|
if (state.readOnly)
|
|
return false;
|
|
dispatch(state.update(changeBySelectedLine(state, (line, changes) => {
|
|
let space = /^\s*/.exec(line.text)[0];
|
|
if (!space)
|
|
return;
|
|
let col = countColumn(space, state.tabSize), keep = 0;
|
|
let insert = indentString(state, Math.max(0, col - getIndentUnit(state)));
|
|
while (keep < space.length && keep < insert.length && space.charCodeAt(keep) == insert.charCodeAt(keep))
|
|
keep++;
|
|
changes.push({ from: line.from + keep, to: line.from + space.length, insert: insert.slice(keep) });
|
|
}), { userEvent: "delete.dedent" }));
|
|
return true;
|
|
};
|
|
/**
|
|
Enables or disables
|
|
[tab-focus mode](https://codemirror.net/6/docs/ref/#view.EditorView.setTabFocusMode). While on, this
|
|
prevents the editor's key bindings from capturing Tab or
|
|
Shift-Tab, making it possible for the user to move focus out of
|
|
the editor with the keyboard.
|
|
*/
|
|
const toggleTabFocusMode = view => {
|
|
view.setTabFocusMode();
|
|
return true;
|
|
};
|
|
/**
|
|
Temporarily enables [tab-focus
|
|
mode](https://codemirror.net/6/docs/ref/#view.EditorView.setTabFocusMode) for two seconds or until
|
|
another key is pressed.
|
|
*/
|
|
const temporarilySetTabFocusMode = view => {
|
|
view.setTabFocusMode(2000);
|
|
return true;
|
|
};
|
|
/**
|
|
Insert a tab character at the cursor or, if something is selected,
|
|
use [`indentMore`](https://codemirror.net/6/docs/ref/#commands.indentMore) to indent the entire
|
|
selection.
|
|
*/
|
|
const insertTab = ({ state, dispatch }) => {
|
|
if (state.selection.ranges.some(r => !r.empty))
|
|
return indentMore({ state, dispatch });
|
|
dispatch(state.update(state.replaceSelection("\t"), { scrollIntoView: true, userEvent: "input" }));
|
|
return true;
|
|
};
|
|
/**
|
|
Array of key bindings containing the Emacs-style bindings that are
|
|
available on macOS by default.
|
|
|
|
- Ctrl-b: [`cursorCharLeft`](https://codemirror.net/6/docs/ref/#commands.cursorCharLeft) ([`selectCharLeft`](https://codemirror.net/6/docs/ref/#commands.selectCharLeft) with Shift)
|
|
- Ctrl-f: [`cursorCharRight`](https://codemirror.net/6/docs/ref/#commands.cursorCharRight) ([`selectCharRight`](https://codemirror.net/6/docs/ref/#commands.selectCharRight) with Shift)
|
|
- Ctrl-p: [`cursorLineUp`](https://codemirror.net/6/docs/ref/#commands.cursorLineUp) ([`selectLineUp`](https://codemirror.net/6/docs/ref/#commands.selectLineUp) with Shift)
|
|
- Ctrl-n: [`cursorLineDown`](https://codemirror.net/6/docs/ref/#commands.cursorLineDown) ([`selectLineDown`](https://codemirror.net/6/docs/ref/#commands.selectLineDown) with Shift)
|
|
- Ctrl-a: [`cursorLineStart`](https://codemirror.net/6/docs/ref/#commands.cursorLineStart) ([`selectLineStart`](https://codemirror.net/6/docs/ref/#commands.selectLineStart) with Shift)
|
|
- Ctrl-e: [`cursorLineEnd`](https://codemirror.net/6/docs/ref/#commands.cursorLineEnd) ([`selectLineEnd`](https://codemirror.net/6/docs/ref/#commands.selectLineEnd) with Shift)
|
|
- Ctrl-d: [`deleteCharForward`](https://codemirror.net/6/docs/ref/#commands.deleteCharForward)
|
|
- Ctrl-h: [`deleteCharBackward`](https://codemirror.net/6/docs/ref/#commands.deleteCharBackward)
|
|
- Ctrl-k: [`deleteToLineEnd`](https://codemirror.net/6/docs/ref/#commands.deleteToLineEnd)
|
|
- Ctrl-Alt-h: [`deleteGroupBackward`](https://codemirror.net/6/docs/ref/#commands.deleteGroupBackward)
|
|
- Ctrl-o: [`splitLine`](https://codemirror.net/6/docs/ref/#commands.splitLine)
|
|
- Ctrl-t: [`transposeChars`](https://codemirror.net/6/docs/ref/#commands.transposeChars)
|
|
- Ctrl-v: [`cursorPageDown`](https://codemirror.net/6/docs/ref/#commands.cursorPageDown)
|
|
- Alt-v: [`cursorPageUp`](https://codemirror.net/6/docs/ref/#commands.cursorPageUp)
|
|
*/
|
|
const emacsStyleKeymap = [
|
|
{ key: "Ctrl-b", run: cursorCharLeft, shift: selectCharLeft, preventDefault: true },
|
|
{ key: "Ctrl-f", run: cursorCharRight, shift: selectCharRight },
|
|
{ key: "Ctrl-p", run: cursorLineUp, shift: selectLineUp },
|
|
{ key: "Ctrl-n", run: cursorLineDown, shift: selectLineDown },
|
|
{ key: "Ctrl-a", run: cursorLineStart, shift: selectLineStart },
|
|
{ key: "Ctrl-e", run: cursorLineEnd, shift: selectLineEnd },
|
|
{ key: "Ctrl-d", run: deleteCharForward },
|
|
{ key: "Ctrl-h", run: deleteCharBackward },
|
|
{ key: "Ctrl-k", run: deleteToLineEnd },
|
|
{ key: "Ctrl-Alt-h", run: deleteGroupBackward },
|
|
{ key: "Ctrl-o", run: splitLine },
|
|
{ key: "Ctrl-t", run: transposeChars },
|
|
{ key: "Ctrl-v", run: cursorPageDown },
|
|
];
|
|
/**
|
|
An array of key bindings closely sticking to platform-standard or
|
|
widely used bindings. (This includes the bindings from
|
|
[`emacsStyleKeymap`](https://codemirror.net/6/docs/ref/#commands.emacsStyleKeymap), with their `key`
|
|
property changed to `mac`.)
|
|
|
|
- ArrowLeft: [`cursorCharLeft`](https://codemirror.net/6/docs/ref/#commands.cursorCharLeft) ([`selectCharLeft`](https://codemirror.net/6/docs/ref/#commands.selectCharLeft) with Shift)
|
|
- ArrowRight: [`cursorCharRight`](https://codemirror.net/6/docs/ref/#commands.cursorCharRight) ([`selectCharRight`](https://codemirror.net/6/docs/ref/#commands.selectCharRight) with Shift)
|
|
- Ctrl-ArrowLeft (Alt-ArrowLeft on macOS): [`cursorGroupLeft`](https://codemirror.net/6/docs/ref/#commands.cursorGroupLeft) ([`selectGroupLeft`](https://codemirror.net/6/docs/ref/#commands.selectGroupLeft) with Shift)
|
|
- Ctrl-ArrowRight (Alt-ArrowRight on macOS): [`cursorGroupRight`](https://codemirror.net/6/docs/ref/#commands.cursorGroupRight) ([`selectGroupRight`](https://codemirror.net/6/docs/ref/#commands.selectGroupRight) with Shift)
|
|
- Cmd-ArrowLeft (on macOS): [`cursorLineStart`](https://codemirror.net/6/docs/ref/#commands.cursorLineStart) ([`selectLineStart`](https://codemirror.net/6/docs/ref/#commands.selectLineStart) with Shift)
|
|
- Cmd-ArrowRight (on macOS): [`cursorLineEnd`](https://codemirror.net/6/docs/ref/#commands.cursorLineEnd) ([`selectLineEnd`](https://codemirror.net/6/docs/ref/#commands.selectLineEnd) with Shift)
|
|
- ArrowUp: [`cursorLineUp`](https://codemirror.net/6/docs/ref/#commands.cursorLineUp) ([`selectLineUp`](https://codemirror.net/6/docs/ref/#commands.selectLineUp) with Shift)
|
|
- ArrowDown: [`cursorLineDown`](https://codemirror.net/6/docs/ref/#commands.cursorLineDown) ([`selectLineDown`](https://codemirror.net/6/docs/ref/#commands.selectLineDown) with Shift)
|
|
- Cmd-ArrowUp (on macOS): [`cursorDocStart`](https://codemirror.net/6/docs/ref/#commands.cursorDocStart) ([`selectDocStart`](https://codemirror.net/6/docs/ref/#commands.selectDocStart) with Shift)
|
|
- Cmd-ArrowDown (on macOS): [`cursorDocEnd`](https://codemirror.net/6/docs/ref/#commands.cursorDocEnd) ([`selectDocEnd`](https://codemirror.net/6/docs/ref/#commands.selectDocEnd) with Shift)
|
|
- Ctrl-ArrowUp (on macOS): [`cursorPageUp`](https://codemirror.net/6/docs/ref/#commands.cursorPageUp) ([`selectPageUp`](https://codemirror.net/6/docs/ref/#commands.selectPageUp) with Shift)
|
|
- Ctrl-ArrowDown (on macOS): [`cursorPageDown`](https://codemirror.net/6/docs/ref/#commands.cursorPageDown) ([`selectPageDown`](https://codemirror.net/6/docs/ref/#commands.selectPageDown) with Shift)
|
|
- PageUp: [`cursorPageUp`](https://codemirror.net/6/docs/ref/#commands.cursorPageUp) ([`selectPageUp`](https://codemirror.net/6/docs/ref/#commands.selectPageUp) with Shift)
|
|
- PageDown: [`cursorPageDown`](https://codemirror.net/6/docs/ref/#commands.cursorPageDown) ([`selectPageDown`](https://codemirror.net/6/docs/ref/#commands.selectPageDown) with Shift)
|
|
- Home: [`cursorLineBoundaryBackward`](https://codemirror.net/6/docs/ref/#commands.cursorLineBoundaryBackward) ([`selectLineBoundaryBackward`](https://codemirror.net/6/docs/ref/#commands.selectLineBoundaryBackward) with Shift)
|
|
- End: [`cursorLineBoundaryForward`](https://codemirror.net/6/docs/ref/#commands.cursorLineBoundaryForward) ([`selectLineBoundaryForward`](https://codemirror.net/6/docs/ref/#commands.selectLineBoundaryForward) with Shift)
|
|
- Ctrl-Home (Cmd-Home on macOS): [`cursorDocStart`](https://codemirror.net/6/docs/ref/#commands.cursorDocStart) ([`selectDocStart`](https://codemirror.net/6/docs/ref/#commands.selectDocStart) with Shift)
|
|
- Ctrl-End (Cmd-Home on macOS): [`cursorDocEnd`](https://codemirror.net/6/docs/ref/#commands.cursorDocEnd) ([`selectDocEnd`](https://codemirror.net/6/docs/ref/#commands.selectDocEnd) with Shift)
|
|
- Enter: [`insertNewlineAndIndent`](https://codemirror.net/6/docs/ref/#commands.insertNewlineAndIndent)
|
|
- Ctrl-a (Cmd-a on macOS): [`selectAll`](https://codemirror.net/6/docs/ref/#commands.selectAll)
|
|
- Backspace: [`deleteCharBackward`](https://codemirror.net/6/docs/ref/#commands.deleteCharBackward)
|
|
- Delete: [`deleteCharForward`](https://codemirror.net/6/docs/ref/#commands.deleteCharForward)
|
|
- Ctrl-Backspace (Alt-Backspace on macOS): [`deleteGroupBackward`](https://codemirror.net/6/docs/ref/#commands.deleteGroupBackward)
|
|
- Ctrl-Delete (Alt-Delete on macOS): [`deleteGroupForward`](https://codemirror.net/6/docs/ref/#commands.deleteGroupForward)
|
|
- Cmd-Backspace (macOS): [`deleteLineBoundaryBackward`](https://codemirror.net/6/docs/ref/#commands.deleteLineBoundaryBackward).
|
|
- Cmd-Delete (macOS): [`deleteLineBoundaryForward`](https://codemirror.net/6/docs/ref/#commands.deleteLineBoundaryForward).
|
|
*/
|
|
const standardKeymap = /*@__PURE__*/[
|
|
{ key: "ArrowLeft", run: cursorCharLeft, shift: selectCharLeft, preventDefault: true },
|
|
{ key: "Mod-ArrowLeft", mac: "Alt-ArrowLeft", run: cursorGroupLeft, shift: selectGroupLeft, preventDefault: true },
|
|
{ mac: "Cmd-ArrowLeft", run: cursorLineBoundaryLeft, shift: selectLineBoundaryLeft, preventDefault: true },
|
|
{ key: "ArrowRight", run: cursorCharRight, shift: selectCharRight, preventDefault: true },
|
|
{ key: "Mod-ArrowRight", mac: "Alt-ArrowRight", run: cursorGroupRight, shift: selectGroupRight, preventDefault: true },
|
|
{ mac: "Cmd-ArrowRight", run: cursorLineBoundaryRight, shift: selectLineBoundaryRight, preventDefault: true },
|
|
{ key: "ArrowUp", run: cursorLineUp, shift: selectLineUp, preventDefault: true },
|
|
{ mac: "Cmd-ArrowUp", run: cursorDocStart, shift: selectDocStart },
|
|
{ mac: "Ctrl-ArrowUp", run: cursorPageUp, shift: selectPageUp },
|
|
{ key: "ArrowDown", run: cursorLineDown, shift: selectLineDown, preventDefault: true },
|
|
{ mac: "Cmd-ArrowDown", run: cursorDocEnd, shift: selectDocEnd },
|
|
{ mac: "Ctrl-ArrowDown", run: cursorPageDown, shift: selectPageDown },
|
|
{ key: "PageUp", run: cursorPageUp, shift: selectPageUp },
|
|
{ key: "PageDown", run: cursorPageDown, shift: selectPageDown },
|
|
{ key: "Home", run: cursorLineBoundaryBackward, shift: selectLineBoundaryBackward, preventDefault: true },
|
|
{ key: "Mod-Home", run: cursorDocStart, shift: selectDocStart },
|
|
{ key: "End", run: cursorLineBoundaryForward, shift: selectLineBoundaryForward, preventDefault: true },
|
|
{ key: "Mod-End", run: cursorDocEnd, shift: selectDocEnd },
|
|
{ key: "Enter", run: insertNewlineAndIndent },
|
|
{ key: "Mod-a", run: selectAll },
|
|
{ key: "Backspace", run: deleteCharBackward, shift: deleteCharBackward },
|
|
{ key: "Delete", run: deleteCharForward },
|
|
{ key: "Mod-Backspace", mac: "Alt-Backspace", run: deleteGroupBackward },
|
|
{ key: "Mod-Delete", mac: "Alt-Delete", run: deleteGroupForward },
|
|
{ mac: "Mod-Backspace", run: deleteLineBoundaryBackward },
|
|
{ mac: "Mod-Delete", run: deleteLineBoundaryForward }
|
|
].concat(/*@__PURE__*/emacsStyleKeymap.map(b => ({ mac: b.key, run: b.run, shift: b.shift })));
|
|
/**
|
|
The default keymap. Includes all bindings from
|
|
[`standardKeymap`](https://codemirror.net/6/docs/ref/#commands.standardKeymap) plus the following:
|
|
|
|
- Alt-ArrowLeft (Ctrl-ArrowLeft on macOS): [`cursorSyntaxLeft`](https://codemirror.net/6/docs/ref/#commands.cursorSyntaxLeft) ([`selectSyntaxLeft`](https://codemirror.net/6/docs/ref/#commands.selectSyntaxLeft) with Shift)
|
|
- Alt-ArrowRight (Ctrl-ArrowRight on macOS): [`cursorSyntaxRight`](https://codemirror.net/6/docs/ref/#commands.cursorSyntaxRight) ([`selectSyntaxRight`](https://codemirror.net/6/docs/ref/#commands.selectSyntaxRight) with Shift)
|
|
- Alt-ArrowUp: [`moveLineUp`](https://codemirror.net/6/docs/ref/#commands.moveLineUp)
|
|
- Alt-ArrowDown: [`moveLineDown`](https://codemirror.net/6/docs/ref/#commands.moveLineDown)
|
|
- Shift-Alt-ArrowUp: [`copyLineUp`](https://codemirror.net/6/docs/ref/#commands.copyLineUp)
|
|
- Shift-Alt-ArrowDown: [`copyLineDown`](https://codemirror.net/6/docs/ref/#commands.copyLineDown)
|
|
- Escape: [`simplifySelection`](https://codemirror.net/6/docs/ref/#commands.simplifySelection)
|
|
- Ctrl-Enter (Cmd-Enter on macOS): [`insertBlankLine`](https://codemirror.net/6/docs/ref/#commands.insertBlankLine)
|
|
- Alt-l (Ctrl-l on macOS): [`selectLine`](https://codemirror.net/6/docs/ref/#commands.selectLine)
|
|
- Ctrl-i (Cmd-i on macOS): [`selectParentSyntax`](https://codemirror.net/6/docs/ref/#commands.selectParentSyntax)
|
|
- Ctrl-[ (Cmd-[ on macOS): [`indentLess`](https://codemirror.net/6/docs/ref/#commands.indentLess)
|
|
- Ctrl-] (Cmd-] on macOS): [`indentMore`](https://codemirror.net/6/docs/ref/#commands.indentMore)
|
|
- Ctrl-Alt-\\ (Cmd-Alt-\\ on macOS): [`indentSelection`](https://codemirror.net/6/docs/ref/#commands.indentSelection)
|
|
- Shift-Ctrl-k (Shift-Cmd-k on macOS): [`deleteLine`](https://codemirror.net/6/docs/ref/#commands.deleteLine)
|
|
- Shift-Ctrl-\\ (Shift-Cmd-\\ on macOS): [`cursorMatchingBracket`](https://codemirror.net/6/docs/ref/#commands.cursorMatchingBracket)
|
|
- Ctrl-/ (Cmd-/ on macOS): [`toggleComment`](https://codemirror.net/6/docs/ref/#commands.toggleComment).
|
|
- Shift-Alt-a: [`toggleBlockComment`](https://codemirror.net/6/docs/ref/#commands.toggleBlockComment).
|
|
- Ctrl-m (Alt-Shift-m on macOS): [`toggleTabFocusMode`](https://codemirror.net/6/docs/ref/#commands.toggleTabFocusMode).
|
|
*/
|
|
const defaultKeymap = /*@__PURE__*/[
|
|
{ key: "Alt-ArrowLeft", mac: "Ctrl-ArrowLeft", run: cursorSyntaxLeft, shift: selectSyntaxLeft },
|
|
{ key: "Alt-ArrowRight", mac: "Ctrl-ArrowRight", run: cursorSyntaxRight, shift: selectSyntaxRight },
|
|
{ key: "Alt-ArrowUp", run: moveLineUp },
|
|
{ key: "Shift-Alt-ArrowUp", run: copyLineUp },
|
|
{ key: "Alt-ArrowDown", run: moveLineDown },
|
|
{ key: "Shift-Alt-ArrowDown", run: copyLineDown },
|
|
{ key: "Escape", run: simplifySelection },
|
|
{ key: "Mod-Enter", run: insertBlankLine },
|
|
{ key: "Alt-l", mac: "Ctrl-l", run: selectLine },
|
|
{ key: "Mod-i", run: selectParentSyntax, preventDefault: true },
|
|
{ key: "Mod-[", run: indentLess },
|
|
{ key: "Mod-]", run: indentMore },
|
|
{ key: "Mod-Alt-\\", run: indentSelection },
|
|
{ key: "Shift-Mod-k", run: deleteLine },
|
|
{ key: "Shift-Mod-\\", run: cursorMatchingBracket },
|
|
{ key: "Mod-/", run: toggleComment },
|
|
{ key: "Alt-A", run: toggleBlockComment },
|
|
{ key: "Ctrl-m", mac: "Shift-Alt-m", run: toggleTabFocusMode },
|
|
].concat(standardKeymap);
|
|
/**
|
|
A binding that binds Tab to [`indentMore`](https://codemirror.net/6/docs/ref/#commands.indentMore) and
|
|
Shift-Tab to [`indentLess`](https://codemirror.net/6/docs/ref/#commands.indentLess).
|
|
Please see the [Tab example](../../examples/tab/) before using
|
|
this.
|
|
*/
|
|
const indentWithTab = { key: "Tab", run: indentMore, shift: indentLess };
|
|
|
|
export { blockComment, blockUncomment, copyLineDown, copyLineUp, cursorCharBackward, cursorCharForward, cursorCharLeft, cursorCharRight, cursorDocEnd, cursorDocStart, cursorGroupBackward, cursorGroupForward, cursorGroupLeft, cursorGroupRight, cursorLineBoundaryBackward, cursorLineBoundaryForward, cursorLineBoundaryLeft, cursorLineBoundaryRight, cursorLineDown, cursorLineEnd, cursorLineStart, cursorLineUp, cursorMatchingBracket, cursorPageDown, cursorPageUp, cursorSubwordBackward, cursorSubwordForward, cursorSyntaxLeft, cursorSyntaxRight, defaultKeymap, deleteCharBackward, deleteCharBackwardStrict, deleteCharForward, deleteGroupBackward, deleteGroupForward, deleteLine, deleteLineBoundaryBackward, deleteLineBoundaryForward, deleteToLineEnd, deleteToLineStart, deleteTrailingWhitespace, emacsStyleKeymap, history, historyField, historyKeymap, indentLess, indentMore, indentSelection, indentWithTab, insertBlankLine, insertNewline, insertNewlineAndIndent, insertNewlineKeepIndent, insertTab, invertedEffects, isolateHistory, lineComment, lineUncomment, moveLineDown, moveLineUp, redo, redoDepth, redoSelection, selectAll, selectCharBackward, selectCharForward, selectCharLeft, selectCharRight, selectDocEnd, selectDocStart, selectGroupBackward, selectGroupForward, selectGroupLeft, selectGroupRight, selectLine, selectLineBoundaryBackward, selectLineBoundaryForward, selectLineBoundaryLeft, selectLineBoundaryRight, selectLineDown, selectLineEnd, selectLineStart, selectLineUp, selectMatchingBracket, selectPageDown, selectPageUp, selectParentSyntax, selectSubwordBackward, selectSubwordForward, selectSyntaxLeft, selectSyntaxRight, simplifySelection, splitLine, standardKeymap, temporarilySetTabFocusMode, toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment, toggleTabFocusMode, transposeChars, undo, undoDepth, undoSelection };
|