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
This commit is contained in:
Tim Down
2023-04-13 08:40:56 +00:00
committed by Copybot
parent 3f992f9453
commit 7f37ba737c
296 changed files with 33268 additions and 54 deletions
@@ -0,0 +1,113 @@
import sinon from 'sinon'
import { waitFor } from '@testing-library/react'
import { expect } from 'chai'
import { EditorView } from '@codemirror/view'
import { EditorSelection, EditorState } from '@codemirror/state'
import {
cursorPosition,
restoreCursorPosition,
} from '../../../../../frontend/js/features/source-editor/extensions/cursor-position'
const doc = `
\\documentclass{article}
\\title{Your Paper}
\\author{You}
\\begin{document}
\\maketitle
\\begin{abstract}
Your abstract.
\\end{abstract}
\\section{Introduction}
Your introduction goes here!
\\end{document}`
const mockDoc = () => {
return {
doc_id: 'test-doc',
}
}
describe('CodeMirror cursor position extension', function () {
afterEach(function () {
sinon.restore()
})
it('stores cursor position when the view is destroyed', async function () {
const currentDoc = mockDoc()
sinon.stub(window.Storage.prototype, 'getItem').callsFake(key => {
switch (key) {
case 'doc.position.test-doc':
return JSON.stringify({
cursorPosition: { row: 1, column: 1 },
firstVisibleLine: 5,
})
default:
return null
}
})
const setItem = sinon.spy(window.Storage.prototype, 'setItem')
const view = new EditorView({
state: EditorState.create({
doc,
extensions: [cursorPosition({ currentDoc })],
}),
})
view.dispatch({
selection: EditorSelection.cursor(50),
})
view.destroy()
await waitFor(() => {
expect(setItem).to.have.been.calledWith(
'doc.position.test-doc',
JSON.stringify({
cursorPosition: {
row: 3,
column: 6,
},
firstVisibleLine: 5,
})
)
})
})
it('restores cursor position', async function () {
const currentDoc = mockDoc()
const getItem = sinon
.stub(window.Storage.prototype, 'getItem')
.callsFake(key => {
switch (key) {
case 'doc.position.test-doc':
return JSON.stringify({
cursorPosition: { row: 3, column: 5 },
firstVisibleLine: 0,
})
default:
return null
}
})
const view = new EditorView({
state: EditorState.create({
doc,
extensions: [cursorPosition({ currentDoc })],
}),
})
view.dispatch(restoreCursorPosition(view.state.doc, 'test-doc'))
expect(getItem).to.have.been.calledWith('doc.position.test-doc')
await waitFor(() => {
const [range] = view.state.selection.ranges
expect(range.head).to.eq(49)
expect(range.anchor).to.eq(49)
expect(range.empty).to.eq(true)
})
})
})
@@ -0,0 +1,169 @@
import { foldEffect, foldState } from '@codemirror/language'
import { EditorSelection, EditorState } from '@codemirror/state'
import { DecorationSet, EditorView } from '@codemirror/view'
import { expect } from 'chai'
import { duplicateSelection } from '../../../../../frontend/js/features/source-editor/commands/ranges'
type Position = {
from: number
to: number
}
function folds(foldRanges: DecorationSet) {
const ranges: Position[] = []
foldRanges.between(Number.MIN_VALUE, Number.MAX_VALUE, (from, to) => {
ranges.push({ from, to })
})
return ranges
}
describe('Line duplication command', function () {
describe('For single selections', function () {
describe('For cursor selection', function () {
it('Cursor selection duplicates line downwards', function () {
const view = new EditorView({
doc: 'line1\nline2',
selection: EditorSelection.cursor(0),
})
duplicateSelection(view)
expect(view.state.doc.toString()).to.equal('line1\nline1\nline2')
expect(view.state.selection.ranges.length).to.equal(1)
expect(view.state.selection.ranges[0].eq(EditorSelection.cursor(0))).to
.be.true
})
it('Preserves folded ranges', function () {
const view = new EditorView({
doc: '\\begin{itemize}\n\t\\item test\n\\end{itemize}',
extensions: foldState,
})
view.dispatch(
view.state.update({
selection: EditorSelection.cursor(0),
// Fold to \begin{itemize}...\end{itemize}
effects: [foldEffect.of({ from: 15, to: 28 })],
})
)
duplicateSelection(view)
expect(folds(view.state.field(foldState))).to.deep.equal([
{ from: 15, to: 28 },
{ from: 57, to: 70 },
])
expect(view.state.selection.ranges.length).to.equal(1)
expect(view.state.selection.ranges[0].eq(EditorSelection.cursor(0))).to
.be.true
})
})
describe('For range selections', function () {
it('Duplicates line with a cursor downwards', function () {
const view = new EditorView({
doc: 'line1\nline2',
selection: EditorSelection.cursor(0),
})
duplicateSelection(view)
expect(view.state.doc.toString()).to.equal('line1\nline1\nline2')
})
it('Duplicates range forwards', function () {
const view = new EditorView({
doc: 'line1\nline2',
selection: EditorSelection.range(0, 5),
})
duplicateSelection(view)
expect(view.state.doc.toString()).to.equal('line1line1\nline2')
expect(view.state.selection.ranges.length).to.equal(1)
expect(view.state.selection.ranges[0].eq(EditorSelection.range(5, 10)))
.to.be.true
})
it('Duplicates range backwards', function () {
const view = new EditorView({
doc: 'line1\nline2',
selection: EditorSelection.range(5, 0),
})
duplicateSelection(view)
expect(view.state.doc.toString()).to.equal('line1line1\nline2')
expect(view.state.selection.ranges.length).to.equal(1)
expect(view.state.selection.ranges[0].eq(EditorSelection.range(5, 0)))
.to.be.true
})
})
})
describe('For multiple selections', function () {
it('Preserves folded ranges', function () {
const doc =
'\\begin{itemize}\n\t\\item line1\n\\end{itemize}\n\\begin{itemize}\n\t\\item line2\n\\end{itemize}'
const view = new EditorView({
doc,
extensions: [foldState, EditorState.allowMultipleSelections.of(true)],
})
view.dispatch(
view.state.update({
selection: EditorSelection.create([
EditorSelection.cursor(0),
EditorSelection.cursor(43),
]),
effects: [
foldEffect.of({ from: 15, to: 29 }),
foldEffect.of({ from: 58, to: 72 }),
],
})
)
duplicateSelection(view)
expect(view.state.doc.toString()).to.equal(
'\\begin{itemize}\n\t\\item line1\n\\end{itemize}\n\\begin{itemize}\n\t\\item line1\n\\end{itemize}\n\\begin{itemize}\n\t\\item line2\n\\end{itemize}\n\\begin{itemize}\n\t\\item line2\n\\end{itemize}'
)
expect(folds(view.state.field(foldState))).to.deep.equal([
{ from: 15, to: 29 },
{ from: 58, to: 72 },
{ from: 101, to: 115 },
{ from: 144, to: 158 },
])
expect(view.state.selection.ranges.length).to.equal(2)
expect(view.state.selection.ranges[0].eq(EditorSelection.cursor(0))).to.be
.true
expect(view.state.selection.ranges[1].eq(EditorSelection.cursor(86))).to
.be.true
})
it('Duplicates all selections', function () {
const view = new EditorView({
doc: 'line1\nline2',
extensions: [EditorState.allowMultipleSelections.of(true)],
selection: EditorSelection.create([
EditorSelection.cursor(1),
EditorSelection.range(7, 9),
]),
})
duplicateSelection(view)
expect(view.state.doc.toString()).to.equal('line1\nline1\nlinine2')
expect(view.state.selection.ranges.length).to.equal(2)
expect(view.state.selection.ranges[0].eq(EditorSelection.cursor(1))).to.be
.true
expect(view.state.selection.ranges[1].eq(EditorSelection.range(15, 17)))
.to.be.true
})
})
})
@@ -0,0 +1,115 @@
import { expect } from 'chai'
import { EditorView, DecorationSet } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import { buildDecorations } from '../../../../../frontend/js/features/source-editor/extensions/line-wrapping-indentation'
const basicDoc = `
\\begin{document}
Test
\\end{document}
`
const docLongLineNoIndentation = `
\\begin{document}
Test
Hello one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four
\\end{document}
`
const docLongLineWithIndentation = `
\\begin{document}
Test
Hello one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four
\\end{document}
`
const docLongLineWithLotsOfIndentation = `
\\begin{document}
Test
Hello one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four
Hello one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four
Hello one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four
Hello
Hello one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four one two three four
Hello
\\end{document}
`
describe('line-wrapping-indentation', function () {
describe('buildDecorations', function () {
const _buildView = (doc: string) => {
return new EditorView({
state: EditorState.create({
doc,
}),
})
}
const _toArray = (decorations: DecorationSet) => {
const result = []
const cursor = decorations.iter()
while (cursor.value) {
result.push({ from: cursor.from, to: cursor.to, value: cursor.value })
cursor.next()
}
return result
}
describe('basic document', function () {
it('should have no decorations', function () {
const view = _buildView(basicDoc)
const decorations = buildDecorations(view, 24)
expect(decorations).to.exist
expect(decorations.size).to.equal(0)
})
})
describe('document with long lines, no indentation', function () {
it('should have no decorations', function () {
const view = _buildView(docLongLineNoIndentation)
const decorations = buildDecorations(view, 24)
expect(decorations).to.exist
expect(decorations.size).to.equal(0)
})
})
describe('document with long lines, with indentation', function () {
it('should have a decoration', function () {
const view = _buildView(docLongLineWithIndentation)
const decorations = buildDecorations(view, 24)
expect(decorations).to.exist
expect(decorations.size).to.equal(1)
const decorationItem = _toArray(decorations)[0]
expect(decorationItem.from).to.equal(23)
expect(decorationItem.to).to.equal(23)
})
})
describe('document with long lines, with lots of indentation', function () {
it('should have a decoration', function () {
const view = _buildView(docLongLineWithLotsOfIndentation)
const decorations = buildDecorations(view, 24)
expect(decorations).to.exist
expect(decorations.size).to.equal(4)
const decorationsArray = _toArray(decorations)
const expectedPositions = [23, 265, 507, 758]
decorationsArray.forEach((item, index) => {
expect(item.from).to.equal(expectedPositions[index])
expect(item.to).to.equal(expectedPositions[index])
})
})
})
})
})
@@ -0,0 +1,66 @@
import sinon from 'sinon'
import { expect } from 'chai'
import { EditorFacade } from '../../../../../frontend/js/features/source-editor/extensions/realtime'
import { EditorView } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
describe('CodeMirror EditorFacade', function () {
let state: EditorState, view: EditorView
beforeEach(function () {
state = EditorState.create()
view = new EditorView({ state })
})
it('should allow us to manipulate the CodeMirror document', function () {
const editor = new EditorFacade(view)
const text = 'basic test, nothing more'
editor.cmInsert(0, text)
expect(editor.getValue()).to.equal(text)
editor.cmDelete(0, 'b')
expect(editor.getValue()).to.equal(text.slice(1))
})
it('should allow us to attach change listeners', function () {
const editor = new EditorFacade(view)
const listenerA = sinon.stub()
const listenerB = sinon.stub()
editor.on('change', listenerA)
editor.on('change', listenerB)
expect(listenerA).to.not.have.been.called
expect(listenerB).to.not.have.been.called
const magicNumber = Math.random()
editor.emit('change', magicNumber)
expect(listenerA).to.have.been.calledWith(magicNumber)
expect(listenerB).to.have.been.calledWith(magicNumber)
})
it('should attach to ShareJs document', function () {
const editor = new EditorFacade(view)
const text = 'something nice'
const shareDoc = {
on: sinon.stub(),
getText: sinon.stub().returns(text),
removeListener: sinon.stub(),
detach_cm6: undefined,
}
editor.cmInsert(0, text)
// @ts-ignore
editor.attachShareJs(shareDoc)
expect(shareDoc.on.callCount).to.equal(2)
expect(shareDoc.on).to.have.been.calledWith('insert')
expect(shareDoc.on).to.have.been.calledWith('delete')
expect(shareDoc.detach_cm6).to.be.a('function')
})
})
@@ -0,0 +1,119 @@
import sinon from 'sinon'
import { fireEvent, waitFor } from '@testing-library/react'
import { expect } from 'chai'
import { EditorView } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import {
restoreScrollPosition,
scrollPosition,
} from '../../../../../frontend/js/features/source-editor/extensions/scroll-position'
const doc = `
\\documentclass{article}
\\title{Your Paper}
\\author{You}
\\begin{document}
\\maketitle
\\begin{abstract}
Your abstract.
\\end{abstract}
\\section{Introduction}
Your introduction goes here!
\\end{document}`
const mockDoc = () => {
return {
doc_id: 'test-doc',
}
}
describe('CodeMirror scroll position extension', function () {
beforeEach(function () {
sinon.stub(HTMLElement.prototype, 'scrollHeight').returns(800)
sinon.stub(HTMLElement.prototype, 'scrollWidth').returns(500)
sinon.stub(HTMLElement.prototype, 'clientHeight').returns(200)
sinon.stub(HTMLElement.prototype, 'clientWidth').returns(500)
sinon
.stub(HTMLElement.prototype, 'getBoundingClientRect')
.returns({ top: 100, left: 0, right: 500, bottom: 200 } as DOMRect)
// Range.getClientRects doesn't exist yet in jsdom
window.Range.prototype.getClientRects = sinon.stub().returns([])
})
afterEach(function () {
sinon.restore()
// @ts-ignore
delete window.Range.prototype.getClientRects
})
it('stores scroll position when the view is destroyed', async function () {
const currentDoc = mockDoc()
sinon.stub(window.Storage.prototype, 'getItem').callsFake(key => {
switch (key) {
case 'doc.position.test-doc':
return JSON.stringify({
cursorPosition: { row: 2, column: 2 },
firstVisibleLine: 5,
})
default:
return null
}
})
const view = new EditorView({
state: EditorState.create({
doc,
extensions: [scrollPosition({ currentDoc })],
}),
})
const setItem = sinon.spy(window.Storage.prototype, 'setItem')
fireEvent.scroll(view.scrollDOM, { target: { scrollTop: 10 } })
view.destroy()
const expected = JSON.stringify({
cursorPosition: { row: 2, column: 2 },
firstVisibleLine: 12,
})
await waitFor(() => {
expect(setItem).to.have.been.calledWith('doc.position.test-doc', expected)
})
})
it('restores scroll position', async function () {
const currentDoc = mockDoc()
const getItem = sinon
.stub(window.Storage.prototype, 'getItem')
.callsFake(key => {
switch (key) {
case 'editor.position.test-doc':
return JSON.stringify({ firstVisibleLine: 12 })
default:
return null
}
})
const view = new EditorView({
state: EditorState.create({
doc,
extensions: [scrollPosition({ currentDoc })],
}),
})
view.dispatch(restoreScrollPosition())
await waitFor(() => {
expect(getItem).to.have.been.calledWith('doc.position.test-doc')
})
// TODO: scrollTop should be a higher value but requires more mocking
// await waitFor(() => {
// expect(view.scrollDOM.scrollTop).to.eq(0)
// })
})
})
@@ -0,0 +1,61 @@
import { WordCache } from '../../../../../../frontend/js/features/source-editor/extensions/spelling/cache'
import { expect } from 'chai'
import { Word } from '../../../../../../frontend/js/features/source-editor/extensions/spelling/spellchecker'
describe('WordCache', function () {
describe('basic operations', function () {
let cache: WordCache, lang: string
beforeEach(function () {
cache = new WordCache()
lang = 'xx'
})
it('should store values in cache', function () {
let word = 'foo'
expect(cache.get(lang, word)).to.not.exist
cache.set(lang, word, true)
expect(cache.get(lang, word)).to.equal(true)
word = 'bar'
expect(cache.get(lang, word)).to.not.exist
cache.set(lang, word, ['a', 'b'])
expect(cache.get(lang, word)).to.deep.equal(['a', 'b'])
})
it('should store words in separate languages', function () {
const word = 'foo'
const otherLang = 'zz'
cache.set(lang, word, 101)
expect(cache.get(lang, word)).to.equal(101)
expect(cache.get(otherLang, word)).to.not.exist
cache.set(otherLang, word, 202)
expect(cache.get(lang, word)).to.equal(101)
expect(cache.get(otherLang, word)).to.equal(202)
})
it('should check words against cache', function () {
cache.set(lang, 'foo', ['a', 'b'])
cache.set(lang, 'bar', true)
cache.set(lang, 'baz', true)
const wordsToCheck = [
{ text: 'foo', from: 0 },
{ text: 'baz', from: 1 },
{ text: 'quux', from: 2 },
{ text: 'foo', from: 3 },
{ text: 'zaz', from: 4 },
] as Word[]
const result = cache.checkWords(lang, wordsToCheck)
expect(result).to.have.keys('knownMisspelledWords', 'unknownWords')
expect(result.knownMisspelledWords).to.deep.equal([
{ text: 'foo', suggestions: ['a', 'b'], from: 0 },
{ text: 'foo', suggestions: ['a', 'b'], from: 3 },
])
expect(result.unknownWords).to.deep.equal([
{ text: 'quux', from: 2 },
{ text: 'zaz', from: 4 },
])
})
})
})
@@ -0,0 +1,182 @@
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],
])
})
})
})
@@ -0,0 +1,271 @@
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'
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 { 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
}
describe('SpellChecker', function () {
describe('getWordsFromLine', function () {
let lang: string, ignoredWords: IgnoredWords
beforeEach(function () {
/* Note: ignore the word 'test' */
lang = 'en'
ignoredWords = new Set([]) as unknown as IgnoredWords
})
it('should get words from a line', function () {
const view = makeView('Hello test one two')
const line = view.state.doc.line(1)
const words = getWordsFromLine(view, line, ignoredWords, lang)
expect(words).to.deep.equal([
{ text: 'Hello', from: 0, to: 5, lineNumber: 1, lang: 'en' },
{ text: 'test', from: 6, to: 10, lineNumber: 1, lang: 'en' },
{ text: 'one', from: 11, to: 14, lineNumber: 1, lang: 'en' },
{ text: 'two', from: 15, to: 18, lineNumber: 1, lang: 'en' },
])
})
it('should ignore words in ignoredWords', function () {
ignoredWords = new Set(['test']) as unknown as IgnoredWords
const view = makeView('Hello test one two')
const line = view.state.doc.line(1)
const words = getWordsFromLine(view, line, ignoredWords, lang)
expect(words).to.deep.equal([
{ text: 'Hello', from: 0, to: 5, lineNumber: 1, lang: 'en' },
{ text: 'one', from: 11, to: 14, lineNumber: 1, lang: 'en' },
{ text: 'two', from: 15, to: 18, lineNumber: 1, lang: 'en' },
])
})
it('should get no words from an empty line', function () {
const view = makeView(' ')
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 line = view.state.doc.line(1)
const words = getWordsFromLine(view, line, ignoredWords, lang)
expect(words).to.deep.equal([
{ text: 'seven', from: 24, to: 29, lineNumber: 1, lang: 'en' },
{ text: 'eight', from: 30, to: 35, lineNumber: 1, lang: 'en' },
])
})
it('should ignore command names in the text', function () {
const view = makeView('\\foo nine \\bar ten \\baz[]{}')
const line = view.state.doc.line(1)
const words = getWordsFromLine(view, line, ignoredWords, lang)
expect(words).to.deep.equal([
{ text: 'nine', from: 5, to: 9, lineNumber: 1, lang: 'en' },
{ text: 'ten', from: 15, to: 18, lineNumber: 1, lang: 'en' },
])
})
})
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[] = []
const unknownWords: Word[] = []
const misspellings: { index: number; suggestions: string[] }[] = []
const result = buildSpellCheckResult(
knownMisspelledWords,
unknownWords,
misspellings
)
expect(result).to.deep.equal({
cacheAdditions: [],
misspelledWords: [],
})
})
it('should build a realistic result', function () {
const _makeWord = (text: string, suggestions?: string[]) => {
const word = new Word({
text,
from: 0,
to: 0,
lineNumber: 0,
lang: 'xx',
})
if (suggestions != null) {
word.suggestions = suggestions
}
return word
}
// We know this word is misspelled
const knownMisspelledWords = [_makeWord('fff', ['food', 'fleece'])]
// These words we didn't know
const unknownWords = [
_makeWord('aaa'),
_makeWord('bbb'),
_makeWord('ccc'),
_makeWord('ddd'),
]
// These are the suggestions we got back from the backend
const misspellings = [
{ index: 1, suggestions: ['box', 'bass'] },
{ index: 3, suggestions: ['docs', 'dance'] },
]
// Build the result structure
const result = buildSpellCheckResult(
knownMisspelledWords,
unknownWords,
misspellings
)
expect(result).to.have.keys('cacheAdditions', 'misspelledWords')
// Check cache additions
expect(result.cacheAdditions.map(([k, v]) => [k.text, v])).to.deep.equal([
// Put these in cache as known misspellings
['bbb', ['box', 'bass']],
['ddd', ['docs', 'dance']],
// Put these in cache as known-correct
['aaa', true],
['ccc', true],
])
// Check misspellings
expect(
result.misspelledWords.map(w => [w.text, w.suggestions])
).to.deep.equal([
// Words in the payload that we now know were misspelled
['bbb', ['box', 'bass']],
['ddd', ['docs', 'dance']],
// Word we already knew was misspelled, preserved here
['fff', ['food', 'fleece']],
])
})
})
})