mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
7f37ba737c
* 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
120 lines
2.7 KiB
TypeScript
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)
|