mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Ensure that the correct lines are spell-checked (#19967)
GitOrigin-RevId: f63601288821d37bddcddc776270263e291d3985
This commit is contained in:
parent
607b3e3494
commit
abfa29e629
5 changed files with 95 additions and 654 deletions
|
@ -1,142 +0,0 @@
|
||||||
import OError from '@overleaf/o-error'
|
|
||||||
import { Text } from '@codemirror/state'
|
|
||||||
import { Word } from './spellchecker'
|
|
||||||
import { ViewUpdate } from '@codemirror/view'
|
|
||||||
|
|
||||||
export class LineTracker {
|
|
||||||
private _lines: boolean[]
|
|
||||||
constructor(doc: Text) {
|
|
||||||
/*
|
|
||||||
* Maintain an array of booleans, one for each line of the document.
|
|
||||||
* `true` means a line has changed
|
|
||||||
*/
|
|
||||||
this._lines = new Array(doc.lines).fill(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
dump() {
|
|
||||||
return [...this._lines]
|
|
||||||
}
|
|
||||||
|
|
||||||
count() {
|
|
||||||
return this._lines.length
|
|
||||||
}
|
|
||||||
|
|
||||||
lineHasChanged(lineNumber: number) {
|
|
||||||
return this._lines[lineNumber - 1] === true
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Given a list of words, clear the 'changed' mark
|
|
||||||
* on the lines the words are on
|
|
||||||
*/
|
|
||||||
clearChangedLinesForWords(words: Word[]) {
|
|
||||||
words.forEach(word => {
|
|
||||||
this.clearLine(word.lineNumber)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
clearLine(lineNumber: number) {
|
|
||||||
this._lines[lineNumber - 1] = false
|
|
||||||
}
|
|
||||||
|
|
||||||
clearAllLines() {
|
|
||||||
this._lines = this._lines.map(() => false)
|
|
||||||
}
|
|
||||||
|
|
||||||
resetAllLines() {
|
|
||||||
this._lines = this._lines.map(() => true)
|
|
||||||
}
|
|
||||||
|
|
||||||
markLineAsUpdated(lineNumber: number) {
|
|
||||||
this._lines[lineNumber - 1] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* On update, for all changes, mark the affected lines
|
|
||||||
* as changed
|
|
||||||
*/
|
|
||||||
applyUpdate(update: ViewUpdate) {
|
|
||||||
for (const transaction of update.transactions) {
|
|
||||||
if (transaction.docChanged) {
|
|
||||||
let lineShift = 0
|
|
||||||
transaction.changes.iterChanges(
|
|
||||||
(fromA, toA, fromB, toB, insertedText) => {
|
|
||||||
const insertedLength = insertedText.length
|
|
||||||
const removedLength = toA - fromA
|
|
||||||
const hasInserted = insertedLength > 0
|
|
||||||
const hasRemoved = removedLength > 0
|
|
||||||
const oldDoc = transaction.startState.doc
|
|
||||||
if (hasRemoved) {
|
|
||||||
const startLine = oldDoc.lineAt(fromA).number
|
|
||||||
const endLine = oldDoc.lineAt(toA).number
|
|
||||||
/* Mark start line as changed, and remove deleted lines
|
|
||||||
* Example:
|
|
||||||
* with this text:
|
|
||||||
* |1|aaaa|
|
|
||||||
* |2|bbbb| => [false, false, false, false]
|
|
||||||
* |3|cccc|
|
|
||||||
* |4|dddd|
|
|
||||||
*
|
|
||||||
* with a selection covering 'bbcccc' across lines 2 and 3,
|
|
||||||
* press backspace,
|
|
||||||
* resulting in:
|
|
||||||
* |1|aaaa|
|
|
||||||
* |2|bb| => [false, true, false]
|
|
||||||
* |3|dddd|
|
|
||||||
*/
|
|
||||||
this._lines.splice(
|
|
||||||
startLine - 1 + lineShift,
|
|
||||||
endLine - startLine + 1,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
lineShift -= endLine - startLine
|
|
||||||
}
|
|
||||||
if (hasInserted) {
|
|
||||||
const startLine = oldDoc.lineAt(fromA).number
|
|
||||||
/* Mark start line as changed, and insert new (changed) lines after.
|
|
||||||
* Example:
|
|
||||||
* with this text:
|
|
||||||
* |1|aaaa|
|
|
||||||
* |2|bbbb| => [false, false, false]
|
|
||||||
* |3|cccc|
|
|
||||||
*
|
|
||||||
* with the cursor at the end of line 2,
|
|
||||||
* insert the following text:
|
|
||||||
* |1|xx|
|
|
||||||
* |2|yy|
|
|
||||||
*
|
|
||||||
* results in:
|
|
||||||
* |1|aaaa|
|
|
||||||
* |2|bbbbxx| => [false, true, true, false]
|
|
||||||
* |3|yy|
|
|
||||||
* |4|cccc|
|
|
||||||
*/
|
|
||||||
|
|
||||||
const changes = new Array(insertedText.lines).fill(true)
|
|
||||||
try {
|
|
||||||
this._lines.splice(startLine - 1 + lineShift, 1, ...changes)
|
|
||||||
lineShift += changes.length - 1
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof RangeError) {
|
|
||||||
throw new OError(error.message).withInfo({
|
|
||||||
changesSize: changes.length,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (update.state.doc.lines !== this._lines.length) {
|
|
||||||
throw new OError(
|
|
||||||
'LineTracker length does not match document line count'
|
|
||||||
).withInfo({
|
|
||||||
documentLines: update.state.doc.lines,
|
|
||||||
trackerLines: this._lines.length,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { StateField, StateEffect, Transaction } from '@codemirror/state'
|
import { StateField, StateEffect } from '@codemirror/state'
|
||||||
import { EditorView, Decoration, DecorationSet } from '@codemirror/view'
|
import { EditorView, Decoration, DecorationSet } from '@codemirror/view'
|
||||||
import { updateAfterAddingIgnoredWord } from './ignored-words'
|
import { updateAfterAddingIgnoredWord } from './ignored-words'
|
||||||
import _ from 'lodash'
|
|
||||||
import { Word } from './spellchecker'
|
import { Word } from './spellchecker'
|
||||||
|
|
||||||
export const addMisspelledWords = StateEffect.define<Word[]>()
|
export const addMisspelledWords = StateEffect.define<Word[]>()
|
||||||
|
@ -9,11 +8,10 @@ export const addMisspelledWords = StateEffect.define<Word[]>()
|
||||||
export const resetMisspelledWords = StateEffect.define()
|
export const resetMisspelledWords = StateEffect.define()
|
||||||
|
|
||||||
const createMark = (word: Word) => {
|
const createMark = (word: Word) => {
|
||||||
const mark = Decoration.mark({
|
return Decoration.mark({
|
||||||
class: 'ol-cm-spelling-error',
|
class: 'ol-cm-spelling-error',
|
||||||
word,
|
word,
|
||||||
})
|
}).range(word.from, word.to)
|
||||||
return mark.range(word.from, word.to)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -26,13 +24,24 @@ export const misspelledWordsField = StateField.define<DecorationSet>({
|
||||||
return Decoration.none
|
return Decoration.none
|
||||||
},
|
},
|
||||||
update(marks, transaction) {
|
update(marks, transaction) {
|
||||||
|
if (transaction.docChanged) {
|
||||||
|
// Remove any marks whose text has just been edited
|
||||||
|
marks = marks.update({
|
||||||
|
filter(from, to) {
|
||||||
|
return !transaction.changes.touchesRange(from, to)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
marks = marks.map(transaction.changes)
|
marks = marks.map(transaction.changes)
|
||||||
marks = removeMarksUnderEdit(marks, transaction)
|
|
||||||
for (const effect of transaction.effects) {
|
for (const effect of transaction.effects) {
|
||||||
if (effect.is(addMisspelledWords)) {
|
if (effect.is(addMisspelledWords)) {
|
||||||
// We're setting a new list of misspelled words
|
// Merge the new misspelled words into the existing set of marks
|
||||||
const misspelledWords = effect.value
|
marks = marks.update({
|
||||||
marks = mergeMarks(marks, misspelledWords)
|
add: effect.value.map(word => createMark(word)),
|
||||||
|
sort: true,
|
||||||
|
})
|
||||||
} else if (effect.is(updateAfterAddingIgnoredWord)) {
|
} else if (effect.is(updateAfterAddingIgnoredWord)) {
|
||||||
// Remove a misspelled word, all instances that match text
|
// Remove a misspelled word, all instances that match text
|
||||||
const word = effect.value
|
const word = effect.value
|
||||||
|
@ -48,46 +57,6 @@ export const misspelledWordsField = StateField.define<DecorationSet>({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
|
||||||
* Remove any marks whos text has just been edited
|
|
||||||
*/
|
|
||||||
const removeMarksUnderEdit = (
|
|
||||||
marks: DecorationSet,
|
|
||||||
transaction: Transaction
|
|
||||||
) => {
|
|
||||||
transaction.changes.iterChanges((fromA, toA, fromB, toB) => {
|
|
||||||
marks = marks.update({
|
|
||||||
// Filter out marks that overlap the change span
|
|
||||||
filter: (from, to, mark) => {
|
|
||||||
const changeStartWithinMark = from <= fromB && to >= fromB
|
|
||||||
const changeEndWithinMark = from <= toB && to >= toB
|
|
||||||
const markHasBeenEdited = changeStartWithinMark || changeEndWithinMark
|
|
||||||
return !markHasBeenEdited
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return marks
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Given the set of marks, and a list of new misspelled-words,
|
|
||||||
* merge these together into a new set of marks
|
|
||||||
*/
|
|
||||||
const mergeMarks = (marks: DecorationSet, words: Word[]) => {
|
|
||||||
const affectedLines = new Set(words.map(w => w.lineNumber))
|
|
||||||
marks = marks
|
|
||||||
.update({
|
|
||||||
filter: (from, to, mark) => {
|
|
||||||
return !affectedLines.has(mark.spec.word.lineNumber)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.update({
|
|
||||||
add: _.sortBy(words, ['from']).map(w => createMark(w)),
|
|
||||||
sort: true,
|
|
||||||
})
|
|
||||||
return marks
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Remove existing marks matching the text of a supplied word
|
* Remove existing marks matching the text of a supplied word
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { addMisspelledWords, misspelledWordsField } from './misspelled-words'
|
import { addMisspelledWords, misspelledWordsField } from './misspelled-words'
|
||||||
import { ignoredWordsField, resetSpellChecker } from './ignored-words'
|
import { ignoredWordsField, resetSpellChecker } from './ignored-words'
|
||||||
import { LineTracker } from './line-tracker'
|
|
||||||
import { cacheField, addWordToCache, WordCacheValue } from './cache'
|
import { cacheField, addWordToCache, WordCacheValue } from './cache'
|
||||||
import { WORD_REGEX } from './helpers'
|
import { WORD_REGEX } from './helpers'
|
||||||
import OError from '@overleaf/o-error'
|
import OError from '@overleaf/o-error'
|
||||||
import { spellCheckRequest } from './backend'
|
import { spellCheckRequest } from './backend'
|
||||||
import { EditorView, ViewUpdate } from '@codemirror/view'
|
import { EditorView, ViewUpdate } from '@codemirror/view'
|
||||||
import { Line, Range, RangeValue } from '@codemirror/state'
|
import { ChangeSet, Line, Range, RangeValue } from '@codemirror/state'
|
||||||
import { IgnoredWords } from '../../../dictionary/ignored-words'
|
import { IgnoredWords } from '../../../dictionary/ignored-words'
|
||||||
import {
|
import {
|
||||||
getNormalTextSpansFromLine,
|
getNormalTextSpansFromLine,
|
||||||
|
@ -15,10 +14,6 @@ import {
|
||||||
import { waitForParser } from '../wait-for-parser'
|
import { waitForParser } from '../wait-for-parser'
|
||||||
import { debugConsole } from '@/utils/debugging'
|
import { debugConsole } from '@/utils/debugging'
|
||||||
|
|
||||||
const _log = (...args: any) => {
|
|
||||||
debugConsole.debug('[SpellChecker]: ', ...args)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Spellchecker, handles updates, schedules spelling checks
|
* Spellchecker, handles updates, schedules spelling checks
|
||||||
*/
|
*/
|
||||||
|
@ -26,43 +21,41 @@ export class SpellChecker {
|
||||||
private abortController?: AbortController | null = null
|
private abortController?: AbortController | null = null
|
||||||
private timeout: number | null = null
|
private timeout: number | null = null
|
||||||
private firstCheck = true
|
private firstCheck = true
|
||||||
private lineTracker: LineTracker | null = null
|
|
||||||
private waitingForParser = false
|
private waitingForParser = false
|
||||||
private firstCheckPending = false
|
private firstCheckPending = false
|
||||||
|
private trackedChanges: ChangeSet
|
||||||
|
|
||||||
// eslint-disable-next-line no-useless-constructor
|
// eslint-disable-next-line no-useless-constructor
|
||||||
constructor(private readonly language: string) {
|
constructor(private readonly language: string) {
|
||||||
this.language = language
|
this.language = language
|
||||||
|
this.trackedChanges = ChangeSet.empty(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
_log('destroy')
|
|
||||||
this._clearPendingSpellCheck()
|
this._clearPendingSpellCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
_abortRequest() {
|
_abortRequest() {
|
||||||
if (this.abortController) {
|
if (this.abortController) {
|
||||||
_log('abort request')
|
|
||||||
this.abortController.abort()
|
this.abortController.abort()
|
||||||
this.abortController = null
|
this.abortController = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdate(update: ViewUpdate) {
|
handleUpdate(update: ViewUpdate) {
|
||||||
if (!this.lineTracker) {
|
|
||||||
this.lineTracker = new LineTracker(update.state.doc)
|
|
||||||
}
|
|
||||||
if (update.docChanged) {
|
if (update.docChanged) {
|
||||||
this.lineTracker.applyUpdate(update)
|
this.trackedChanges = this.trackedChanges.compose(update.changes)
|
||||||
this.scheduleSpellCheck(update.view)
|
this.scheduleSpellCheck(update.view)
|
||||||
} else if (update.viewportChanged) {
|
} else if (update.viewportChanged) {
|
||||||
|
this.trackedChanges = ChangeSet.empty(0)
|
||||||
this.scheduleSpellCheck(update.view)
|
this.scheduleSpellCheck(update.view)
|
||||||
} else if (
|
} else if (
|
||||||
update.transactions.some(tr => {
|
update.transactions.some(tr => {
|
||||||
return tr.effects.some(effect => effect.is(resetSpellChecker))
|
return tr.effects.some(effect => effect.is(resetSpellChecker))
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
this.lineTracker.resetAllLines()
|
// for tests
|
||||||
|
this.trackedChanges = ChangeSet.empty(0)
|
||||||
this.spellCheckAsap(update.view)
|
this.spellCheckAsap(update.view)
|
||||||
}
|
}
|
||||||
// At the point that the spellchecker is initialized, the editor may not
|
// At the point that the spellchecker is initialized, the editor may not
|
||||||
|
@ -78,21 +71,15 @@ export class SpellChecker {
|
||||||
update.state.facet(EditorView.editable)
|
update.state.facet(EditorView.editable)
|
||||||
) {
|
) {
|
||||||
this.firstCheckPending = true
|
this.firstCheckPending = true
|
||||||
_log('Scheduling initial spellcheck')
|
|
||||||
this.spellCheckAsap(update.view)
|
this.spellCheckAsap(update.view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_performSpellCheck(view: EditorView) {
|
_performSpellCheck(view: EditorView) {
|
||||||
_log('Begin ---------------->')
|
|
||||||
const wordsToCheck = this.getWordsToCheck(view)
|
const wordsToCheck = this.getWordsToCheck(view)
|
||||||
if (wordsToCheck.length === 0) {
|
if (wordsToCheck.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_log(
|
|
||||||
'- words to check',
|
|
||||||
wordsToCheck.map(w => w.text)
|
|
||||||
)
|
|
||||||
const cache = view.state.field(cacheField)
|
const cache = view.state.field(cacheField)
|
||||||
const { knownMisspelledWords, unknownWords } = cache.checkWords(
|
const { knownMisspelledWords, unknownWords } = cache.checkWords(
|
||||||
this.language,
|
this.language,
|
||||||
|
@ -101,7 +88,8 @@ export class SpellChecker {
|
||||||
const processResult = (
|
const processResult = (
|
||||||
misspellings: { index: number; suggestions: string[] }[]
|
misspellings: { index: number; suggestions: string[] }[]
|
||||||
) => {
|
) => {
|
||||||
this.lineTracker?.clearChangedLinesForWords(wordsToCheck)
|
this.trackedChanges = ChangeSet.empty(0)
|
||||||
|
|
||||||
if (this.firstCheck) {
|
if (this.firstCheck) {
|
||||||
this.firstCheck = false
|
this.firstCheck = false
|
||||||
this.firstCheckPending = false
|
this.firstCheckPending = false
|
||||||
|
@ -111,25 +99,13 @@ export class SpellChecker {
|
||||||
unknownWords,
|
unknownWords,
|
||||||
misspellings
|
misspellings
|
||||||
)
|
)
|
||||||
_log('- result', result)
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
effects: compileEffects(result),
|
effects: compileEffects(result),
|
||||||
})
|
})
|
||||||
}, 0)
|
}, 0)
|
||||||
_log('<---------------- End')
|
|
||||||
}
|
}
|
||||||
_log('- before spellcheck request')
|
|
||||||
_log(
|
|
||||||
' - unknownWords',
|
|
||||||
unknownWords.map(w => w.text)
|
|
||||||
)
|
|
||||||
_log(
|
|
||||||
' - knownMisspelledWords',
|
|
||||||
knownMisspelledWords.map(w => w.text)
|
|
||||||
)
|
|
||||||
if (unknownWords.length === 0) {
|
if (unknownWords.length === 0) {
|
||||||
_log('- skip request')
|
|
||||||
processResult([])
|
processResult([])
|
||||||
} else {
|
} else {
|
||||||
this._abortRequest()
|
this._abortRequest()
|
||||||
|
@ -141,7 +117,7 @@ export class SpellChecker {
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.abortController = null
|
this.abortController = null
|
||||||
_log('>> error in spellcheck request', error)
|
debugConsole.error(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -152,10 +128,7 @@ export class SpellChecker {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.waitingForParser = true
|
this.waitingForParser = true
|
||||||
waitForParser(
|
waitForParser(view, view => view.viewport.to).then(() => {
|
||||||
view,
|
|
||||||
view => viewportRangeToCheck(this.firstCheck, view).to
|
|
||||||
).then(() => {
|
|
||||||
this.waitingForParser = false
|
this.waitingForParser = false
|
||||||
this._performSpellCheck(view)
|
this._performSpellCheck(view)
|
||||||
})
|
})
|
||||||
|
@ -187,22 +160,36 @@ export class SpellChecker {
|
||||||
}
|
}
|
||||||
|
|
||||||
getWordsToCheck(view: EditorView) {
|
getWordsToCheck(view: EditorView) {
|
||||||
const lang = this.language
|
const wordsToCheck: Word[] = []
|
||||||
const ignoredWords = view.state.field(ignoredWordsField)
|
|
||||||
_log('- ignored words', ignoredWords)
|
const { from, to } = view.viewport
|
||||||
if (!this.lineTracker) {
|
const changedLineNumbers = new Set<number>()
|
||||||
this.lineTracker = new LineTracker(view.state.doc)
|
if (this.trackedChanges.length > 0) {
|
||||||
|
this.trackedChanges.iterChangedRanges((fromA, toA, fromB, toB) => {
|
||||||
|
if (fromB <= to && toB >= from) {
|
||||||
|
const fromLine = view.state.doc.lineAt(fromB).number
|
||||||
|
const toLine = view.state.doc.lineAt(toB).number
|
||||||
|
for (let i = fromLine; i <= toLine; i++) {
|
||||||
|
changedLineNumbers.add(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const fromLine = view.state.doc.lineAt(from).number
|
||||||
|
const toLine = view.state.doc.lineAt(to).number
|
||||||
|
for (let i = fromLine; i <= toLine; i++) {
|
||||||
|
changedLineNumbers.add(i)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let wordsToCheck: Word[] = []
|
|
||||||
for (const line of viewportLinesToCheck(
|
const ignoredWords = view.state.field(ignoredWordsField)
|
||||||
this.lineTracker,
|
for (const i of changedLineNumbers) {
|
||||||
this.firstCheck,
|
const line = view.state.doc.line(i)
|
||||||
view
|
wordsToCheck.push(
|
||||||
)) {
|
...getWordsFromLine(view, line, ignoredWords, this.language)
|
||||||
wordsToCheck = wordsToCheck.concat(
|
|
||||||
getWordsFromLine(view, line, ignoredWords, lang)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return wordsToCheck
|
return wordsToCheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -246,34 +233,29 @@ export const buildSpellCheckResult = (
|
||||||
misspellings: { index: number; suggestions: string[] }[]
|
misspellings: { index: number; suggestions: string[] }[]
|
||||||
) => {
|
) => {
|
||||||
const cacheAdditions: [Word, string[] | boolean][] = []
|
const cacheAdditions: [Word, string[] | boolean][] = []
|
||||||
|
|
||||||
// Put known misspellings into cache
|
// Put known misspellings into cache
|
||||||
const misspelledWords = misspellings.map(item => {
|
const misspelledWords = misspellings.map(item => {
|
||||||
const word = { ...unknownWords[item.index] }
|
const word = {
|
||||||
|
...unknownWords[item.index],
|
||||||
|
}
|
||||||
word.suggestions = item.suggestions
|
word.suggestions = item.suggestions
|
||||||
if (word.suggestions) {
|
if (word.suggestions) {
|
||||||
cacheAdditions.push([word, word.suggestions])
|
cacheAdditions.push([word, word.suggestions])
|
||||||
}
|
}
|
||||||
return word
|
return word
|
||||||
})
|
})
|
||||||
|
|
||||||
// if word was not misspelled, put it in the cache
|
// if word was not misspelled, put it in the cache
|
||||||
for (const word of unknownWords) {
|
for (const word of unknownWords) {
|
||||||
if (!misspelledWords.find(mw => mw.text === word.text)) {
|
if (!misspelledWords.find(mw => mw.text === word.text)) {
|
||||||
cacheAdditions.push([word, true])
|
cacheAdditions.push([word, true])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const finalMisspellings = misspelledWords.concat(knownMisspelledWords)
|
|
||||||
_log('- result')
|
|
||||||
_log(
|
|
||||||
' - finalMisspellings',
|
|
||||||
finalMisspellings.map(w => w.text)
|
|
||||||
)
|
|
||||||
_log(
|
|
||||||
' - cacheAdditions',
|
|
||||||
cacheAdditions.map(([w, v]) => `'${w.text}'=>${v}`)
|
|
||||||
)
|
|
||||||
return {
|
return {
|
||||||
cacheAdditions,
|
cacheAdditions,
|
||||||
misspelledWords: finalMisspellings,
|
misspelledWords: misspelledWords.concat(knownMisspelledWords),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,73 +276,20 @@ export const compileEffects = (results: {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewportRangeToCheck = (firstCheck: boolean, view: EditorView) => {
|
|
||||||
const doc = view.state.doc
|
|
||||||
let { from, to } = view.viewport
|
|
||||||
let firstLineNumber = doc.lineAt(from).number
|
|
||||||
let lastLineNumber = doc.lineAt(to).number
|
|
||||||
|
|
||||||
/*
|
|
||||||
* On first check, we scan the viewport plus some padding on either side.
|
|
||||||
* Then on subsequent checks we just scan the viewport
|
|
||||||
*/
|
|
||||||
if (firstCheck) {
|
|
||||||
const visibleLineCount = lastLineNumber - firstLineNumber + 1
|
|
||||||
const padding = Math.floor(visibleLineCount * 2)
|
|
||||||
firstLineNumber = Math.max(firstLineNumber - padding, 1)
|
|
||||||
lastLineNumber = Math.min(lastLineNumber + padding, doc.lines)
|
|
||||||
from = doc.line(firstLineNumber).from
|
|
||||||
to = doc.line(lastLineNumber).to
|
|
||||||
}
|
|
||||||
|
|
||||||
return { from, to, firstLineNumber, lastLineNumber }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const viewportLinesToCheck = (
|
|
||||||
lineTracker: LineTracker,
|
|
||||||
firstCheck: boolean,
|
|
||||||
view: EditorView
|
|
||||||
) => {
|
|
||||||
const { firstLineNumber, lastLineNumber } = viewportRangeToCheck(
|
|
||||||
firstCheck,
|
|
||||||
view
|
|
||||||
)
|
|
||||||
_log('- viewport lines', firstLineNumber, lastLineNumber)
|
|
||||||
const lines = []
|
|
||||||
for (
|
|
||||||
let lineNumber = firstLineNumber;
|
|
||||||
lineNumber <= lastLineNumber;
|
|
||||||
lineNumber++
|
|
||||||
) {
|
|
||||||
if (!lineTracker.lineHasChanged(lineNumber)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
lines.push(view.state.doc.line(lineNumber))
|
|
||||||
}
|
|
||||||
_log(
|
|
||||||
'- lines to check',
|
|
||||||
lines.map(l => l.number)
|
|
||||||
)
|
|
||||||
return lines
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getWordsFromLine = (
|
export const getWordsFromLine = (
|
||||||
view: EditorView,
|
view: EditorView,
|
||||||
line: Line,
|
line: Line,
|
||||||
ignoredWords: IgnoredWords,
|
ignoredWords: IgnoredWords,
|
||||||
lang: string
|
lang: string
|
||||||
): Word[] => {
|
): Word[] => {
|
||||||
const lineNumber = line.number
|
|
||||||
const normalTextSpans: Array<NormalTextSpan> = getNormalTextSpansFromLine(
|
const normalTextSpans: Array<NormalTextSpan> = getNormalTextSpansFromLine(
|
||||||
view,
|
view,
|
||||||
line
|
line
|
||||||
)
|
)
|
||||||
const words: Word[] = []
|
const words: Word[] = []
|
||||||
let regexResult
|
for (const span of normalTextSpans) {
|
||||||
normalTextSpans.forEach(span => {
|
for (const match of span.text.matchAll(WORD_REGEX)) {
|
||||||
WORD_REGEX.lastIndex = 0 // reset global stateful regexp for this usage
|
let word = match[0]
|
||||||
while ((regexResult = WORD_REGEX.exec(span.text))) {
|
|
||||||
let word = regexResult[0]
|
|
||||||
if (word.startsWith("'")) {
|
if (word.startsWith("'")) {
|
||||||
word = word.slice(1)
|
word = word.slice(1)
|
||||||
}
|
}
|
||||||
|
@ -368,18 +297,19 @@ export const getWordsFromLine = (
|
||||||
word = word.slice(0, -1)
|
word = word.slice(0, -1)
|
||||||
}
|
}
|
||||||
if (!ignoredWords.has(word)) {
|
if (!ignoredWords.has(word)) {
|
||||||
|
const from = span.from + match.index
|
||||||
words.push(
|
words.push(
|
||||||
new Word({
|
new Word({
|
||||||
text: word,
|
text: word,
|
||||||
from: span.from + regexResult.index,
|
from,
|
||||||
to: span.from + regexResult.index + word.length,
|
to: from + word.length,
|
||||||
lineNumber,
|
lineNumber: line.number,
|
||||||
lang,
|
lang,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
return words
|
return words
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,206 +0,0 @@
|
||||||
import { LineTracker } from '../../../../../../frontend/js/features/source-editor/extensions/spelling/line-tracker'
|
|
||||||
// import sinon from 'sinon'
|
|
||||||
import { expect } from 'chai'
|
|
||||||
import { EditorView } from '@codemirror/view'
|
|
||||||
import { EditorState } from '@codemirror/state'
|
|
||||||
import { Word } from '../../../../../../frontend/js/features/source-editor/extensions/spelling/spellchecker'
|
|
||||||
|
|
||||||
const doc = [
|
|
||||||
'Hello test one two',
|
|
||||||
'three four five six',
|
|
||||||
'seven eight nine.',
|
|
||||||
].join('\n')
|
|
||||||
|
|
||||||
describe('LineTracker', function () {
|
|
||||||
describe('basic operations', function () {
|
|
||||||
let state: EditorState,
|
|
||||||
view: EditorView,
|
|
||||||
lineTracker: LineTracker,
|
|
||||||
check: (spec: [number, any][]) => void
|
|
||||||
beforeEach(function () {
|
|
||||||
state = EditorState.create({
|
|
||||||
doc,
|
|
||||||
extensions: [
|
|
||||||
EditorView.updateListener.of(update => {
|
|
||||||
lineTracker.applyUpdate(update)
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
view = new EditorView({ state })
|
|
||||||
lineTracker = new LineTracker(view.state.doc)
|
|
||||||
check = spec => {
|
|
||||||
spec.forEach(([n, expectedValue]) => {
|
|
||||||
expect(lineTracker.lineHasChanged(n)).to.equal(expectedValue)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('start with the correct number of lines', function () {
|
|
||||||
expect(state.doc.lines).to.equal(3)
|
|
||||||
expect(lineTracker.count()).to.equal(state.doc.lines)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('starts with all lines marked as changed', function () {
|
|
||||||
expect(state.doc.lines).to.equal(3)
|
|
||||||
check([
|
|
||||||
[1, true],
|
|
||||||
[2, true],
|
|
||||||
[3, true],
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('clears a line', function () {
|
|
||||||
check([[1, true]])
|
|
||||||
lineTracker.clearLine(1)
|
|
||||||
check([[1, false]])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('clears lines based on a list of words', function () {
|
|
||||||
lineTracker.clearChangedLinesForWords([
|
|
||||||
{ lineNumber: 1 },
|
|
||||||
{ lineNumber: 3 },
|
|
||||||
] as Word[])
|
|
||||||
check([
|
|
||||||
[1, false],
|
|
||||||
[2, true],
|
|
||||||
[3, false],
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update lines in response to text insertion', function () {
|
|
||||||
lineTracker.clearChangedLinesForWords([
|
|
||||||
{ lineNumber: 1 },
|
|
||||||
{ lineNumber: 2 },
|
|
||||||
{ lineNumber: 3 },
|
|
||||||
] as Word[])
|
|
||||||
check([
|
|
||||||
[1, false],
|
|
||||||
[2, false],
|
|
||||||
[3, false],
|
|
||||||
])
|
|
||||||
|
|
||||||
let transaction = view.state.update({
|
|
||||||
changes: [{ from: 0, insert: 'x' }],
|
|
||||||
})
|
|
||||||
view.dispatch(transaction)
|
|
||||||
check([
|
|
||||||
[1, true],
|
|
||||||
[2, false],
|
|
||||||
[3, false],
|
|
||||||
])
|
|
||||||
|
|
||||||
transaction = view.state.update({
|
|
||||||
changes: [{ from: view.state.doc.length - 2, insert: 'x' }],
|
|
||||||
})
|
|
||||||
view.dispatch(transaction)
|
|
||||||
check([
|
|
||||||
[1, true],
|
|
||||||
[2, false],
|
|
||||||
[3, true],
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update lines in response to large text insertion', function () {
|
|
||||||
lineTracker.clearChangedLinesForWords([
|
|
||||||
{ lineNumber: 1 },
|
|
||||||
{ lineNumber: 2 },
|
|
||||||
{ lineNumber: 3 },
|
|
||||||
] as Word[])
|
|
||||||
check([
|
|
||||||
[1, false],
|
|
||||||
[2, false],
|
|
||||||
[3, false],
|
|
||||||
])
|
|
||||||
|
|
||||||
const text = new Array(1000).fill('x').join('\n')
|
|
||||||
|
|
||||||
const transaction = view.state.update({
|
|
||||||
changes: [{ from: 0, insert: text }],
|
|
||||||
})
|
|
||||||
view.dispatch(transaction)
|
|
||||||
expect(lineTracker.count()).to.equal(1002)
|
|
||||||
const expectations: [number, boolean][] = []
|
|
||||||
for (let i = 1; i <= 1000; i++) {
|
|
||||||
expectations.push([i, true])
|
|
||||||
}
|
|
||||||
expectations.push([1001, false])
|
|
||||||
expectations.push([1002, false])
|
|
||||||
check(expectations)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update lines in response to removal of a line', function () {
|
|
||||||
lineTracker.clearChangedLinesForWords([
|
|
||||||
{ lineNumber: 1 },
|
|
||||||
{ lineNumber: 2 },
|
|
||||||
{ lineNumber: 3 },
|
|
||||||
] as Word[])
|
|
||||||
check([
|
|
||||||
[1, false],
|
|
||||||
[2, false],
|
|
||||||
[3, false],
|
|
||||||
])
|
|
||||||
|
|
||||||
// Overwrite the line plus some part of the second line
|
|
||||||
const transaction = view.state.update({
|
|
||||||
changes: [{ from: 0, to: doc[0].length + 3, insert: 'x' }],
|
|
||||||
})
|
|
||||||
view.dispatch(transaction)
|
|
||||||
check([
|
|
||||||
[1, true],
|
|
||||||
[2, false],
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle multiple changes', function () {
|
|
||||||
lineTracker.clearChangedLinesForWords([
|
|
||||||
{ lineNumber: 1 },
|
|
||||||
{ lineNumber: 2 },
|
|
||||||
{ lineNumber: 3 },
|
|
||||||
] as Word[])
|
|
||||||
check([
|
|
||||||
[1, false],
|
|
||||||
[2, false],
|
|
||||||
[3, false],
|
|
||||||
])
|
|
||||||
|
|
||||||
const transaction = view.state.update({
|
|
||||||
changes: [
|
|
||||||
{ from: 0, insert: 'x' },
|
|
||||||
{ from: doc[0].length + 2, insert: 'xxxxx\nxxxxx\nxxxx' },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
view.dispatch(transaction)
|
|
||||||
check([
|
|
||||||
[1, true],
|
|
||||||
[2, true],
|
|
||||||
[3, true],
|
|
||||||
[4, false],
|
|
||||||
[5, false],
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle multiple deletions', function () {
|
|
||||||
const transaction = view.state.update({
|
|
||||||
changes: [
|
|
||||||
{ from: 0, to: 24, insert: '' },
|
|
||||||
{ from: 39, to: 44, insert: '' },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
view.dispatch(transaction)
|
|
||||||
check([
|
|
||||||
[1, true],
|
|
||||||
[2, true],
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle multiple insertions', function () {
|
|
||||||
const transactionUndo = view.state.update({
|
|
||||||
changes: [
|
|
||||||
{ from: 0, to: 0, insert: 'big change\n'.repeat(20) },
|
|
||||||
{ from: 50, to: 50, insert: 'xxxx' },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
view.dispatch(transactionUndo)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,29 +1,15 @@
|
||||||
import {
|
import {
|
||||||
getWordsFromLine,
|
getWordsFromLine,
|
||||||
viewportLinesToCheck,
|
|
||||||
buildSpellCheckResult,
|
buildSpellCheckResult,
|
||||||
Word,
|
Word,
|
||||||
} from '../../../../../../frontend/js/features/source-editor/extensions/spelling/spellchecker'
|
} from '@/features/source-editor/extensions/spelling/spellchecker'
|
||||||
import { LineTracker } from '../../../../../../frontend/js/features/source-editor/extensions/spelling/line-tracker'
|
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { EditorView } from '@codemirror/view'
|
import { EditorView } from '@codemirror/view'
|
||||||
import { EditorState, Line } from '@codemirror/state'
|
import { IgnoredWords } from '@/features/dictionary/ignored-words'
|
||||||
import _ from 'lodash'
|
import { LaTeXLanguage } from '@/features/source-editor/languages/latex/latex-language'
|
||||||
import { IgnoredWords } from '../../../../../../frontend/js/features/dictionary/ignored-words'
|
|
||||||
import { LaTeXLanguage } from '../../../../../../frontend/js/features/source-editor/languages/latex/latex-language'
|
|
||||||
import { LanguageSupport } from '@codemirror/language'
|
import { LanguageSupport } from '@codemirror/language'
|
||||||
|
|
||||||
const latex = new LanguageSupport(LaTeXLanguage)
|
const extensions = [new LanguageSupport(LaTeXLanguage)]
|
||||||
|
|
||||||
const makeView = (text: string) => {
|
|
||||||
const view = new EditorView({
|
|
||||||
state: EditorState.create({
|
|
||||||
doc: text,
|
|
||||||
extensions: [latex],
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('SpellChecker', function () {
|
describe('SpellChecker', function () {
|
||||||
describe('getWordsFromLine', function () {
|
describe('getWordsFromLine', function () {
|
||||||
|
@ -35,7 +21,10 @@ describe('SpellChecker', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should get words from a line', function () {
|
it('should get words from a line', function () {
|
||||||
const view = makeView('Hello test one two')
|
const view = new EditorView({
|
||||||
|
doc: 'Hello test one two',
|
||||||
|
extensions,
|
||||||
|
})
|
||||||
const line = view.state.doc.line(1)
|
const line = view.state.doc.line(1)
|
||||||
const words = getWordsFromLine(view, line, ignoredWords, lang)
|
const words = getWordsFromLine(view, line, ignoredWords, lang)
|
||||||
expect(words).to.deep.equal([
|
expect(words).to.deep.equal([
|
||||||
|
@ -48,7 +37,10 @@ describe('SpellChecker', function () {
|
||||||
|
|
||||||
it('should ignore words in ignoredWords', function () {
|
it('should ignore words in ignoredWords', function () {
|
||||||
ignoredWords = new Set(['test']) as unknown as IgnoredWords
|
ignoredWords = new Set(['test']) as unknown as IgnoredWords
|
||||||
const view = makeView('Hello test one two')
|
const view = new EditorView({
|
||||||
|
doc: 'Hello test one two',
|
||||||
|
extensions,
|
||||||
|
})
|
||||||
const line = view.state.doc.line(1)
|
const line = view.state.doc.line(1)
|
||||||
const words = getWordsFromLine(view, line, ignoredWords, lang)
|
const words = getWordsFromLine(view, line, ignoredWords, lang)
|
||||||
expect(words).to.deep.equal([
|
expect(words).to.deep.equal([
|
||||||
|
@ -59,14 +51,20 @@ describe('SpellChecker', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should get no words from an empty line', function () {
|
it('should get no words from an empty line', function () {
|
||||||
const view = makeView(' ')
|
const view = new EditorView({
|
||||||
|
doc: ' ',
|
||||||
|
extensions,
|
||||||
|
})
|
||||||
const line = view.state.doc.line(1)
|
const line = view.state.doc.line(1)
|
||||||
const words = getWordsFromLine(view, line, ignoredWords, lang)
|
const words = getWordsFromLine(view, line, ignoredWords, lang)
|
||||||
expect(words).to.deep.equal([])
|
expect(words).to.deep.equal([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should ignore content of some commands in the text', function () {
|
it('should ignore content of some commands in the text', function () {
|
||||||
const view = makeView('\\usepackage[foo]{ bar } seven eight')
|
const view = new EditorView({
|
||||||
|
doc: '\\usepackage[foo]{ bar } seven eight',
|
||||||
|
extensions,
|
||||||
|
})
|
||||||
const line = view.state.doc.line(1)
|
const line = view.state.doc.line(1)
|
||||||
const words = getWordsFromLine(view, line, ignoredWords, lang)
|
const words = getWordsFromLine(view, line, ignoredWords, lang)
|
||||||
expect(words).to.deep.equal([
|
expect(words).to.deep.equal([
|
||||||
|
@ -76,7 +74,10 @@ describe('SpellChecker', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should ignore command names in the text', function () {
|
it('should ignore command names in the text', function () {
|
||||||
const view = makeView('\\foo nine \\bar ten \\baz[]{}')
|
const view = new EditorView({
|
||||||
|
doc: '\\foo nine \\bar ten \\baz[]{}',
|
||||||
|
extensions,
|
||||||
|
})
|
||||||
const line = view.state.doc.line(1)
|
const line = view.state.doc.line(1)
|
||||||
const words = getWordsFromLine(view, line, ignoredWords, lang)
|
const words = getWordsFromLine(view, line, ignoredWords, lang)
|
||||||
expect(words).to.deep.equal([
|
expect(words).to.deep.equal([
|
||||||
|
@ -86,117 +87,6 @@ describe('SpellChecker', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('viewportLinesToCheck', function () {
|
|
||||||
const expectLines = (lines: Line[], expectations: any) => {
|
|
||||||
expect(lines.map(l => l.number)).to.deep.equal(expectations)
|
|
||||||
}
|
|
||||||
const expectLineRange = (lines: Line[], from: number, to: number) => {
|
|
||||||
expect(lines.map(l => l.number)).to.deep.equal(_.range(from, to + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
let view: EditorView
|
|
||||||
beforeEach(function () {
|
|
||||||
view = makeView(new Array(1000).fill('aa bb cc dd').join('\n'))
|
|
||||||
// Test preconditions on these structures
|
|
||||||
const viewport = view.viewport
|
|
||||||
expect(view.state.doc.lines).to.equal(1000)
|
|
||||||
const firstVisibleLine = view.state.doc.lineAt(viewport.from).number
|
|
||||||
const lastVisibleLine = view.state.doc.lineAt(viewport.to).number
|
|
||||||
expect(firstVisibleLine).to.equal(1)
|
|
||||||
expect(lastVisibleLine).to.equal(36)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when all lines are changed', function () {
|
|
||||||
let lineTracker: LineTracker
|
|
||||||
beforeEach(function () {
|
|
||||||
lineTracker = new LineTracker(view.state.doc)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should check all lines in the viewport when not first check', function () {
|
|
||||||
const firstCheck = false
|
|
||||||
const linesToCheck = viewportLinesToCheck(lineTracker, firstCheck, view)
|
|
||||||
expectLineRange(linesToCheck, 1, 36)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should check more lines than the viewport on first check', function () {
|
|
||||||
const firstCheck = true
|
|
||||||
const lineTracker = new LineTracker(view.state.doc)
|
|
||||||
const linesToCheck = viewportLinesToCheck(lineTracker, firstCheck, view)
|
|
||||||
expectLineRange(linesToCheck, 1, 108)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when no lines have changed', function () {
|
|
||||||
let lineTracker: LineTracker
|
|
||||||
beforeEach(function () {
|
|
||||||
lineTracker = new LineTracker(view.state.doc)
|
|
||||||
lineTracker.clearAllLines()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('on first check, should not check any lines', function () {
|
|
||||||
const firstCheck = true
|
|
||||||
const linesToCheck = viewportLinesToCheck(lineTracker, firstCheck, view)
|
|
||||||
expect(linesToCheck).to.deep.equal([])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('on not first check, should not check any lines', function () {
|
|
||||||
const firstCheck = false
|
|
||||||
const linesToCheck = viewportLinesToCheck(lineTracker, firstCheck, view)
|
|
||||||
expectLines(linesToCheck, [])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when some lines have changed in viewport', function () {
|
|
||||||
let lineTracker: LineTracker
|
|
||||||
beforeEach(function () {
|
|
||||||
lineTracker = new LineTracker(view.state.doc)
|
|
||||||
lineTracker.clearAllLines()
|
|
||||||
lineTracker.markLineAsUpdated(3)
|
|
||||||
lineTracker.markLineAsUpdated(7)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should check correct lines', function () {
|
|
||||||
const firstCheck = false
|
|
||||||
const linesToCheck = viewportLinesToCheck(lineTracker, firstCheck, view)
|
|
||||||
expectLines(linesToCheck, [3, 7])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when some lines have changed outside viewport', function () {
|
|
||||||
let lineTracker: LineTracker
|
|
||||||
beforeEach(function () {
|
|
||||||
lineTracker = new LineTracker(view.state.doc)
|
|
||||||
lineTracker.clearAllLines()
|
|
||||||
lineTracker.markLineAsUpdated(300)
|
|
||||||
lineTracker.markLineAsUpdated(307)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not check lines', function () {
|
|
||||||
const firstCheck = false
|
|
||||||
const linesToCheck = viewportLinesToCheck(lineTracker, firstCheck, view)
|
|
||||||
expectLines(linesToCheck, [])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when some lines have changed inside and outside viewport', function () {
|
|
||||||
let lineTracker: LineTracker
|
|
||||||
beforeEach(function () {
|
|
||||||
lineTracker = new LineTracker(view.state.doc)
|
|
||||||
lineTracker.clearAllLines()
|
|
||||||
lineTracker.markLineAsUpdated(10)
|
|
||||||
lineTracker.markLineAsUpdated(12)
|
|
||||||
lineTracker.markLineAsUpdated(300)
|
|
||||||
lineTracker.markLineAsUpdated(307)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should check only lines in viewport', function () {
|
|
||||||
const firstCheck = false
|
|
||||||
const linesToCheck = viewportLinesToCheck(lineTracker, firstCheck, view)
|
|
||||||
expectLines(linesToCheck, [10, 12])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('buildSpellCheckResult', function () {
|
describe('buildSpellCheckResult', function () {
|
||||||
it('should build an empty result', function () {
|
it('should build an empty result', function () {
|
||||||
const knownMisspelledWords: Word[] = []
|
const knownMisspelledWords: Word[] = []
|
||||||
|
|
Loading…
Reference in a new issue