diff --git a/cspell.schema.json b/cspell.schema.json index d04d50eb397..40c8d925300 100644 --- a/cspell.schema.json +++ b/cspell.schema.json @@ -517,6 +517,16 @@ }, "type": "array" }, + "suggestionNumChanges": { + "default": 3, + "description": "The maximum number of changes allowed on a word to be considered a suggestions.\n\nFor example, appending an `s` onto `example` -> `examples` is considered 1 change.\n\nRange: between 1 and 5.", + "type": "number" + }, + "suggestionsTimeout": { + "default": 500, + "description": "The maximum amount of time in milliseconds to generate suggestions for a word.", + "type": "number" + }, "usePnP": { "default": false, "description": "Packages managers like Yarn 2 use a `.pnp.cjs` file to assist in loading packages stored in the repository.\n\nWhen true, the spell checker will search up the directory structure for the existence of a PnP file and load it.", @@ -866,6 +876,16 @@ "description": "Delay in ms after a document has changed before checking it for spelling errors.", "type": "number" }, + "suggestionNumChanges": { + "default": 3, + "description": "The maximum number of changes allowed on a word to be considered a suggestions.\n\nFor example, appending an `s` onto `example` -> `examples` is considered 1 change.\n\nRange: between 1 and 5.", + "type": "number" + }, + "suggestionsTimeout": { + "default": 500, + "description": "The maximum amount of time in milliseconds to generate suggestions for a word.", + "type": "number" + }, "usePnP": { "default": false, "description": "Packages managers like Yarn 2 use a `.pnp.cjs` file to assist in loading packages stored in the repository.\n\nWhen true, the spell checker will search up the directory structure for the existence of a PnP file and load it.", diff --git a/packages/cspell-lib/src/Settings/DefaultSettings.ts b/packages/cspell-lib/src/Settings/DefaultSettings.ts index c639b21ad3d..af83a7fee7b 100644 --- a/packages/cspell-lib/src/Settings/DefaultSettings.ts +++ b/packages/cspell-lib/src/Settings/DefaultSettings.ts @@ -113,6 +113,8 @@ export const _defaultSettings: Readonly = { ], maxNumberOfProblems: 100, numSuggestions: 10, + suggestionsTimeout: 500, + suggestionNumChanges: 3, words: [], userWords: [], ignorePaths: [], diff --git a/packages/cspell-lib/src/SpellingDictionary/SpellingDictionary.ts b/packages/cspell-lib/src/SpellingDictionary/SpellingDictionary.ts index adf2002670f..318a05054cc 100644 --- a/packages/cspell-lib/src/SpellingDictionary/SpellingDictionary.ts +++ b/packages/cspell-lib/src/SpellingDictionary/SpellingDictionary.ts @@ -9,10 +9,33 @@ export interface SearchOptions { } export interface SuggestOptions { + /** + * Compounding Mode. + * `NONE` is the best option. + */ compoundMethod?: CompoundWordsMethod; + /** + * The limit on the number of suggestions to generate. If `allowTies` is true, it is possible + * for more suggestions to be generated. + */ numSuggestions?: number; + /** + * Max number of changes / edits to the word to get to a suggestion matching suggestion. + */ numChanges?: number; + /** + * Allow for case-ingestive checking. + */ ignoreCase?: boolean; + /** + * If multiple suggestions have the same edit / change "cost", then included them even if + * it causes more than `numSuggestions` to be returned. + */ + includeTies?: boolean; + /** + * Maximum amount of time to allow for generating suggestions. + */ + timeout?: number; } export type FindOptions = SearchOptions; diff --git a/packages/cspell-lib/src/SpellingDictionary/SpellingDictionaryCollection.ts b/packages/cspell-lib/src/SpellingDictionary/SpellingDictionaryCollection.ts index 600805586d9..3e9af34a8af 100644 --- a/packages/cspell-lib/src/SpellingDictionary/SpellingDictionaryCollection.ts +++ b/packages/cspell-lib/src/SpellingDictionary/SpellingDictionaryCollection.ts @@ -85,7 +85,9 @@ export class SpellingDictionaryCollection implements SpellingDictionary { numSuggestions = getDefaultSettings().numSuggestions || defaultNumSuggestions, numChanges, compoundMethod, - ignoreCase = true, + ignoreCase, + includeTies, + timeout, } = suggestOptions; _suggestOptions.compoundMethod = this.options.useCompounds ? CompoundWordsMethod.JOIN_WORDS : compoundMethod; const prefixNoCase = CASE_INSENSITIVE_PREFIX; @@ -100,8 +102,9 @@ export class SpellingDictionaryCollection implements SpellingDictionary { numSuggestions, filter, changeLimit: numChanges, - includeTies: true, + includeTies, ignoreCase, + timeout, }); this.genSuggestions(collector, suggestOptions); return collector.suggestions.map((r) => ({ ...r, word: r.word })); diff --git a/packages/cspell-lib/src/SpellingDictionary/SpellingDictionaryFromTrie.ts b/packages/cspell-lib/src/SpellingDictionary/SpellingDictionaryFromTrie.ts index 146d9906dac..dc22aa4aa51 100644 --- a/packages/cspell-lib/src/SpellingDictionary/SpellingDictionaryFromTrie.ts +++ b/packages/cspell-lib/src/SpellingDictionary/SpellingDictionaryFromTrie.ts @@ -152,7 +152,9 @@ export class SpellingDictionaryFromTrie implements SpellingDictionary { const { numSuggestions = getDefaultSettings().numSuggestions || defaultNumSuggestions, numChanges, - ignoreCase = true, + includeTies, + ignoreCase, + timeout, } = suggestOptions; function filter(_word: string): boolean { return true; @@ -161,8 +163,9 @@ export class SpellingDictionaryFromTrie implements SpellingDictionary { numSuggestions, filter, changeLimit: numChanges, - includeTies: true, + includeTies, ignoreCase, + timeout, }); this.genSuggestions(collector, suggestOptions); return collector.suggestions.map((r) => ({ ...r, word: r.word })); diff --git a/packages/cspell-lib/src/SpellingDictionary/SpellingDictionaryMethods.ts b/packages/cspell-lib/src/SpellingDictionary/SpellingDictionaryMethods.ts index f770c7166cb..801dc9fa493 100644 --- a/packages/cspell-lib/src/SpellingDictionary/SpellingDictionaryMethods.ts +++ b/packages/cspell-lib/src/SpellingDictionary/SpellingDictionaryMethods.ts @@ -119,6 +119,8 @@ export function suggestArgsToSuggestOptions(args: SuggestArgs): SuggestOptions { compoundMethod, numChanges, ignoreCase, + includeTies: undefined, + timeout: undefined, }; return suggestOptions; } diff --git a/packages/cspell-lib/src/validator.ts b/packages/cspell-lib/src/validator.ts index 28c39f5249a..214da98b8ac 100644 --- a/packages/cspell-lib/src/validator.ts +++ b/packages/cspell-lib/src/validator.ts @@ -31,7 +31,16 @@ export async function validateText( return issues; } const withSugs = issues.map((t) => { - const suggestions = dict.suggest(t.text, options.numSuggestions, CompoundWordsMethod.NONE).map((r) => r.word); + const suggestions = dict + .suggest(t.text, { + numSuggestions: options.numSuggestions, + compoundMethod: CompoundWordsMethod.NONE, + includeTies: true, + ignoreCase: !(settings.caseSensitive ?? false), + timeout: settings.suggestionsTimeout, + numChanges: settings.suggestionNumChanges, + }) + .map((r) => r.word); return { ...t, suggestions }; }); diff --git a/packages/cspell-trie-lib/src/lib/suggest-en-a-star.test.ts b/packages/cspell-trie-lib/src/lib/suggest-en-a-star.test.ts index 17581d7c819..3fdf70a5d7c 100644 --- a/packages/cspell-trie-lib/src/lib/suggest-en-a-star.test.ts +++ b/packages/cspell-trie-lib/src/lib/suggest-en-a-star.test.ts @@ -241,6 +241,7 @@ function opts( changeLimit, includeTies, ignoreCase, + timeout, }; } diff --git a/packages/cspell-trie-lib/src/lib/suggest-en.test.ts b/packages/cspell-trie-lib/src/lib/suggest-en.test.ts index b40eb81f749..f486b4d62b7 100644 --- a/packages/cspell-trie-lib/src/lib/suggest-en.test.ts +++ b/packages/cspell-trie-lib/src/lib/suggest-en.test.ts @@ -179,6 +179,7 @@ function opts( changeLimit, includeTies, ignoreCase, + timeout, }; } diff --git a/packages/cspell-trie-lib/src/lib/suggest.test.ts b/packages/cspell-trie-lib/src/lib/suggest.test.ts index ff61568f482..d066128092e 100644 --- a/packages/cspell-trie-lib/src/lib/suggest.test.ts +++ b/packages/cspell-trie-lib/src/lib/suggest.test.ts @@ -14,6 +14,7 @@ const defaultOptions: SuggestionCollectorOptions = { numSuggestions: 10, ignoreCase: undefined, changeLimit: undefined, + timeout: undefined, }; describe('Validate Suggest', () => { diff --git a/packages/cspell-trie-lib/src/lib/suggest.ts b/packages/cspell-trie-lib/src/lib/suggest.ts index b500f9921bc..5e9d5eaa19b 100644 --- a/packages/cspell-trie-lib/src/lib/suggest.ts +++ b/packages/cspell-trie-lib/src/lib/suggest.ts @@ -25,6 +25,7 @@ export function suggest( changeLimit: opts.maxNumChanges, includeTies: opts.allowTies, ignoreCase: opts.ignoreCase, + timeout: opts.timeout, }); collector.collect(genSuggestions(root, word, opts)); return collector.suggestions; diff --git a/packages/cspell-trie-lib/src/lib/suggestAStar.test.ts b/packages/cspell-trie-lib/src/lib/suggestAStar.test.ts index 2ed00dc0a49..7d467b36836 100644 --- a/packages/cspell-trie-lib/src/lib/suggestAStar.test.ts +++ b/packages/cspell-trie-lib/src/lib/suggestAStar.test.ts @@ -10,6 +10,7 @@ const defaultOptions: SuggestionCollectorOptions = { ignoreCase: undefined, changeLimit: undefined, includeTies: true, + timeout: undefined, }; const stopHere = true; diff --git a/packages/cspell-trie-lib/src/lib/suggestAStar.ts b/packages/cspell-trie-lib/src/lib/suggestAStar.ts index c9a2288b12a..0b11dd438a4 100644 --- a/packages/cspell-trie-lib/src/lib/suggestAStar.ts +++ b/packages/cspell-trie-lib/src/lib/suggestAStar.ts @@ -478,6 +478,7 @@ export function suggest(root: TrieRoot | TrieRoot[], word: string, options: Sugg changeLimit: opts.maxNumChanges, includeTies: true, ignoreCase: opts.ignoreCase, + timeout: opts.timeout, }); collector.collect(genSuggestions(root, word, opts)); return collector.suggestions; diff --git a/packages/cspell-trie-lib/src/lib/suggestCollector.test.ts b/packages/cspell-trie-lib/src/lib/suggestCollector.test.ts index 284b69233d7..466b765a596 100644 --- a/packages/cspell-trie-lib/src/lib/suggestCollector.test.ts +++ b/packages/cspell-trie-lib/src/lib/suggestCollector.test.ts @@ -4,6 +4,7 @@ const defaultOptions: SuggestionCollectorOptions = { numSuggestions: 10, ignoreCase: undefined, changeLimit: undefined, + timeout: undefined, }; describe('Validate suggestCollector', () => { diff --git a/packages/cspell-trie-lib/src/lib/suggestCollector.ts b/packages/cspell-trie-lib/src/lib/suggestCollector.ts index 048c22836ed..44efb6d895c 100644 --- a/packages/cspell-trie-lib/src/lib/suggestCollector.ts +++ b/packages/cspell-trie-lib/src/lib/suggestCollector.ts @@ -87,29 +87,40 @@ export interface SuggestionCollector { export interface SuggestionCollectorOptions { /** * number of best matching suggestions. + * @default 10 */ numSuggestions: number; /** * An optional filter function that can be used to limit remove unwanted suggestions. * I.E. to remove forbidden terms. + * @default () => true */ filter?: (word: string, cost: number) => boolean; /** * The number of letters that can be changed when looking for a match + * @default 5 */ changeLimit: number | undefined; /** * Include suggestions with tied cost even if the number is greater than `numSuggestions`. + * @default true */ includeTies?: boolean; /** * specify if case / accents should be ignored when looking for suggestions. + * @default true */ ignoreCase: boolean | undefined; + + /** + * the total amount of time to allow for suggestions. + * @default 1000 + */ + timeout?: number | undefined; } export const defaultSuggestionCollectorOptions: SuggestionCollectorOptions = { @@ -118,14 +129,23 @@ export const defaultSuggestionCollectorOptions: SuggestionCollectorOptions = { changeLimit: maxNumChanges, includeTies: true, ignoreCase: true, + timeout: defaultCollectorTimeout, }; export function suggestionCollector(wordToMatch: string, options: SuggestionCollectorOptions): SuggestionCollector { - const { filter = () => true, changeLimit = maxNumChanges, includeTies = false, ignoreCase = true } = options; + const { + filter = () => true, + changeLimit = maxNumChanges, + includeTies = false, + ignoreCase = true, + timeout = defaultCollectorTimeout, + } = options; const numSuggestions = Math.max(options.numSuggestions, 0) || 0; const sugs = new Map(); let maxCost: number = baseCost * Math.min(wordToMatch.length * maxAllowedCostScale, changeLimit); + let timeRemaining = timeout; + function dropMax() { if (sugs.size < 2) { sugs.clear(); @@ -175,8 +195,12 @@ export function suggestionCollector(wordToMatch: string, options: SuggestionColl * @param src - the SuggestionIterator used to generate suggestions. * @param timeout - the amount of time in milliseconds to allow for suggestions. */ - function collect(src: SuggestionGenerator, timeout = defaultCollectorTimeout) { + function collect(src: SuggestionGenerator, timeout?: number) { let stop: false | symbol = false; + timeout = timeout ?? timeRemaining; + timeout = Math.min(timeout, timeRemaining); + if (timeout < 0) return; + const timer = createTimer(); let ir: IteratorResult; @@ -192,6 +216,8 @@ export function suggestionCollector(wordToMatch: string, options: SuggestionColl } handleProgress(value); } + + timeRemaining -= timer.elapsed(); } function suggestions() { diff --git a/packages/cspell-trie-lib/src/lib/trie.test.ts b/packages/cspell-trie-lib/src/lib/trie.test.ts index a530d079b62..f624d47302f 100644 --- a/packages/cspell-trie-lib/src/lib/trie.test.ts +++ b/packages/cspell-trie-lib/src/lib/trie.test.ts @@ -359,5 +359,6 @@ function opts( changeLimit, includeTies, ignoreCase, + timeout: undefined, }; } diff --git a/packages/cspell-types/cspell.schema.json b/packages/cspell-types/cspell.schema.json index d04d50eb397..40c8d925300 100644 --- a/packages/cspell-types/cspell.schema.json +++ b/packages/cspell-types/cspell.schema.json @@ -517,6 +517,16 @@ }, "type": "array" }, + "suggestionNumChanges": { + "default": 3, + "description": "The maximum number of changes allowed on a word to be considered a suggestions.\n\nFor example, appending an `s` onto `example` -> `examples` is considered 1 change.\n\nRange: between 1 and 5.", + "type": "number" + }, + "suggestionsTimeout": { + "default": 500, + "description": "The maximum amount of time in milliseconds to generate suggestions for a word.", + "type": "number" + }, "usePnP": { "default": false, "description": "Packages managers like Yarn 2 use a `.pnp.cjs` file to assist in loading packages stored in the repository.\n\nWhen true, the spell checker will search up the directory structure for the existence of a PnP file and load it.", @@ -866,6 +876,16 @@ "description": "Delay in ms after a document has changed before checking it for spelling errors.", "type": "number" }, + "suggestionNumChanges": { + "default": 3, + "description": "The maximum number of changes allowed on a word to be considered a suggestions.\n\nFor example, appending an `s` onto `example` -> `examples` is considered 1 change.\n\nRange: between 1 and 5.", + "type": "number" + }, + "suggestionsTimeout": { + "default": 500, + "description": "The maximum amount of time in milliseconds to generate suggestions for a word.", + "type": "number" + }, "usePnP": { "default": false, "description": "Packages managers like Yarn 2 use a `.pnp.cjs` file to assist in loading packages stored in the repository.\n\nWhen true, the spell checker will search up the directory structure for the existence of a PnP file and load it.", diff --git a/packages/cspell-types/src/settings/CSpellSettingsDef.ts b/packages/cspell-types/src/settings/CSpellSettingsDef.ts index 86ec31daf31..8f63d38d1b8 100644 --- a/packages/cspell-types/src/settings/CSpellSettingsDef.ts +++ b/packages/cspell-types/src/settings/CSpellSettingsDef.ts @@ -86,7 +86,7 @@ export interface ExtendableSettings extends Settings { overrides?: OverrideSettings[]; } -export interface Settings extends BaseSetting, PnPSettings { +export interface Settings extends ReportingConfiguration, BaseSetting, PnPSettings { /** * Current active spelling language. * @@ -118,6 +118,14 @@ export interface Settings extends BaseSetting, PnPSettings { */ enableFiletypes?: LanguageIdSingle[]; + /** Additional settings for individual languages. */ + languageSettings?: LanguageSetting[]; + + /** Forces the spell checker to assume a give language id. Used mainly as an Override. */ + languageId?: LanguageId; +} + +export interface ReportingConfiguration extends SuggestionsConfiguration { /** * The maximum number of problems to report in a file. * @default 100 @@ -135,18 +143,30 @@ export interface Settings extends BaseSetting, PnPSettings { * @default 4 */ minWordLength?: number; +} +export interface SuggestionsConfiguration { /** * Number of suggestions to make * @default 10 */ numSuggestions?: number; - /** Additional settings for individual languages. */ - languageSettings?: LanguageSetting[]; + /** + * The maximum amount of time in milliseconds to generate suggestions for a word. + * @default 500 + */ + suggestionsTimeout?: number; - /** Forces the spell checker to assume a give language id. Used mainly as an Override. */ - languageId?: LanguageId; + /** + * The maximum number of changes allowed on a word to be considered a suggestions. + * + * For example, appending an `s` onto `example` -> `examples` is considered 1 change. + * + * Range: between 1 and 5. + * @default 3 + */ + suggestionNumChanges?: number; } /**