Improve spellcheck error handling and types (#21012)

GitOrigin-RevId: 0227122154f139401793aae2c8ca9c9960fe5e98
This commit is contained in:
Alf Eaton 2024-10-11 10:24:17 +01:00 committed by Copybot
parent 0131178491
commit ecb94d6525
5 changed files with 136 additions and 52 deletions

View file

@ -19,6 +19,7 @@ import { SpellChecker } from './spellchecker'
import { parserWatcher } from '../wait-for-parser' import { parserWatcher } from '../wait-for-parser'
import type { HunspellManager } from '@/features/source-editor/hunspell/HunspellManager' import type { HunspellManager } from '@/features/source-editor/hunspell/HunspellManager'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import { captureException } from '@/infrastructure/error-reporter'
type Options = { type Options = {
spellCheckLanguage?: string spellCheckLanguage?: string
@ -81,9 +82,15 @@ const spellCheckerField = StateField.define<SpellChecker | null>({
) )
: null : null
} else if (effect.is(addIgnoredWord)) { } else if (effect.is(addIgnoredWord)) {
value?.addWord(effect.value.text).catch(debugConsole.error) value?.addWord(effect.value.text).catch(error => {
captureException(error)
debugConsole.error(error)
})
} else if (effect.is(removeIgnoredWord)) { } else if (effect.is(removeIgnoredWord)) {
value?.removeWord(effect.value.text).catch(debugConsole.error) value?.removeWord(effect.value.text).catch(error => {
captureException(error)
debugConsole.error(error)
})
} }
} }
return value return value

View file

@ -14,6 +14,7 @@ import {
import { waitForParser } from '../wait-for-parser' import { waitForParser } from '../wait-for-parser'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import type { HunspellManager } from '../../hunspell/HunspellManager' import type { HunspellManager } from '../../hunspell/HunspellManager'
import { captureException } from '@/infrastructure/error-reporter'
/* /*
* Spellchecker, handles updates, schedules spelling checks * Spellchecker, handles updates, schedules spelling checks
@ -120,10 +121,14 @@ export class SpellChecker {
type: 'spell', type: 'spell',
words: unknownWords.map(word => word.text), words: unknownWords.map(word => word.text),
}, },
(result: any) => { result => {
if (!signal.aborted) { if (!signal.aborted) {
if (result.error) { if ('error' in result) {
debugConsole.error(result.error) debugConsole.error(result.error)
captureException(
new Error('Error running spellcheck for word'),
{ language: this.language }
)
} else { } else {
processResult(result.misspellings) processResult(result.misspellings)
} }
@ -148,10 +153,10 @@ export class SpellChecker {
return new Promise<{ suggestions: string[] }>((resolve, reject) => { return new Promise<{ suggestions: string[] }>((resolve, reject) => {
if (this.hunspellManager) { if (this.hunspellManager) {
this.hunspellManager.send({ type: 'suggest', word }, result => { this.hunspellManager.send({ type: 'suggest', word }, result => {
if ((result as { error: true }).error) { if ('error' in result) {
reject(new Error()) reject(new Error('Error finding spelling suggestions for word'))
} else { } else {
resolve(result as { suggestions: string[] }) resolve(result)
} }
}) })
} }
@ -162,8 +167,8 @@ export class SpellChecker {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
if (this.hunspellManager) { if (this.hunspellManager) {
this.hunspellManager.send({ type: 'add_word', word }, result => { this.hunspellManager.send({ type: 'add_word', word }, result => {
if ((result as { error: true }).error) { if ('error' in result) {
reject(new Error()) reject(new Error('Error adding word to spellcheck'))
} else { } else {
resolve() resolve()
} }
@ -176,8 +181,8 @@ export class SpellChecker {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
if (this.hunspellManager) { if (this.hunspellManager) {
this.hunspellManager.send({ type: 'remove_word', word }, result => { this.hunspellManager.send({ type: 'remove_word', word }, result => {
if ((result as { error: true }).error) { if ('error' in result) {
reject(new Error()) reject(new Error('Error removing word from spellcheck'))
} else { } else {
resolve() resolve()
} }

View file

@ -13,6 +13,8 @@ import classnames from 'classnames'
import { useFeatureFlag } from '@/shared/context/split-test-context' import { useFeatureFlag } from '@/shared/context/split-test-context'
import { sendMB } from '@/infrastructure/event-tracking' import { sendMB } from '@/infrastructure/event-tracking'
import SpellingSuggestionsFeedback from './spelling-suggestions-feedback' import SpellingSuggestionsFeedback from './spelling-suggestions-feedback'
import { captureException } from '@/infrastructure/error-reporter'
import { debugConsole } from '@/utils/debugging'
const ITEMS_TO_SHOW = 8 const ITEMS_TO_SHOW = 8
@ -51,7 +53,9 @@ export const SpellingSuggestions: FC<{
useEffect(() => { useEffect(() => {
if (!word.suggestions) { if (!word.suggestions) {
spellChecker?.suggest(word.text).then(result => { spellChecker
?.suggest(word.text)
.then(result => {
setSuggestions(result.suggestions.slice(0, ITEMS_TO_SHOW)) setSuggestions(result.suggestions.slice(0, ITEMS_TO_SHOW))
setWaiting(false) setWaiting(false)
sendMB('spelling-suggestion-shown', { sendMB('spelling-suggestion-shown', {
@ -60,6 +64,12 @@ export const SpellingSuggestions: FC<{
// word: transaction.state.sliceDoc(mark.from, mark.to), // word: transaction.state.sliceDoc(mark.from, mark.to),
}) })
}) })
.catch(error => {
captureException(error, {
language: spellCheckLanguage,
})
debugConsole.error(error)
})
} }
}, [word, spellChecker, spellCheckLanguage]) }, [word, spellChecker, spellCheckLanguage])

View file

@ -1,32 +1,60 @@
/* eslint-disable no-dupe-class-members */
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import getMeta from '@/utils/meta' import getMeta from '@/utils/meta'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import { captureException } from '@/infrastructure/error-reporter'
type Message = type SpellMessage = {
| {
id?: string
type: 'spell' type: 'spell'
words: string[] words: string[]
} }
| {
id?: string type SuggestMessage = {
type: 'suggest' type: 'suggest'
word: string word: string
} }
| {
id?: string type AddWordMessage = {
type: 'add_word' type: 'add_word'
word: string word: string
} }
| {
id?: string type RemoveWordMessage = {
type: 'remove_word' type: 'remove_word'
word: string word: string
} }
| {
id?: string type DestroyMessage = {
type: 'destroy' type: 'destroy'
} }
type Message = { id?: string } & (
| SpellMessage
| SuggestMessage
| AddWordMessage
| RemoveWordMessage
| DestroyMessage
)
type EmptyResult = Record<string, never>
type ErrorResult = {
error: true
}
type SpellResult = {
misspellings: { index: number }[]
}
type SuggestResult = {
suggestions: string[]
}
type ResultCallback =
| ((value: SpellResult | ErrorResult) => void)
| ((value: SuggestResult | ErrorResult) => void)
| ((value: EmptyResult | ErrorResult) => void)
export class HunspellManager { export class HunspellManager {
baseAssetPath: string baseAssetPath: string
@ -35,8 +63,9 @@ export class HunspellManager {
abortController: AbortController | undefined abortController: AbortController | undefined
listening = false listening = false
loaded = false loaded = false
loadingFailed = false
pendingMessages: Message[] = [] pendingMessages: Message[] = []
callbacks: Map<string, (value: unknown) => void> = new Map() callbacks: Map<string, ResultCallback> = new Map()
constructor( constructor(
private readonly language: string, private readonly language: string,
@ -64,8 +93,36 @@ export class HunspellManager {
}) })
} }
send(message: Message, callback: (value: unknown) => void) { send(
debugConsole.log(message) message: AddWordMessage,
callback: (value: EmptyResult | ErrorResult) => void
): void
send(
message: RemoveWordMessage,
callback: (value: EmptyResult | ErrorResult) => void
): void
send(
message: DestroyMessage,
callback: (value: EmptyResult | ErrorResult) => void
): void
send(
message: SuggestMessage,
callback: (value: SuggestResult | ErrorResult) => void
): void
send(
message: SpellMessage,
callback: (value: SpellResult | ErrorResult) => void
): void
send(message: Message, callback: ResultCallback): void {
if (this.loadingFailed) {
return // ignore the message
}
if (callback) { if (callback) {
message.id = uuid() message.id = uuid()
this.callbacks.set(message.id, callback) this.callbacks.set(message.id, callback)
@ -80,14 +137,14 @@ export class HunspellManager {
receive(event: MessageEvent) { receive(event: MessageEvent) {
debugConsole.log(event.data) debugConsole.log(event.data)
const { id, listening, loaded, ...rest } = event.data const { id, ...rest } = event.data
if (id) { if (id) {
const callback = this.callbacks.get(id) const callback = this.callbacks.get(id)
if (callback) { if (callback) {
this.callbacks.delete(id) this.callbacks.delete(id)
callback(rest) callback(rest)
} }
} else if (listening) { } else if (rest.listening) {
this.listening = true this.listening = true
this.hunspellWorker.postMessage({ this.hunspellWorker.postMessage({
type: 'init', type: 'init',
@ -100,9 +157,14 @@ export class HunspellManager {
this.hunspellWorker.postMessage(message) this.hunspellWorker.postMessage(message)
this.pendingMessages.length = 0 this.pendingMessages.length = 0
} }
} else if (loaded) { } else if (rest.loaded) {
this.loaded = true this.loaded = true
// TODO: use this to display pending state? } else if (rest.loadingFailed) {
captureException(new Error('Spell check loading failed'), {
language: this.language,
})
this.loadingFailed = true
this.pendingMessages.length = 0
} }
} }
} }

View file

@ -171,7 +171,7 @@ self.addEventListener('message', async event => {
self.postMessage({ loaded: true }) self.postMessage({ loaded: true })
} catch (error) { } catch (error) {
console.error(error) console.error(error)
self.postMessage({ error: true }) self.postMessage({ loadingFailed: true })
} }
break break