spellchecker.test.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import {
  2. getWordsFromLine,
  3. viewportLinesToCheck,
  4. buildSpellCheckResult,
  5. Word,
  6. } from '../../../../../../frontend/js/features/source-editor/extensions/spelling/spellchecker'
  7. import { LineTracker } from '../../../../../../frontend/js/features/source-editor/extensions/spelling/line-tracker'
  8. import { expect } from 'chai'
  9. import { EditorView } from '@codemirror/view'
  10. import { EditorState, Line } from '@codemirror/state'
  11. import _ from 'lodash'
  12. import { IgnoredWords } from '../../../../../../frontend/js/features/dictionary/ignored-words'
  13. import { LaTeXLanguage } from '../../../../../../frontend/js/features/source-editor/languages/latex/latex-language'
  14. import { LanguageSupport } from '@codemirror/language'
  15. const latex = new LanguageSupport(LaTeXLanguage)
  16. const makeView = (text: string) => {
  17. const view = new EditorView({
  18. state: EditorState.create({
  19. doc: text,
  20. extensions: [latex],
  21. }),
  22. })
  23. return view
  24. }
  25. describe('SpellChecker', function () {
  26. describe('getWordsFromLine', function () {
  27. let lang: string, ignoredWords: IgnoredWords
  28. beforeEach(function () {
  29. /* Note: ignore the word 'test' */
  30. lang = 'en'
  31. ignoredWords = new Set([]) as unknown as IgnoredWords
  32. })
  33. it('should get words from a line', function () {
  34. const view = makeView('Hello test one two')
  35. const line = view.state.doc.line(1)
  36. const words = getWordsFromLine(view, line, ignoredWords, lang)
  37. expect(words).to.deep.equal([
  38. { text: 'Hello', from: 0, to: 5, lineNumber: 1, lang: 'en' },
  39. { text: 'test', from: 6, to: 10, lineNumber: 1, lang: 'en' },
  40. { text: 'one', from: 11, to: 14, lineNumber: 1, lang: 'en' },
  41. { text: 'two', from: 15, to: 18, lineNumber: 1, lang: 'en' },
  42. ])
  43. })
  44. it('should ignore words in ignoredWords', function () {
  45. ignoredWords = new Set(['test']) as unknown as IgnoredWords
  46. const view = makeView('Hello test one two')
  47. const line = view.state.doc.line(1)
  48. const words = getWordsFromLine(view, line, ignoredWords, lang)
  49. expect(words).to.deep.equal([
  50. { text: 'Hello', from: 0, to: 5, lineNumber: 1, lang: 'en' },
  51. { text: 'one', from: 11, to: 14, lineNumber: 1, lang: 'en' },
  52. { text: 'two', from: 15, to: 18, lineNumber: 1, lang: 'en' },
  53. ])
  54. })
  55. it('should get no words from an empty line', function () {
  56. const view = makeView(' ')
  57. const line = view.state.doc.line(1)
  58. const words = getWordsFromLine(view, line, ignoredWords, lang)
  59. expect(words).to.deep.equal([])
  60. })
  61. it('should ignore content of some commands in the text', function () {
  62. const view = makeView('\\usepackage[foo]{ bar } seven eight')
  63. const line = view.state.doc.line(1)
  64. const words = getWordsFromLine(view, line, ignoredWords, lang)
  65. expect(words).to.deep.equal([
  66. { text: 'seven', from: 24, to: 29, lineNumber: 1, lang: 'en' },
  67. { text: 'eight', from: 30, to: 35, lineNumber: 1, lang: 'en' },
  68. ])
  69. })
  70. it('should ignore command names in the text', function () {
  71. const view = makeView('\\foo nine \\bar ten \\baz[]{}')
  72. const line = view.state.doc.line(1)
  73. const words = getWordsFromLine(view, line, ignoredWords, lang)
  74. expect(words).to.deep.equal([
  75. { text: 'nine', from: 5, to: 9, lineNumber: 1, lang: 'en' },
  76. { text: 'ten', from: 15, to: 18, lineNumber: 1, lang: 'en' },
  77. ])
  78. })
  79. })
  80. describe('viewportLinesToCheck', function () {
  81. const expectLines = (lines: Line[], expectations: any) => {
  82. expect(lines.map(l => l.number)).to.deep.equal(expectations)
  83. }
  84. const expectLineRange = (lines: Line[], from: number, to: number) => {
  85. expect(lines.map(l => l.number)).to.deep.equal(_.range(from, to + 1))
  86. }
  87. let view: EditorView
  88. beforeEach(function () {
  89. view = makeView(new Array(1000).fill('aa bb cc dd').join('\n'))
  90. // Test preconditions on these structures
  91. const viewport = view.viewport
  92. expect(view.state.doc.lines).to.equal(1000)
  93. const firstVisibleLine = view.state.doc.lineAt(viewport.from).number
  94. const lastVisibleLine = view.state.doc.lineAt(viewport.to).number
  95. expect(firstVisibleLine).to.equal(1)
  96. expect(lastVisibleLine).to.equal(36)
  97. })
  98. describe('when all lines are changed', function () {
  99. let lineTracker: LineTracker
  100. beforeEach(function () {
  101. lineTracker = new LineTracker(view.state.doc)
  102. })
  103. it('should check all lines in the viewport when not first check', function () {
  104. const firstCheck = false
  105. const linesToCheck = viewportLinesToCheck(lineTracker, firstCheck, view)
  106. expectLineRange(linesToCheck, 1, 36)
  107. })
  108. it('should check more lines than the viewport on first check', function () {
  109. const firstCheck = true
  110. const lineTracker = new LineTracker(view.state.doc)
  111. const linesToCheck = viewportLinesToCheck(lineTracker, firstCheck, view)
  112. expectLineRange(linesToCheck, 1, 108)
  113. })
  114. })
  115. describe('when no lines have changed', function () {
  116. let lineTracker: LineTracker
  117. beforeEach(function () {
  118. lineTracker = new LineTracker(view.state.doc)
  119. lineTracker.clearAllLines()
  120. })
  121. it('on first check, should not check any lines', function () {
  122. const firstCheck = true
  123. const linesToCheck = viewportLinesToCheck(lineTracker, firstCheck, view)
  124. expect(linesToCheck).to.deep.equal([])
  125. })
  126. it('on not first check, should not check any lines', function () {
  127. const firstCheck = false
  128. const linesToCheck = viewportLinesToCheck(lineTracker, firstCheck, view)
  129. expectLines(linesToCheck, [])
  130. })
  131. })
  132. describe('when some lines have changed in viewport', function () {
  133. let lineTracker: LineTracker
  134. beforeEach(function () {
  135. lineTracker = new LineTracker(view.state.doc)
  136. lineTracker.clearAllLines()
  137. lineTracker.markLineAsUpdated(3)
  138. lineTracker.markLineAsUpdated(7)
  139. })
  140. it('should check correct lines', function () {
  141. const firstCheck = false
  142. const linesToCheck = viewportLinesToCheck(lineTracker, firstCheck, view)
  143. expectLines(linesToCheck, [3, 7])
  144. })
  145. })
  146. describe('when some lines have changed outside viewport', function () {
  147. let lineTracker: LineTracker
  148. beforeEach(function () {
  149. lineTracker = new LineTracker(view.state.doc)
  150. lineTracker.clearAllLines()
  151. lineTracker.markLineAsUpdated(300)
  152. lineTracker.markLineAsUpdated(307)
  153. })
  154. it('should not check lines', function () {
  155. const firstCheck = false
  156. const linesToCheck = viewportLinesToCheck(lineTracker, firstCheck, view)
  157. expectLines(linesToCheck, [])
  158. })
  159. })
  160. describe('when some lines have changed inside and outside viewport', function () {
  161. let lineTracker: LineTracker
  162. beforeEach(function () {
  163. lineTracker = new LineTracker(view.state.doc)
  164. lineTracker.clearAllLines()
  165. lineTracker.markLineAsUpdated(10)
  166. lineTracker.markLineAsUpdated(12)
  167. lineTracker.markLineAsUpdated(300)
  168. lineTracker.markLineAsUpdated(307)
  169. })
  170. it('should check only lines in viewport', function () {
  171. const firstCheck = false
  172. const linesToCheck = viewportLinesToCheck(lineTracker, firstCheck, view)
  173. expectLines(linesToCheck, [10, 12])
  174. })
  175. })
  176. })
  177. describe('buildSpellCheckResult', function () {
  178. it('should build an empty result', function () {
  179. const knownMisspelledWords: Word[] = []
  180. const unknownWords: Word[] = []
  181. const misspellings: { index: number; suggestions: string[] }[] = []
  182. const result = buildSpellCheckResult(
  183. knownMisspelledWords,
  184. unknownWords,
  185. misspellings
  186. )
  187. expect(result).to.deep.equal({
  188. cacheAdditions: [],
  189. misspelledWords: [],
  190. })
  191. })
  192. it('should build a realistic result', function () {
  193. const _makeWord = (text: string, suggestions?: string[]) => {
  194. const word = new Word({
  195. text,
  196. from: 0,
  197. to: 0,
  198. lineNumber: 0,
  199. lang: 'xx',
  200. })
  201. if (suggestions != null) {
  202. word.suggestions = suggestions
  203. }
  204. return word
  205. }
  206. // We know this word is misspelled
  207. const knownMisspelledWords = [_makeWord('fff', ['food', 'fleece'])]
  208. // These words we didn't know
  209. const unknownWords = [
  210. _makeWord('aaa'),
  211. _makeWord('bbb'),
  212. _makeWord('ccc'),
  213. _makeWord('ddd'),
  214. ]
  215. // These are the suggestions we got back from the backend
  216. const misspellings = [
  217. { index: 1, suggestions: ['box', 'bass'] },
  218. { index: 3, suggestions: ['docs', 'dance'] },
  219. ]
  220. // Build the result structure
  221. const result = buildSpellCheckResult(
  222. knownMisspelledWords,
  223. unknownWords,
  224. misspellings
  225. )
  226. expect(result).to.have.keys('cacheAdditions', 'misspelledWords')
  227. // Check cache additions
  228. expect(result.cacheAdditions.map(([k, v]) => [k.text, v])).to.deep.equal([
  229. // Put these in cache as known misspellings
  230. ['bbb', ['box', 'bass']],
  231. ['ddd', ['docs', 'dance']],
  232. // Put these in cache as known-correct
  233. ['aaa', true],
  234. ['ccc', true],
  235. ])
  236. // Check misspellings
  237. expect(
  238. result.misspelledWords.map(w => [w.text, w.suggestions])
  239. ).to.deep.equal([
  240. // Words in the payload that we now know were misspelled
  241. ['bbb', ['box', 'bass']],
  242. ['ddd', ['docs', 'dance']],
  243. // Word we already knew was misspelled, preserved here
  244. ['fff', ['food', 'fleece']],
  245. ])
  246. })
  247. })
  248. })