import { signalLater } from "../util/operation_group.js" import { ensureCursorVisible } from "../display/scrolling.js" import { clipPos, cmp, Pos } from "../line/pos.js" import { getLine } from "../line/utils_line.js" import { hasHandler, signal, signalCursorActivity } from "../util/event.js" import { lst, sel_dontScroll } from "../util/misc.js" import { addSelectionToHistory } from "./history.js" import { normalizeSelection, Range, Selection, simpleSelection } from "./selection.js" // The 'scroll' parameter given to many of these indicated whether // the new cursor position should be scrolled into view after // modifying the selection. // If shift is held or the extend flag is set, extends a range to // include a given position (and optionally a second position). // Otherwise, simply returns the range between the given positions. // Used for cursor motion and such. export function extendRange(range, head, other, extend) { if (extend) { let anchor = range.anchor if (other) { let posBefore = cmp(head, anchor) < 0 if (posBefore != (cmp(other, anchor) < 0)) { anchor = head head = other } else if (posBefore != (cmp(head, other) < 0)) { head = other } } return new Range(anchor, head) } else { return new Range(other || head, head) } } // Extend the primary selection range, discard the rest. export function extendSelection(doc, head, other, options, extend) { if (extend == null) extend = doc.cm && (doc.cm.display.shift || doc.extend) setSelection(doc, new Selection([extendRange(doc.sel.primary(), head, other, extend)], 0), options) } // Extend all selections (pos is an array of selections with length // equal the number of selections) export function extendSelections(doc, heads, options) { let out = [] let extend = doc.cm && (doc.cm.display.shift || doc.extend) for (let i = 0; i < doc.sel.ranges.length; i++) out[i] = extendRange(doc.sel.ranges[i], heads[i], null, extend) let newSel = normalizeSelection(doc.cm, out, doc.sel.primIndex) setSelection(doc, newSel, options) } // Updates a single range in the selection. export function replaceOneSelection(doc, i, range, options) { let ranges = doc.sel.ranges.slice(0) ranges[i] = range setSelection(doc, normalizeSelection(doc.cm, ranges, doc.sel.primIndex), options) } // Reset the selection to a single range. export function setSimpleSelection(doc, anchor, head, options) { setSelection(doc, simpleSelection(anchor, head), options) } // Give beforeSelectionChange handlers a change to influence a // selection update. function filterSelectionChange(doc, sel, options) { let obj = { ranges: sel.ranges, update: function(ranges) { this.ranges = [] for (let i = 0; i < ranges.length; i++) this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), clipPos(doc, ranges[i].head)) }, origin: options && options.origin } signal(doc, "beforeSelectionChange", doc, obj) if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj) if (obj.ranges != sel.ranges) return normalizeSelection(doc.cm, obj.ranges, obj.ranges.length - 1) else return sel } export function setSelectionReplaceHistory(doc, sel, options) { let done = doc.history.done, last = lst(done) if (last && last.ranges) { done[done.length - 1] = sel setSelectionNoUndo(doc, sel, options) } else { setSelection(doc, sel, options) } } // Set a new selection. export function setSelection(doc, sel, options) { setSelectionNoUndo(doc, sel, options) addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options) } export function setSelectionNoUndo(doc, sel, options) { if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) sel = filterSelectionChange(doc, sel, options) let bias = options && options.bias || (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1) setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)) if (!(options && options.scroll === false) && doc.cm) ensureCursorVisible(doc.cm) } function setSelectionInner(doc, sel) { if (sel.equals(doc.sel)) return doc.sel = sel if (doc.cm) { doc.cm.curOp.updateInput = 1 doc.cm.curOp.selectionChanged = true signalCursorActivity(doc.cm) } signalLater(doc, "cursorActivity", doc) } // Verify that the selection does not partially select any atomic // marked ranges. export function reCheckSelection(doc) { setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false)) } // Return a selection that does not partially select any atomic // ranges. function skipAtomicInSelection(doc, sel, bias, mayClear) { let out for (let i = 0; i < sel.ranges.length; i++) { let range = sel.ranges[i] let old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i] let newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear) let newHead = skipAtomic(doc, range.head, old && old.head, bias, mayClear) if (out || newAnchor != range.anchor || newHead != range.head) { if (!out) out = sel.ranges.slice(0, i) out[i] = new Range(newAnchor, newHead) } } return out ? normalizeSelection(doc.cm, out, sel.primIndex) : sel } function skipAtomicInner(doc, pos, oldPos, dir, mayClear) { let line = getLine(doc, pos.line) if (line.markedSpans) for (let i = 0; i < line.markedSpans.length; ++i) { let sp = line.markedSpans[i], m = sp.marker if ((sp.from == null || (m.inclusiveLeft ? sp.from <= pos.ch : sp.from < pos.ch)) && (sp.to == null || (m.inclusiveRight ? sp.to >= pos.ch : sp.to > pos.ch))) { if (mayClear) { signal(m, "beforeCursorEnter") if (m.explicitlyCleared) { if (!line.markedSpans) break else {--i; continue} } } if (!m.atomic) continue if (oldPos) { let near = m.find(dir < 0 ? 1 : -1), diff if (dir < 0 ? m.inclusiveRight : m.inclusiveLeft) near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null) if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0)) return skipAtomicInner(doc, near, pos, dir, mayClear) } let far = m.find(dir < 0 ? -1 : 1) if (dir < 0 ? m.inclusiveLeft : m.inclusiveRight) far = movePos(doc, far, dir, far.line == pos.line ? line : null) return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null } } return pos } // Ensure a given position is not inside an atomic range. export function skipAtomic(doc, pos, oldPos, bias, mayClear) { let dir = bias || 1 let found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) || (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) || skipAtomicInner(doc, pos, oldPos, -dir, mayClear) || (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true)) if (!found) { doc.cantEdit = true return Pos(doc.first, 0) } return found } function movePos(doc, pos, dir, line) { if (dir < 0 && pos.ch == 0) { if (pos.line > doc.first) return clipPos(doc, Pos(pos.line - 1)) else return null } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) { if (pos.line < doc.first + doc.size - 1) return Pos(pos.line + 1, 0) else return null } else { return new Pos(pos.line, pos.ch + dir) } } export function selectAll(cm) { cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll) }