diff --git a/services/web/frontend/js/features/source-editor/extensions/spelling/line-tracker.ts b/services/web/frontend/js/features/source-editor/extensions/spelling/line-tracker.ts deleted file mode 100644 index 02711cf827..0000000000 --- a/services/web/frontend/js/features/source-editor/extensions/spelling/line-tracker.ts +++ /dev/null @@ -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, - }) - } - } - } -} diff --git a/services/web/frontend/js/features/source-editor/extensions/spelling/misspelled-words.ts b/services/web/frontend/js/features/source-editor/extensions/spelling/misspelled-words.ts index 91b732ceb9..ce052842c0 100644 --- a/services/web/frontend/js/features/source-editor/extensions/spelling/misspelled-words.ts +++ b/services/web/frontend/js/features/source-editor/extensions/spelling/misspelled-words.ts @@ -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 { updateAfterAddingIgnoredWord } from './ignored-words' -import _ from 'lodash' import { Word } from './spellchecker' export const addMisspelledWords = StateEffect.define() @@ -9,11 +8,10 @@ export const addMisspelledWords = StateEffect.define() export const resetMisspelledWords = StateEffect.define() const createMark = (word: Word) => { - const mark = Decoration.mark({ + return Decoration.mark({ class: 'ol-cm-spelling-error', word, - }) - return mark.range(word.from, word.to) + }).range(word.from, word.to) } /* @@ -26,13 +24,24 @@ export const misspelledWordsField = StateField.define({ return Decoration.none }, 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 = removeMarksUnderEdit(marks, transaction) + for (const effect of transaction.effects) { if (effect.is(addMisspelledWords)) { - // We're setting a new list of misspelled words - const misspelledWords = effect.value - marks = mergeMarks(marks, misspelledWords) + // Merge the new misspelled words into the existing set of marks + marks = marks.update({ + add: effect.value.map(word => createMark(word)), + sort: true, + }) } else if (effect.is(updateAfterAddingIgnoredWord)) { // Remove a misspelled word, all instances that match text const word = effect.value @@ -48,46 +57,6 @@ export const misspelledWordsField = StateField.define({ }, }) -/* - * 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 */ diff --git a/services/web/frontend/js/features/source-editor/extensions/spelling/spellchecker.ts b/services/web/frontend/js/features/source-editor/extensions/spelling/spellchecker.ts index 410edfae5c..9923d382a8 100644 --- a/services/web/frontend/js/features/source-editor/extensions/spelling/spellchecker.ts +++ b/services/web/frontend/js/features/source-editor/extensions/spelling/spellchecker.ts @@ -1,12 +1,11 @@ import { addMisspelledWords, misspelledWordsField } from './misspelled-words' import { ignoredWordsField, resetSpellChecker } from './ignored-words' -import { LineTracker } from './line-tracker' import { cacheField, addWordToCache, WordCacheValue } from './cache' import { WORD_REGEX } from './helpers' import OError from '@overleaf/o-error' import { spellCheckRequest } from './backend' 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 { getNormalTextSpansFromLine, @@ -15,10 +14,6 @@ import { import { waitForParser } from '../wait-for-parser' import { debugConsole } from '@/utils/debugging' -const _log = (...args: any) => { - debugConsole.debug('[SpellChecker]: ', ...args) -} - /* * Spellchecker, handles updates, schedules spelling checks */ @@ -26,43 +21,41 @@ export class SpellChecker { private abortController?: AbortController | null = null private timeout: number | null = null private firstCheck = true - private lineTracker: LineTracker | null = null private waitingForParser = false private firstCheckPending = false + private trackedChanges: ChangeSet // eslint-disable-next-line no-useless-constructor constructor(private readonly language: string) { this.language = language + this.trackedChanges = ChangeSet.empty(0) } destroy() { - _log('destroy') this._clearPendingSpellCheck() } _abortRequest() { if (this.abortController) { - _log('abort request') this.abortController.abort() this.abortController = null } } handleUpdate(update: ViewUpdate) { - if (!this.lineTracker) { - this.lineTracker = new LineTracker(update.state.doc) - } if (update.docChanged) { - this.lineTracker.applyUpdate(update) + this.trackedChanges = this.trackedChanges.compose(update.changes) this.scheduleSpellCheck(update.view) } else if (update.viewportChanged) { + this.trackedChanges = ChangeSet.empty(0) this.scheduleSpellCheck(update.view) } else if ( update.transactions.some(tr => { return tr.effects.some(effect => effect.is(resetSpellChecker)) }) ) { - this.lineTracker.resetAllLines() + // for tests + this.trackedChanges = ChangeSet.empty(0) this.spellCheckAsap(update.view) } // At the point that the spellchecker is initialized, the editor may not @@ -78,21 +71,15 @@ export class SpellChecker { update.state.facet(EditorView.editable) ) { this.firstCheckPending = true - _log('Scheduling initial spellcheck') this.spellCheckAsap(update.view) } } _performSpellCheck(view: EditorView) { - _log('Begin ---------------->') const wordsToCheck = this.getWordsToCheck(view) if (wordsToCheck.length === 0) { return } - _log( - '- words to check', - wordsToCheck.map(w => w.text) - ) const cache = view.state.field(cacheField) const { knownMisspelledWords, unknownWords } = cache.checkWords( this.language, @@ -101,7 +88,8 @@ export class SpellChecker { const processResult = ( misspellings: { index: number; suggestions: string[] }[] ) => { - this.lineTracker?.clearChangedLinesForWords(wordsToCheck) + this.trackedChanges = ChangeSet.empty(0) + if (this.firstCheck) { this.firstCheck = false this.firstCheckPending = false @@ -111,25 +99,13 @@ export class SpellChecker { unknownWords, misspellings ) - _log('- result', result) window.setTimeout(() => { view.dispatch({ effects: compileEffects(result), }) }, 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) { - _log('- skip request') processResult([]) } else { this._abortRequest() @@ -141,7 +117,7 @@ export class SpellChecker { }) .catch(error => { this.abortController = null - _log('>> error in spellcheck request', error) + debugConsole.error(error) }) } } @@ -152,10 +128,7 @@ export class SpellChecker { } this.waitingForParser = true - waitForParser( - view, - view => viewportRangeToCheck(this.firstCheck, view).to - ).then(() => { + waitForParser(view, view => view.viewport.to).then(() => { this.waitingForParser = false this._performSpellCheck(view) }) @@ -187,22 +160,36 @@ export class SpellChecker { } getWordsToCheck(view: EditorView) { - const lang = this.language - const ignoredWords = view.state.field(ignoredWordsField) - _log('- ignored words', ignoredWords) - if (!this.lineTracker) { - this.lineTracker = new LineTracker(view.state.doc) + const wordsToCheck: Word[] = [] + + const { from, to } = view.viewport + const changedLineNumbers = new Set() + 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( - this.lineTracker, - this.firstCheck, - view - )) { - wordsToCheck = wordsToCheck.concat( - getWordsFromLine(view, line, ignoredWords, lang) + + const ignoredWords = view.state.field(ignoredWordsField) + for (const i of changedLineNumbers) { + const line = view.state.doc.line(i) + wordsToCheck.push( + ...getWordsFromLine(view, line, ignoredWords, this.language) ) } + return wordsToCheck } } @@ -246,34 +233,29 @@ export const buildSpellCheckResult = ( misspellings: { index: number; suggestions: string[] }[] ) => { const cacheAdditions: [Word, string[] | boolean][] = [] + // Put known misspellings into cache const misspelledWords = misspellings.map(item => { - const word = { ...unknownWords[item.index] } + const word = { + ...unknownWords[item.index], + } word.suggestions = item.suggestions if (word.suggestions) { cacheAdditions.push([word, word.suggestions]) } return word }) + // if word was not misspelled, put it in the cache for (const word of unknownWords) { if (!misspelledWords.find(mw => mw.text === word.text)) { 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 { 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 = ( view: EditorView, line: Line, ignoredWords: IgnoredWords, lang: string ): Word[] => { - const lineNumber = line.number const normalTextSpans: Array = getNormalTextSpansFromLine( view, line ) const words: Word[] = [] - let regexResult - normalTextSpans.forEach(span => { - WORD_REGEX.lastIndex = 0 // reset global stateful regexp for this usage - while ((regexResult = WORD_REGEX.exec(span.text))) { - let word = regexResult[0] + for (const span of normalTextSpans) { + for (const match of span.text.matchAll(WORD_REGEX)) { + let word = match[0] if (word.startsWith("'")) { word = word.slice(1) } @@ -368,18 +297,19 @@ export const getWordsFromLine = ( word = word.slice(0, -1) } if (!ignoredWords.has(word)) { + const from = span.from + match.index words.push( new Word({ text: word, - from: span.from + regexResult.index, - to: span.from + regexResult.index + word.length, - lineNumber, + from, + to: from + word.length, + lineNumber: line.number, lang, }) ) } } - }) + } return words } diff --git a/services/web/test/frontend/features/source-editor/extensions/spelling/line-tracker.test.ts b/services/web/test/frontend/features/source-editor/extensions/spelling/line-tracker.test.ts deleted file mode 100644 index 0074b83424..0000000000 --- a/services/web/test/frontend/features/source-editor/extensions/spelling/line-tracker.test.ts +++ /dev/null @@ -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) - }) - }) -}) diff --git a/services/web/test/frontend/features/source-editor/extensions/spelling/spellchecker.test.ts b/services/web/test/frontend/features/source-editor/extensions/spelling/spellchecker.test.ts index d143baf1d1..eff1d98d55 100644 --- a/services/web/test/frontend/features/source-editor/extensions/spelling/spellchecker.test.ts +++ b/services/web/test/frontend/features/source-editor/extensions/spelling/spellchecker.test.ts @@ -1,29 +1,15 @@ import { getWordsFromLine, - viewportLinesToCheck, buildSpellCheckResult, Word, -} from '../../../../../../frontend/js/features/source-editor/extensions/spelling/spellchecker' -import { LineTracker } from '../../../../../../frontend/js/features/source-editor/extensions/spelling/line-tracker' +} from '@/features/source-editor/extensions/spelling/spellchecker' import { expect } from 'chai' import { EditorView } from '@codemirror/view' -import { EditorState, Line } from '@codemirror/state' -import _ from 'lodash' -import { IgnoredWords } from '../../../../../../frontend/js/features/dictionary/ignored-words' -import { LaTeXLanguage } from '../../../../../../frontend/js/features/source-editor/languages/latex/latex-language' +import { IgnoredWords } from '@/features/dictionary/ignored-words' +import { LaTeXLanguage } from '@/features/source-editor/languages/latex/latex-language' import { LanguageSupport } from '@codemirror/language' -const latex = new LanguageSupport(LaTeXLanguage) - -const makeView = (text: string) => { - const view = new EditorView({ - state: EditorState.create({ - doc: text, - extensions: [latex], - }), - }) - return view -} +const extensions = [new LanguageSupport(LaTeXLanguage)] describe('SpellChecker', function () { describe('getWordsFromLine', function () { @@ -35,7 +21,10 @@ describe('SpellChecker', 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 words = getWordsFromLine(view, line, ignoredWords, lang) expect(words).to.deep.equal([ @@ -48,7 +37,10 @@ describe('SpellChecker', function () { it('should ignore words in ignoredWords', function () { 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 words = getWordsFromLine(view, line, ignoredWords, lang) expect(words).to.deep.equal([ @@ -59,14 +51,20 @@ describe('SpellChecker', 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 words = getWordsFromLine(view, line, ignoredWords, lang) expect(words).to.deep.equal([]) }) 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 words = getWordsFromLine(view, line, ignoredWords, lang) expect(words).to.deep.equal([ @@ -76,7 +74,10 @@ describe('SpellChecker', 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 words = getWordsFromLine(view, line, ignoredWords, lang) 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 () { it('should build an empty result', function () { const knownMisspelledWords: Word[] = []