Skip to content

Commit

Permalink
feat: Improve trace words command results. (#1558)
Browse files Browse the repository at this point in the history
* dev: use findWord to search for words in Trie
    It is needed to return more comprehensive results.
* dev: Add find method to `SpellingDictionary`
* test: improve code coverage
* dev: enrich trace results.
* dev: display new trace results
* dev: fix name of dictionaries from settings.
* dev: sort the list of dictionaries before showing them.
  • Loading branch information
Jason3S committed Aug 21, 2021
1 parent fa4b28b commit ed8a5dc
Show file tree
Hide file tree
Showing 18 changed files with 525 additions and 151 deletions.
44 changes: 43 additions & 1 deletion packages/cspell-lib/src/SpellingDictionary/Dictionaries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('Validate getDictionary', () => {
${'rhone'} | ${ignoreCaseFalse} | ${false}
${'rhone'} | ${ignoreCaseTrue} | ${true}
${'snarf'} | ${ignoreCaseTrue} | ${false}
`('tests that userWords are included in the dictionary', async ({ word, opts, expected }) => {
`('tests that userWords are included in the dictionary $word', async ({ word, opts, expected }) => {
const settings = {
...getDefaultSettings(),
dictionaries: [],
Expand All @@ -54,6 +54,37 @@ describe('Validate getDictionary', () => {
expect(dict.has(word, opts)).toBe(expected);
});

test.each`
word | expected
${'zero'} | ${{ found: 'zero', forbidden: false, noSuggest: false }}
${'zeros'} | ${{ found: 'zeros', forbidden: false, noSuggest: true }}
${'google'} | ${{ found: 'google', forbidden: false, noSuggest: true }}
${'Café'} | ${{ found: 'café', forbidden: false, noSuggest: false }}
${'CAFÉ'} | ${{ found: 'café', forbidden: false, noSuggest: false }}
${'café'} | ${{ found: 'café', forbidden: false, noSuggest: false }}
${'cafe'} | ${{ found: 'cafe', forbidden: false, noSuggest: false }}
${'CAFE'} | ${{ found: 'cafe', forbidden: false, noSuggest: false }}
${'Rhône'} | ${{ found: 'Rhône', forbidden: false, noSuggest: false }}
${'RHÔNE'} | ${{ found: 'rhône', forbidden: false, noSuggest: false }}
${'rhône'} | ${{ found: 'rhône', forbidden: false, noSuggest: false }}
${'rhone'} | ${{ found: 'rhone', forbidden: false, noSuggest: false }}
${'snarf'} | ${{ found: 'snarf', forbidden: true, noSuggest: false }}
${'hte'} | ${{ found: 'hte', forbidden: true, noSuggest: false }}
${'colour'} | ${{ found: 'colour', forbidden: true, noSuggest: false }}
`('find words $word', async ({ word, expected }) => {
const settings: CSpellUserSettings = {
...getDefaultSettings(),
noSuggestDictionaries: ['companies'],
words: ['one', 'two', 'three', 'café', '!snarf'],
userWords: ['four', 'five', 'six', 'Rhône'],
ignoreWords: ['zeros'],
flagWords: ['hte', 'colour'],
};

const dict = await Dictionaries.getDictionary(settings);
expect(dict.find(word)).toEqual(expected);
});

test.each`
word | opts | expected
${'zero'} | ${undefined} | ${false}
Expand Down Expand Up @@ -91,6 +122,17 @@ describe('Validate getDictionary', () => {
expect(dict.has(word, opts)).toBe(expected);
});

test('Dictionary NOT Found', async () => {
const settings: CSpellUserSettings = {
dictionaryDefinitions: [{ name: 'my-words', path: './not-found.txt' }],
dictionaries: ['my-words'],
};

const dict = await Dictionaries.getDictionary(settings);
expect(dict.getErrors()).toEqual([expect.objectContaining(new Error('my-words: failed to load'))]);
expect(dict.dictionaries.map((d) => d.name)).toEqual(['my-words', '[words]', '[ignoreWords]', '[flagWords]']);
});

test('Refresh Dictionary Cache', async () => {
const tempDictPath = path.join(__dirname, '..', '..', 'temp', 'words.txt');
await fs.mkdirp(path.dirname(tempDictPath));
Expand Down
37 changes: 24 additions & 13 deletions packages/cspell-lib/src/SpellingDictionary/Dictionaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,7 @@ export function loadDictionaries(
): Promise<SpellingDictionary>[] {
const defsToLoad = filterDictDefsToLoad(dictIds, defs);

return defsToLoad
.map((def) => loadDictionary(def.path, def))
.map((p) => p.catch(() => undefined))
.filter((p) => !!p)
.map((a) => a as Promise<SpellingDictionary>);
return defsToLoad.map((def) => loadDictionary(def.path, def));
}

export function refreshDictionaryCache(maxAge?: number): Promise<void> {
Expand All @@ -42,14 +38,29 @@ export function getDictionary(settings: CSpellUserSettings): Promise<SpellingDic
return { ...def, noSuggest: enabled };
});
const spellDictionaries = loadDictionaries(colDicts.enabled(), modDefs);
const settingsDictionary = createSpellingDictionary(words.concat(userWords), 'user_words', 'From Settings', {
caseSensitive: true,
});
const ignoreWordsDictionary = createSpellingDictionary(ignoreWords, 'ignore_words', 'From Settings', {
caseSensitive: true,
noSuggest: true,
});
const flagWordsDictionary = createForbiddenWordsDictionary(flagWords, 'flag_words', 'From Settings', {});
const settingsDictionary = createSpellingDictionary(
words.concat(userWords),
'[words]',
'From Settings `words` and `userWords`',
{
caseSensitive: true,
}
);
const ignoreWordsDictionary = createSpellingDictionary(
ignoreWords,
'[ignoreWords]',
'From Settings `ignoreWords`',
{
caseSensitive: true,
noSuggest: true,
}
);
const flagWordsDictionary = createForbiddenWordsDictionary(
flagWords,
'[flagWords]',
'From Settings `flagWords`',
{}
);
return createCollectionP(
[...spellDictionaries, settingsDictionary, ignoreWordsDictionary, flagWordsDictionary],
'dictionary collection'
Expand Down
26 changes: 15 additions & 11 deletions packages/cspell-lib/src/SpellingDictionary/SpellingDictionary.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import { SuggestionCollector, SuggestionResult, CompoundWordsMethod } from 'cspell-trie-lib';
import { ReplaceMap } from '@cspell/cspell-types';

export {
CompoundWordsMethod,
JOIN_SEPARATOR,
SuggestionCollector,
suggestionCollector,
SuggestionResult,
WORD_SEPARATOR,
} from 'cspell-trie-lib';
export { CompoundWordsMethod, SuggestionCollector, SuggestionResult } from 'cspell-trie-lib';

export interface SearchOptions {
useCompounds?: boolean | number;
Expand All @@ -22,7 +15,18 @@ export interface SuggestOptions {
ignoreCase?: boolean;
}

export type HasOptions = boolean | SearchOptions;
export type FindOptions = SearchOptions;

export interface FindResult {
/** the text found, otherwise `false` */
found: string | false;
/** `true` if it is considered a forbidden word. */
forbidden: boolean;
/** `true` if it is a no-suggest word. */
noSuggest: boolean;
}

export type HasOptions = SearchOptions;

export interface SpellingDictionaryOptions {
repMap?: ReplaceMap;
Expand All @@ -36,9 +40,9 @@ export interface SpellingDictionary {
readonly type: string;
readonly source: string;
readonly containsNoSuggestWords: boolean;
has(word: string, useCompounds: boolean): boolean;
has(word: string, options: HasOptions): boolean;
has(word: string, options?: HasOptions): boolean;
/** A more detailed search for a word, might take longer than `has` */
find(word: string, options?: SearchOptions): FindResult | undefined;
isForbidden(word: string): boolean;
isNoSuggestWord(word: string, options: HasOptions): boolean;
suggest(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as Trie from 'cspell-trie-lib';
import { SpellingDictionaryCollection, createCollectionP, createCollection } from './SpellingDictionaryCollection';
import { CompoundWordsMethod } from './SpellingDictionaryMethods';
import {
createFailedToLoadDictionary,
createForbiddenWordsDictionary,
createSpellingDictionary,
} from './createSpellingDictionary';
import { SpellingDictionaryFromTrie } from './SpellingDictionaryFromTrie';
import { CompoundWordsMethod } from './SpellingDictionary';
import { createCollection, createCollectionP, SpellingDictionaryCollection } from './SpellingDictionaryCollection';
import { SpellingDictionaryLoadError } from './SpellingDictionaryError';
import { SpellingDictionaryFromTrie } from './SpellingDictionaryFromTrie';

describe('Verify using multiple dictionaries', () => {
const wordsA = [
Expand All @@ -28,6 +28,15 @@ describe('Verify using multiple dictionaries', () => {
const wordsC = ['ant', 'snail', 'beetle', 'worm', 'stink bug', 'centipede', 'millipede', 'flea', 'fly'];
const wordsD = ['red*', 'green*', 'blue*', 'pink*', 'black*', '*berry', '+-fruit', '*bug', 'pinkie'];
const wordsF = ['!pink*', '+berry', '+bug', '!stinkbug'];

const wordsLegacy = ['error', 'code', 'system', 'ctrl'];

// cspell:ignore pinkberry
const wordsNoSug = ['colour', 'behaviour', 'favour', 'pinkberry'];

const dictNoSug = createSpellingDictionary(wordsNoSug, 'words-no-suggest', 'test', { noSuggest: true });
const dictLegacy = createSpellingDictionary(wordsLegacy, 'legacy-dict', 'test', { useCompounds: true });

test('checks for existence', async () => {
const dicts = await Promise.all([
createSpellingDictionary(wordsA, 'wordsA', 'test', {}),
Expand Down Expand Up @@ -143,6 +152,61 @@ describe('Verify using multiple dictionaries', () => {
expect(dictCollection.has(word)).toEqual(expected);
});

test.each`
word | expected
${'redberry'} | ${{ found: 'redberry', forbidden: false, noSuggest: false }}
${'pinkberry'} | ${{ found: 'pinkberry', forbidden: false, noSuggest: true }}
${'pink'} | ${{ found: 'pink', forbidden: true, noSuggest: false }}
${'bug'} | ${{ found: 'bug', forbidden: false, noSuggest: false }}
${'blackberry'} | ${{ found: 'blackberry', forbidden: false, noSuggest: false }}
${'pinkbug'} | ${{ found: 'pinkbug', forbidden: false, noSuggest: false }}
${'colour'} | ${{ found: 'colour', forbidden: false, noSuggest: true }}
${'behaviour'} | ${{ found: 'behaviour', forbidden: false, noSuggest: true }}
`('find: "$word"', ({ word, expected }) => {
const dicts = [
createSpellingDictionary(wordsA, 'wordsA', 'test', undefined),
createSpellingDictionary(wordsB, 'wordsB', 'test', undefined),
createSpellingDictionary(wordsC, 'wordsC', 'test', undefined),
createSpellingDictionary(wordsD, 'wordsD', 'test', undefined),
createSpellingDictionary(wordsF, 'wordsF', 'test', undefined),
createForbiddenWordsDictionary(['Avocado'], 'flag_words', 'test', undefined),
dictNoSug,
];

const dictCollection = createCollection(dicts, 'test');
expect(dictCollection.find(word)).toEqual(expected);
});

// cspell:ignore error* *code ctrl* *code *berry*
test.each`
word | expected
${'redberry'} | ${{ found: 'redberry', forbidden: false, noSuggest: false }}
${'pinkberry'} | ${{ found: 'pinkberry', forbidden: false, noSuggest: true }}
${'berryberry'} | ${{ found: 'berry+berry', forbidden: false, noSuggest: false }}
${'errorcode'} | ${{ found: 'error+code', forbidden: false, noSuggest: false }}
${'ctrlcode'} | ${{ found: 'ctrl+code', forbidden: false, noSuggest: false }}
${'pink'} | ${{ found: 'pink', forbidden: true, noSuggest: false }}
${'bug'} | ${{ found: 'bug', forbidden: false, noSuggest: false }}
${'blackberry'} | ${{ found: 'blackberry', forbidden: false, noSuggest: false }}
${'pinkbug'} | ${{ found: 'pinkbug', forbidden: false, noSuggest: false }}
${'colour'} | ${{ found: 'colour', forbidden: false, noSuggest: true }}
${'behaviour'} | ${{ found: 'behaviour', forbidden: false, noSuggest: true }}
`('find compound: "$word"', ({ word, expected }) => {
const dicts = [
createSpellingDictionary(wordsA, 'wordsA', 'test', undefined),
createSpellingDictionary(wordsB, 'wordsB', 'test', undefined),
createSpellingDictionary(wordsC, 'wordsC', 'test', undefined),
createSpellingDictionary(wordsD, 'wordsD', 'test', undefined),
createSpellingDictionary(wordsF, 'wordsF', 'test', undefined),
createForbiddenWordsDictionary(['Avocado'], 'flag_words', 'test', undefined),
dictNoSug,
dictLegacy,
];

const dictCollection = createCollection(dicts, 'test');
expect(dictCollection.find(word, { useCompounds: true })).toEqual(expected);
});

// cspell:ignore pinkbug redberry
// Note: `pinkbug` is not forbidden because compound forbidden words is not yet supported.
test.each`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import { CASE_INSENSITIVE_PREFIX } from 'cspell-trie-lib';
import { genSequence } from 'gensequence';
import { getDefaultSettings } from '../Settings';
import { memorizer, memorizerKeyBy } from '../util/Memorizer';
import { isDefined } from '../util/util';
import {
CompoundWordsMethod,
FindResult,
HasOptions,
SearchOptions,
SpellingDictionary,
SpellingDictionaryOptions,
SuggestionCollector,
suggestionCollector,
SuggestionResult,
hasOptionToSearchOption,
SuggestOptions,
} from './SpellingDictionary';
import { SpellingDictionaryFromTrie } from './SpellingDictionaryFromTrie';
import {
defaultNumSuggestions,
hasOptionToSearchOption,
SuggestArgs,
suggestArgsToSuggestOptions,
suggestionCollector,
} from './SpellingDictionaryMethods';
import {
SpellingDictionary,
HasOptions,
SearchOptions,
SuggestOptions,
SpellingDictionaryOptions,
} from './SpellingDictionary';
import { CASE_INSENSITIVE_PREFIX } from 'cspell-trie-lib';
import { genSequence } from 'gensequence';
import { getDefaultSettings } from '../Settings';
import { memorizer, memorizerKeyBy } from '../util/Memorizer';
import { SpellingDictionaryFromTrie } from './SpellingDictionaryFromTrie';

function identityString(w: string): string {
return w;
Expand All @@ -45,6 +47,16 @@ export class SpellingDictionaryCollection implements SpellingDictionary {
return !!isWordInAnyDictionary(this.dictionaries, word, options) && !this.isForbidden(word);
}

public find(word: string, hasOptions?: HasOptions): FindResult | undefined {
const options = hasOptionToSearchOption(hasOptions);
const {
found = false,
forbidden = false,
noSuggest = false,
} = findInAnyDictionary(this.dictionaries, word, options) || {};
return { found, forbidden, noSuggest };
}

public isNoSuggestWord(word: string, options?: HasOptions): boolean {
return this._isNoSuggestWord(word, options);
}
Expand Down Expand Up @@ -118,7 +130,7 @@ export class SpellingDictionaryCollection implements SpellingDictionary {
private _isNoSuggestWord = memorizerKeyBy(
(word: string, options?: HasOptions) => {
if (!this.containsNoSuggestWords) return false;
return !!isNoSuggestWordInAnyDictionary(this.dictionaries, word, options || false);
return !!isNoSuggestWordInAnyDictionary(this.dictionaries, word, options || {});
},
(word: string, options?: HasOptions) => {
const opts = hasOptionToSearchOption(options);
Expand All @@ -140,6 +152,20 @@ function isWordInAnyDictionary(
return genSequence(dicts).first((dict) => dict.has(word, options));
}

function findInAnyDictionary(
dicts: SpellingDictionary[],
word: string,
options: SearchOptions
): FindResult | undefined {
const found = dicts.map((dict) => dict.find(word, options)).filter(isDefined);
if (!found.length) return undefined;
return found.reduce((a, b) => ({
found: a.forbidden ? a.found : b.forbidden ? b.found : a.found || b.found,
forbidden: a.forbidden || b.forbidden,
noSuggest: a.noSuggest || b.noSuggest,
}));
}

function isNoSuggestWordInAnyDictionary(
dicts: SpellingDictionary[],
word: string,
Expand Down
Loading

0 comments on commit ed8a5dc

Please sign in to comment.