1513821720d79dc12868224bee3d3d4c386be495d378c6c7a8c1ae09c26415e043d27733de2b49e664e75522f92943ef82b43682b4988d4b32af9ae3b304b0 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import { Pos } from "../line/pos.js"
  2. import { prepareMeasureForLine, measureCharPrepared, wrappedLineExtentChar } from "../measurement/position_measurement.js"
  3. import { getBidiPartAt, getOrder } from "../util/bidi.js"
  4. import { findFirst, lst, skipExtendingChars } from "../util/misc.js"
  5. function moveCharLogically(line, ch, dir) {
  6. let target = skipExtendingChars(line.text, ch + dir, dir)
  7. return target < 0 || target > line.text.length ? null : target
  8. }
  9. export function moveLogically(line, start, dir) {
  10. let ch = moveCharLogically(line, start.ch, dir)
  11. return ch == null ? null : new Pos(start.line, ch, dir < 0 ? "after" : "before")
  12. }
  13. export function endOfLine(visually, cm, lineObj, lineNo, dir) {
  14. if (visually) {
  15. if (cm.doc.direction == "rtl") dir = -dir
  16. let order = getOrder(lineObj, cm.doc.direction)
  17. if (order) {
  18. let part = dir < 0 ? lst(order) : order[0]
  19. let moveInStorageOrder = (dir < 0) == (part.level == 1)
  20. let sticky = moveInStorageOrder ? "after" : "before"
  21. let ch
  22. // With a wrapped rtl chunk (possibly spanning multiple bidi parts),
  23. // it could be that the last bidi part is not on the last visual line,
  24. // since visual lines contain content order-consecutive chunks.
  25. // Thus, in rtl, we are looking for the first (content-order) character
  26. // in the rtl chunk that is on the last line (that is, the same line
  27. // as the last (content-order) character).
  28. if (part.level > 0 || cm.doc.direction == "rtl") {
  29. let prep = prepareMeasureForLine(cm, lineObj)
  30. ch = dir < 0 ? lineObj.text.length - 1 : 0
  31. let targetTop = measureCharPrepared(cm, prep, ch).top
  32. ch = findFirst(ch => measureCharPrepared(cm, prep, ch).top == targetTop, (dir < 0) == (part.level == 1) ? part.from : part.to - 1, ch)
  33. if (sticky == "before") ch = moveCharLogically(lineObj, ch, 1)
  34. } else ch = dir < 0 ? part.to : part.from
  35. return new Pos(lineNo, ch, sticky)
  36. }
  37. }
  38. return new Pos(lineNo, dir < 0 ? lineObj.text.length : 0, dir < 0 ? "before" : "after")
  39. }
  40. export function moveVisually(cm, line, start, dir) {
  41. let bidi = getOrder(line, cm.doc.direction)
  42. if (!bidi) return moveLogically(line, start, dir)
  43. if (start.ch >= line.text.length) {
  44. start.ch = line.text.length
  45. start.sticky = "before"
  46. } else if (start.ch <= 0) {
  47. start.ch = 0
  48. start.sticky = "after"
  49. }
  50. let partPos = getBidiPartAt(bidi, start.ch, start.sticky), part = bidi[partPos]
  51. if (cm.doc.direction == "ltr" && part.level % 2 == 0 && (dir > 0 ? part.to > start.ch : part.from < start.ch)) {
  52. // Case 1: We move within an ltr part in an ltr editor. Even with wrapped lines,
  53. // nothing interesting happens.
  54. return moveLogically(line, start, dir)
  55. }
  56. let mv = (pos, dir) => moveCharLogically(line, pos instanceof Pos ? pos.ch : pos, dir)
  57. let prep
  58. let getWrappedLineExtent = ch => {
  59. if (!cm.options.lineWrapping) return {begin: 0, end: line.text.length}
  60. prep = prep || prepareMeasureForLine(cm, line)
  61. return wrappedLineExtentChar(cm, line, prep, ch)
  62. }
  63. let wrappedLineExtent = getWrappedLineExtent(start.sticky == "before" ? mv(start, -1) : start.ch)
  64. if (cm.doc.direction == "rtl" || part.level == 1) {
  65. let moveInStorageOrder = (part.level == 1) == (dir < 0)
  66. let ch = mv(start, moveInStorageOrder ? 1 : -1)
  67. if (ch != null && (!moveInStorageOrder ? ch >= part.from && ch >= wrappedLineExtent.begin : ch <= part.to && ch <= wrappedLineExtent.end)) {
  68. // Case 2: We move within an rtl part or in an rtl editor on the same visual line
  69. let sticky = moveInStorageOrder ? "before" : "after"
  70. return new Pos(start.line, ch, sticky)
  71. }
  72. }
  73. // Case 3: Could not move within this bidi part in this visual line, so leave
  74. // the current bidi part
  75. let searchInVisualLine = (partPos, dir, wrappedLineExtent) => {
  76. let getRes = (ch, moveInStorageOrder) => moveInStorageOrder
  77. ? new Pos(start.line, mv(ch, 1), "before")
  78. : new Pos(start.line, ch, "after")
  79. for (; partPos >= 0 && partPos < bidi.length; partPos += dir) {
  80. let part = bidi[partPos]
  81. let moveInStorageOrder = (dir > 0) == (part.level != 1)
  82. let ch = moveInStorageOrder ? wrappedLineExtent.begin : mv(wrappedLineExtent.end, -1)
  83. if (part.from <= ch && ch < part.to) return getRes(ch, moveInStorageOrder)
  84. ch = moveInStorageOrder ? part.from : mv(part.to, -1)
  85. if (wrappedLineExtent.begin <= ch && ch < wrappedLineExtent.end) return getRes(ch, moveInStorageOrder)
  86. }
  87. }
  88. // Case 3a: Look for other bidi parts on the same visual line
  89. let res = searchInVisualLine(partPos + dir, dir, wrappedLineExtent)
  90. if (res) return res
  91. // Case 3b: Look for other bidi parts on the next visual line
  92. let nextCh = dir > 0 ? wrappedLineExtent.end : mv(wrappedLineExtent.begin, -1)
  93. if (nextCh != null && !(dir > 0 && nextCh == line.text.length)) {
  94. res = searchInVisualLine(dir > 0 ? 0 : bidi.length - 1, dir, getWrappedLineExtent(nextCh))
  95. if (res) return res
  96. }
  97. // Case 4: Nowhere to move
  98. return null
  99. }