selection_updates.min.js (7567B)
1 import { signalLater } from "../util/operation_group.js" 2 import { ensureCursorVisible } from "../display/scrolling.js" 3 import { clipPos, cmp, Pos } from "../line/pos.js" 4 import { getLine } from "../line/utils_line.js" 5 import { hasHandler, signal, signalCursorActivity } from "../util/event.js" 6 import { lst, sel_dontScroll } from "../util/misc.js" 7 8 import { addSelectionToHistory } from "./history.js" 9 import { normalizeSelection, Range, Selection, simpleSelection } from "./selection.js" 10 11 // The 'scroll' parameter given to many of these indicated whether 12 // the new cursor position should be scrolled into view after 13 // modifying the selection. 14 15 // If shift is held or the extend flag is set, extends a range to 16 // include a given position (and optionally a second position). 17 // Otherwise, simply returns the range between the given positions. 18 // Used for cursor motion and such. 19 export function extendRange(range, head, other, extend) { 20 if (extend) { 21 let anchor = range.anchor 22 if (other) { 23 let posBefore = cmp(head, anchor) < 0 24 if (posBefore != (cmp(other, anchor) < 0)) { 25 anchor = head 26 head = other 27 } else if (posBefore != (cmp(head, other) < 0)) { 28 head = other 29 } 30 } 31 return new Range(anchor, head) 32 } else { 33 return new Range(other || head, head) 34 } 35 } 36 37 // Extend the primary selection range, discard the rest. 38 export function extendSelection(doc, head, other, options, extend) { 39 if (extend == null) extend = doc.cm && (doc.cm.display.shift || doc.extend) 40 setSelection(doc, new Selection([extendRange(doc.sel.primary(), head, other, extend)], 0), options) 41 } 42 43 // Extend all selections (pos is an array of selections with length 44 // equal the number of selections) 45 export function extendSelections(doc, heads, options) { 46 let out = [] 47 let extend = doc.cm && (doc.cm.display.shift || doc.extend) 48 for (let i = 0; i < doc.sel.ranges.length; i++) 49 out[i] = extendRange(doc.sel.ranges[i], heads[i], null, extend) 50 let newSel = normalizeSelection(out, doc.sel.primIndex) 51 setSelection(doc, newSel, options) 52 } 53 54 // Updates a single range in the selection. 55 export function replaceOneSelection(doc, i, range, options) { 56 let ranges = doc.sel.ranges.slice(0) 57 ranges[i] = range 58 setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options) 59 } 60 61 // Reset the selection to a single range. 62 export function setSimpleSelection(doc, anchor, head, options) { 63 setSelection(doc, simpleSelection(anchor, head), options) 64 } 65 66 // Give beforeSelectionChange handlers a change to influence a 67 // selection update. 68 function filterSelectionChange(doc, sel, options) { 69 let obj = { 70 ranges: sel.ranges, 71 update: function(ranges) { 72 this.ranges = [] 73 for (let i = 0; i < ranges.length; i++) 74 this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), 75 clipPos(doc, ranges[i].head)) 76 }, 77 origin: options && options.origin 78 } 79 signal(doc, "beforeSelectionChange", doc, obj) 80 if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj) 81 if (obj.ranges != sel.ranges) return normalizeSelection(obj.ranges, obj.ranges.length - 1) 82 else return sel 83 } 84 85 export function setSelectionReplaceHistory(doc, sel, options) { 86 let done = doc.history.done, last = lst(done) 87 if (last && last.ranges) { 88 done[done.length - 1] = sel 89 setSelectionNoUndo(doc, sel, options) 90 } else { 91 setSelection(doc, sel, options) 92 } 93 } 94 95 // Set a new selection. 96 export function setSelection(doc, sel, options) { 97 setSelectionNoUndo(doc, sel, options) 98 addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options) 99 } 100 101 export function setSelectionNoUndo(doc, sel, options) { 102 if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) 103 sel = filterSelectionChange(doc, sel, options) 104 105 let bias = options && options.bias || 106 (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1) 107 setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)) 108 109 if (!(options && options.scroll === false) && doc.cm) 110 ensureCursorVisible(doc.cm) 111 } 112 113 function setSelectionInner(doc, sel) { 114 if (sel.equals(doc.sel)) return 115 116 doc.sel = sel 117 118 if (doc.cm) { 119 doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true 120 signalCursorActivity(doc.cm) 121 } 122 signalLater(doc, "cursorActivity", doc) 123 } 124 125 // Verify that the selection does not partially select any atomic 126 // marked ranges. 127 export function reCheckSelection(doc) { 128 setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false)) 129 } 130 131 // Return a selection that does not partially select any atomic 132 // ranges. 133 function skipAtomicInSelection(doc, sel, bias, mayClear) { 134 let out 135 for (let i = 0; i < sel.ranges.length; i++) { 136 let range = sel.ranges[i] 137 let old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i] 138 let newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear) 139 let newHead = skipAtomic(doc, range.head, old && old.head, bias, mayClear) 140 if (out || newAnchor != range.anchor || newHead != range.head) { 141 if (!out) out = sel.ranges.slice(0, i) 142 out[i] = new Range(newAnchor, newHead) 143 } 144 } 145 return out ? normalizeSelection(out, sel.primIndex) : sel 146 } 147 148 function skipAtomicInner(doc, pos, oldPos, dir, mayClear) { 149 let line = getLine(doc, pos.line) 150 if (line.markedSpans) for (let i = 0; i < line.markedSpans.length; ++i) { 151 let sp = line.markedSpans[i], m = sp.marker 152 if ((sp.from == null || (m.inclusiveLeft ? sp.from <= pos.ch : sp.from < pos.ch)) && 153 (sp.to == null || (m.inclusiveRight ? sp.to >= pos.ch : sp.to > pos.ch))) { 154 if (mayClear) { 155 signal(m, "beforeCursorEnter") 156 if (m.explicitlyCleared) { 157 if (!line.markedSpans) break 158 else {--i; continue} 159 } 160 } 161 if (!m.atomic) continue 162 163 if (oldPos) { 164 let near = m.find(dir < 0 ? 1 : -1), diff 165 if (dir < 0 ? m.inclusiveRight : m.inclusiveLeft) 166 near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null) 167 if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0)) 168 return skipAtomicInner(doc, near, pos, dir, mayClear) 169 } 170 171 let far = m.find(dir < 0 ? -1 : 1) 172 if (dir < 0 ? m.inclusiveLeft : m.inclusiveRight) 173 far = movePos(doc, far, dir, far.line == pos.line ? line : null) 174 return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null 175 } 176 } 177 return pos 178 } 179 180 // Ensure a given position is not inside an atomic range. 181 export function skipAtomic(doc, pos, oldPos, bias, mayClear) { 182 let dir = bias || 1 183 let found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) || 184 (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) || 185 skipAtomicInner(doc, pos, oldPos, -dir, mayClear) || 186 (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true)) 187 if (!found) { 188 doc.cantEdit = true 189 return Pos(doc.first, 0) 190 } 191 return found 192 } 193 194 function movePos(doc, pos, dir, line) { 195 if (dir < 0 && pos.ch == 0) { 196 if (pos.line > doc.first) return clipPos(doc, Pos(pos.line - 1)) 197 else return null 198 } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) { 199 if (pos.line < doc.first + doc.size - 1) return Pos(pos.line + 1, 0) 200 else return null 201 } else { 202 return new Pos(pos.line, pos.ch + dir) 203 } 204 } 205 206 export function selectAll(cm) { 207 cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll) 208 }