Ensure that the correct lines are spell-checked (#19967)

GitOrigin-RevId: f63601288821d37bddcddc776270263e291d3985
This commit is contained in:
Alf Eaton
2024-09-04 08:04:53 +00:00
committed by Copybot
parent 607b3e3494
commit abfa29e629
5 changed files with 95 additions and 654 deletions
@@ -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 {
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[] = []