Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Support Suggestion timeouts #1668

Merged
merged 1 commit into from Sep 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 20 additions & 0 deletions cspell.schema.json
Expand Up @@ -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.",
Expand Down Expand Up @@ -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.",
Expand Down
2 changes: 2 additions & 0 deletions packages/cspell-lib/src/Settings/DefaultSettings.ts
Expand Up @@ -113,6 +113,8 @@ export const _defaultSettings: Readonly<CSpellSettingsWithSourceTrace> = {
],
maxNumberOfProblems: 100,
numSuggestions: 10,
suggestionsTimeout: 500,
suggestionNumChanges: 3,
words: [],
userWords: [],
ignorePaths: [],
Expand Down
23 changes: 23 additions & 0 deletions packages/cspell-lib/src/SpellingDictionary/SpellingDictionary.ts
Expand Up @@ -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;
Expand Down
Expand Up @@ -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;
Expand All @@ -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 }));
Expand Down
Expand Up @@ -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;
Expand All @@ -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 }));
Expand Down
Expand Up @@ -119,6 +119,8 @@ export function suggestArgsToSuggestOptions(args: SuggestArgs): SuggestOptions {
compoundMethod,
numChanges,
ignoreCase,
includeTies: undefined,
timeout: undefined,
};
return suggestOptions;
}
Expand Down
11 changes: 10 additions & 1 deletion packages/cspell-lib/src/validator.ts
Expand Up @@ -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 };
});

Expand Down
1 change: 1 addition & 0 deletions packages/cspell-trie-lib/src/lib/suggest-en-a-star.test.ts
Expand Up @@ -241,6 +241,7 @@ function opts(
changeLimit,
includeTies,
ignoreCase,
timeout,
};
}

Expand Down
1 change: 1 addition & 0 deletions packages/cspell-trie-lib/src/lib/suggest-en.test.ts
Expand Up @@ -179,6 +179,7 @@ function opts(
changeLimit,
includeTies,
ignoreCase,
timeout,
};
}

Expand Down
1 change: 1 addition & 0 deletions packages/cspell-trie-lib/src/lib/suggest.test.ts
Expand Up @@ -14,6 +14,7 @@ const defaultOptions: SuggestionCollectorOptions = {
numSuggestions: 10,
ignoreCase: undefined,
changeLimit: undefined,
timeout: undefined,
};

describe('Validate Suggest', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/cspell-trie-lib/src/lib/suggest.ts
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/cspell-trie-lib/src/lib/suggestAStar.test.ts
Expand Up @@ -10,6 +10,7 @@ const defaultOptions: SuggestionCollectorOptions = {
ignoreCase: undefined,
changeLimit: undefined,
includeTies: true,
timeout: undefined,
};

const stopHere = true;
Expand Down
1 change: 1 addition & 0 deletions packages/cspell-trie-lib/src/lib/suggestAStar.ts
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/cspell-trie-lib/src/lib/suggestCollector.test.ts
Expand Up @@ -4,6 +4,7 @@ const defaultOptions: SuggestionCollectorOptions = {
numSuggestions: 10,
ignoreCase: undefined,
changeLimit: undefined,
timeout: undefined,
};

describe('Validate suggestCollector', () => {
Expand Down
30 changes: 28 additions & 2 deletions packages/cspell-trie-lib/src/lib/suggestCollector.ts
Expand Up @@ -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 = {
Expand All @@ -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<string, SuggestionResult>();
let maxCost: number = baseCost * Math.min(wordToMatch.length * maxAllowedCostScale, changeLimit);

let timeRemaining = timeout;

function dropMax() {
if (sugs.size < 2) {
sugs.clear();
Expand Down Expand Up @@ -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<SuggestionResult | Progress | undefined>;
Expand All @@ -192,6 +216,8 @@ export function suggestionCollector(wordToMatch: string, options: SuggestionColl
}
handleProgress(value);
}

timeRemaining -= timer.elapsed();
}

function suggestions() {
Expand Down
1 change: 1 addition & 0 deletions packages/cspell-trie-lib/src/lib/trie.test.ts
Expand Up @@ -359,5 +359,6 @@ function opts(
changeLimit,
includeTies,
ignoreCase,
timeout: undefined,
};
}
20 changes: 20 additions & 0 deletions packages/cspell-types/cspell.schema.json
Expand Up @@ -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.",
Expand Down Expand Up @@ -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.",
Expand Down
30 changes: 25 additions & 5 deletions packages/cspell-types/src/settings/CSpellSettingsDef.ts
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
Expand All @@ -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;
}

/**
Expand Down