| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- import { EditorView } from '@codemirror/view'
- import { EditorSelection, Text } from '@codemirror/state'
- import { selectNextOccurrence, SearchCursor } from '@codemirror/search'
- type Spec = {
- caseSensitive?: boolean
- unquoted: string
- }
- const stringCursor = (spec: Spec, doc: Text, from: number, to: number) => {
- return new SearchCursor(
- doc,
- spec.unquoted,
- from,
- to,
- spec.caseSensitive ? undefined : x => x.toLowerCase()
- )
- }
- class QueryType {
- protected spec
- constructor(spec: Spec) {
- this.spec = spec
- }
- }
- class StringQuery extends QueryType {
- // Searching in reverse is, rather than implementing inverted search
- // cursor, done by scanning chunk after chunk forward.
- prevMatchInRange(doc: Text, from: number, to: number) {
- for (let pos = to; ; ) {
- const start = Math.max(
- from,
- pos - 10000 /* ChunkSize */ - this.spec.unquoted.length
- )
- const cursor = stringCursor(this.spec, doc, start, pos)
- let range = null
- while (!cursor.nextOverlapping().done) {
- range = cursor.value
- }
- if (range) {
- return range
- }
- if (start === from) {
- return null
- }
- pos -= 10000 /* ChunkSize */
- }
- }
- prevMatch(doc: Text, curFrom: number, curTo: number) {
- return (
- this.prevMatchInRange(doc, 0, curFrom) ||
- this.prevMatchInRange(doc, curTo, doc.length)
- )
- }
- }
- const selectWord = (view: EditorView) => {
- const { selection } = view.state
- const newSelection = EditorSelection.create(
- selection.ranges.map(
- range =>
- view.state.wordAt(range.head) || EditorSelection.cursor(range.head)
- ),
- selection.mainIndex
- )
- if (newSelection.eq(selection)) {
- return false
- }
- view.dispatch(view.state.update({ selection: newSelection }))
- return true
- }
- const selectPrevOccurrence = (view: EditorView) => {
- const { state } = view
- const { ranges } = state.selection
- if (ranges.some(range => range.from === range.to)) {
- return selectWord(view)
- }
- const searchedText = state.sliceDoc(ranges[0].from, ranges[0].to)
- if (
- state.selection.ranges.some(
- range => state.sliceDoc(range.from, range.to) !== searchedText
- )
- ) {
- return false
- }
- const query = new StringQuery({ unquoted: searchedText })
- const { main } = state.selection
- const range = query.prevMatch(state.doc, main.from, main.to)
- if (!range) {
- return false
- }
- view.dispatch({
- selection: state.selection.addRange(
- EditorSelection.range(range.from, range.to)
- ),
- effects: EditorView.scrollIntoView(range.to),
- })
- return true
- }
- export const selectOccurrence = (forward: boolean) => (view: EditorView) =>
- forward ? selectNextOccurrence(view) : selectPrevOccurrence(view)
|