b6bc0fcaa5b883c8efe58c443d081c1e36f5628c9734f834ef1480908abf1a04b918dffb2910078cd0c0c30587f7fc8a1e45f63e2d40f0f40a90be55f979ce 7.9 KB

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