select.ts 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import { EditorView } from '@codemirror/view'
  2. import { EditorSelection, Text } from '@codemirror/state'
  3. import { selectNextOccurrence, SearchCursor } from '@codemirror/search'
  4. type Spec = {
  5. caseSensitive?: boolean
  6. unquoted: string
  7. }
  8. const stringCursor = (spec: Spec, doc: Text, from: number, to: number) => {
  9. return new SearchCursor(
  10. doc,
  11. spec.unquoted,
  12. from,
  13. to,
  14. spec.caseSensitive ? undefined : x => x.toLowerCase()
  15. )
  16. }
  17. class QueryType {
  18. protected spec
  19. constructor(spec: Spec) {
  20. this.spec = spec
  21. }
  22. }
  23. class StringQuery extends QueryType {
  24. // Searching in reverse is, rather than implementing inverted search
  25. // cursor, done by scanning chunk after chunk forward.
  26. prevMatchInRange(doc: Text, from: number, to: number) {
  27. for (let pos = to; ; ) {
  28. const start = Math.max(
  29. from,
  30. pos - 10000 /* ChunkSize */ - this.spec.unquoted.length
  31. )
  32. const cursor = stringCursor(this.spec, doc, start, pos)
  33. let range = null
  34. while (!cursor.nextOverlapping().done) {
  35. range = cursor.value
  36. }
  37. if (range) {
  38. return range
  39. }
  40. if (start === from) {
  41. return null
  42. }
  43. pos -= 10000 /* ChunkSize */
  44. }
  45. }
  46. prevMatch(doc: Text, curFrom: number, curTo: number) {
  47. return (
  48. this.prevMatchInRange(doc, 0, curFrom) ||
  49. this.prevMatchInRange(doc, curTo, doc.length)
  50. )
  51. }
  52. }
  53. const selectWord = (view: EditorView) => {
  54. const { selection } = view.state
  55. const newSelection = EditorSelection.create(
  56. selection.ranges.map(
  57. range =>
  58. view.state.wordAt(range.head) || EditorSelection.cursor(range.head)
  59. ),
  60. selection.mainIndex
  61. )
  62. if (newSelection.eq(selection)) {
  63. return false
  64. }
  65. view.dispatch(view.state.update({ selection: newSelection }))
  66. return true
  67. }
  68. const selectPrevOccurrence = (view: EditorView) => {
  69. const { state } = view
  70. const { ranges } = state.selection
  71. if (ranges.some(range => range.from === range.to)) {
  72. return selectWord(view)
  73. }
  74. const searchedText = state.sliceDoc(ranges[0].from, ranges[0].to)
  75. if (
  76. state.selection.ranges.some(
  77. range => state.sliceDoc(range.from, range.to) !== searchedText
  78. )
  79. ) {
  80. return false
  81. }
  82. const query = new StringQuery({ unquoted: searchedText })
  83. const { main } = state.selection
  84. const range = query.prevMatch(state.doc, main.from, main.to)
  85. if (!range) {
  86. return false
  87. }
  88. view.dispatch({
  89. selection: state.selection.addRange(
  90. EditorSelection.range(range.from, range.to)
  91. ),
  92. effects: EditorView.scrollIntoView(range.to),
  93. })
  94. return true
  95. }
  96. export const selectOccurrence = (forward: boolean) => (view: EditorView) =>
  97. forward ? selectNextOccurrence(view) : selectPrevOccurrence(view)