overleaf/services/web/frontend/js/features/source-editor/commands/select.ts
Tim Down 7f37ba737c Move source editor out of module (#12457)
* Update Copybara options in preparation for open-sourcing the source editor

* Move files

* Update paths

* Remove source-editor module and checks for its existence

* Explicitly mention CM6 license in files that contain code adapted from CM6

GitOrigin-RevId: 89b7cc2b409db01ad103198ccbd1b126ab56349b
2023-04-13 08:40:56 +00:00

120 lines
2.7 KiB
TypeScript

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)