Skip to content

Commit a59f963

Browse files
authored
fix: Support adding separators when tracing compound words (#8176)
1 parent 4407eed commit a59f963

File tree

29 files changed

+354
-98
lines changed

29 files changed

+354
-98
lines changed

packages/cspell-dictionary/src/SpellingDictionary/SpellingDictionary.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,31 @@ describe('Verify building Dictionary', () => {
119119
});
120120
});
121121

122+
describe('Validate finding compound words.', () => {
123+
const words = [
124+
'*apple*',
125+
'*apples*',
126+
'*ape*',
127+
'*able*',
128+
'*apple*',
129+
'*banana*',
130+
'*orange*',
131+
'*pear*',
132+
'*aim*',
133+
'*approach*',
134+
'*bear*',
135+
];
136+
137+
test.each`
138+
word | expected
139+
${'applebanana'} | ${'apple|banana'}
140+
`('find $word in word list', ({ word, expected }) => {
141+
const dict = createSpellingDictionary(words, 'words', 'test', opts({}));
142+
const r = dict.find(word, { compoundSeparator: '|' });
143+
expect(r?.found).toBe(expected);
144+
});
145+
});
146+
122147
describe('Validate wordSearchForms', () => {
123148
test.each`
124149
word | isCaseSensitive | ignoreCase | expected

packages/cspell-dictionary/src/SpellingDictionary/SpellingDictionary.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ export interface SearchOptions {
2020

2121
export type SearchOptionsRO = Readonly<SearchOptions>;
2222

23-
export type FindOptions = SearchOptions;
23+
export interface FindOptions extends SearchOptions {
24+
/**
25+
* Separate compound words using the specified separator.
26+
*/
27+
compoundSeparator?: string | undefined;
28+
}
29+
2430
export type FindOptionsRO = Readonly<FindOptions>;
2531

2632
export interface Suggestion {
@@ -112,7 +118,7 @@ export interface SpellingDictionary extends DictionaryInfo {
112118
readonly containsNoSuggestWords: boolean;
113119
has(word: string, options?: HasOptionsRO): boolean;
114120
/** A more detailed search for a word, might take longer than `has` */
115-
find(word: string, options?: SearchOptionsRO): FindResult | undefined;
121+
find(word: string, options?: FindOptionsRO): FindResult | undefined;
116122
/**
117123
* Checks if a word is forbidden.
118124
* @param word - word to check.

packages/cspell-dictionary/src/SpellingDictionary/SpellingDictionaryFromTrie.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { clean } from '../util/clean.js';
1414
import { createMapper, createRepMapper } from '../util/repMap.js';
1515
import * as Defaults from './defaults.js';
1616
import type {
17+
FindOptionsRO,
1718
FindResult,
1819
HasOptionsRO,
1920
SpellingDictionary,
@@ -82,13 +83,13 @@ export class SpellingDictionaryFromTrie implements SpellingDictionary {
8283
}
8384
public has(word: string, hasOptions?: HasOptionsRO): boolean {
8485
const { useCompounds, ignoreCase } = this.resolveOptions(hasOptions);
85-
const r = this._find(word, useCompounds, ignoreCase);
86+
const r = this._find(word, useCompounds, ignoreCase, undefined);
8687
return (r && !r.forbidden && !!r.found) || false;
8788
}
8889

89-
public find(word: string, hasOptions?: HasOptionsRO): FindResult | undefined {
90+
public find(word: string, hasOptions?: FindOptionsRO): FindResult | undefined {
9091
const { useCompounds, ignoreCase } = this.resolveOptions(hasOptions);
91-
const r = this._find(word, useCompounds, ignoreCase);
92+
const r = this._find(word, useCompounds, ignoreCase, hasOptions?.compoundSeparator);
9293
const { forbidden = this.#isForbidden(word) } = r || {};
9394
if (this.#ignoreForbiddenWords && forbidden) {
9495
return undefined;
@@ -99,7 +100,7 @@ export class SpellingDictionaryFromTrie implements SpellingDictionary {
99100
return { found, forbidden, noSuggest };
100101
}
101102

102-
private resolveOptions(hasOptions?: HasOptionsRO): {
103+
private resolveOptions(hasOptions?: FindOptionsRO): {
103104
useCompounds: HasOptionsRO['useCompounds'] | undefined;
104105
ignoreCase: boolean;
105106
} {
@@ -108,18 +109,23 @@ export class SpellingDictionaryFromTrie implements SpellingDictionary {
108109
return { useCompounds, ignoreCase };
109110
}
110111

111-
private _find = (word: string, useCompounds: number | boolean | undefined, ignoreCase: boolean) =>
112-
this.findAnyForm(word, useCompounds, ignoreCase);
112+
private _find = (
113+
word: string,
114+
useCompounds: number | boolean | undefined,
115+
ignoreCase: boolean,
116+
compoundSeparator: string | undefined,
117+
) => this.findAnyForm(word, useCompounds, ignoreCase, compoundSeparator);
113118

114119
private findAnyForm(
115120
word: string,
116121
useCompounds: number | boolean | undefined,
117122
ignoreCase: boolean,
123+
compoundSeparator: string | undefined,
118124
): FindAnyFormResult | undefined {
119125
const outerForms = outerWordForms(word, this.remapWord || ((word) => [this.mapWord(word)]));
120126

121127
for (const form of outerForms) {
122-
const r = this._findAnyForm(form, useCompounds, ignoreCase);
128+
const r = this._findAnyForm(form, useCompounds, ignoreCase, compoundSeparator);
123129
if (r) return r;
124130
}
125131
return undefined;
@@ -129,10 +135,15 @@ export class SpellingDictionaryFromTrie implements SpellingDictionary {
129135
mWord: string,
130136
useCompounds: number | boolean | undefined,
131137
ignoreCase: boolean,
138+
compoundSeparator: string | undefined,
132139
): FindAnyFormResult | undefined {
133-
const opts: FindWordOptions = ignoreCase
140+
let opts: FindWordOptions = ignoreCase
134141
? this.#findWordOptionsNotCaseSensitive
135142
: this.#findWordOptionsCaseSensitive;
143+
144+
if (compoundSeparator) {
145+
opts = { ...opts, compoundSeparator };
146+
}
136147
const findResult = this.trie.findWord(mWord, opts);
137148
if (findResult.found !== false) {
138149
return findResult;

packages/cspell-lib/api/api.d.ts

Lines changed: 9 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cspell-lib/src/lib/textValidation/traceWord.test.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { pathPackageFixturesURL } from '../../test-util/test.locations.js';
44
import type { TextDocumentRef } from '../Models/TextDocument.js';
55
import { searchForConfig } from '../Settings/index.js';
66
import { getDictionaryInternal } from '../SpellingDictionary/index.js';
7-
import { toUri } from '../util/Uri.js';
87
import { determineTextDocumentSettings } from './determineTextDocumentSettings.js';
98
import type { WordSplits } from './traceWord.js';
109
import { traceWord } from './traceWord.js';
@@ -17,7 +16,7 @@ const ac = (...params: Parameters<typeof expect.arrayContaining>) => expect.arra
1716
const oc = (...params: Parameters<typeof expect.objectContaining>) => expect.objectContaining(...params);
1817

1918
describe('traceWord', async () => {
20-
const doc: TextDocumentRef = { uri: toUri(import.meta.url), languageId: 'typescript' };
19+
const doc: TextDocumentRef = { uri: import.meta.url, languageId: 'typescript' };
2120
const fixtureSettings = (await searchForConfig(urlReadme)) || {};
2221
const baseSettings = await determineTextDocumentSettings(doc, fixtureSettings);
2322
const dicts = await getDictionaryInternal(baseSettings);
@@ -70,6 +69,65 @@ describe('traceWord', async () => {
7069
});
7170
});
7271

72+
describe('traceWord CPP file', async () => {
73+
const doc: TextDocumentRef = {
74+
uri: new URL('code.cpp', import.meta.url),
75+
languageId: 'cpp',
76+
text: '/* This is a CPP file. */',
77+
};
78+
const fixtureSettings = (await searchForConfig(urlReadme)) || {};
79+
const baseSettings = await determineTextDocumentSettings(doc, fixtureSettings);
80+
const dicts = await getDictionaryInternal(baseSettings);
81+
82+
test('traceWord', () => {
83+
const r = traceWord('trace', dicts, baseSettings);
84+
expect(r.filter((r) => r.found)).toEqual(
85+
ac([
86+
{
87+
word: 'trace',
88+
found: true,
89+
foundWord: 'trace',
90+
forbidden: false,
91+
noSuggest: false,
92+
dictName: 'en_us',
93+
dictSource: expect.any(String),
94+
configSource: undefined,
95+
errors: undefined,
96+
},
97+
]),
98+
);
99+
});
100+
101+
test.each`
102+
word | expected
103+
${'word'} | ${[wft('word')]}
104+
${'word_word'} | ${[wff('word_word'), wft('word'), wft('word')]}
105+
${'word_nword'} | ${[wff('word_nword'), wft('word'), wft('nword')] /* cspell:ignore nword */}
106+
${'ISpellResult'} | ${[wff('ISpellResult'), wft('I'), wft('Spell'), wft('Result')]}
107+
${'ERRORcode'} | ${[wft('ERRORcode'), wft('ERROR'), wft('code')]}
108+
`('traceWord splits $word', ({ word, expected }) => {
109+
const r = traceWord(word, dicts, baseSettings);
110+
expect(r.splits).toEqual(expected);
111+
});
112+
113+
test.each`
114+
word | expected
115+
${'word_word'} | ${{ ...wft('word'), dictName: 'en_us' }}
116+
${'ISpellResult'} | ${{ ...wft('Result'), foundWord: 'result', dictName: 'en_us' }}
117+
${'errorcode'} | ${{ ...wft('errorcode'), foundWord: 'error•code', dictName: 'cpp' }}
118+
${'ERRORcode'} | ${{ ...wft('ERRORcode'), foundWord: 'error•code', dictName: 'cpp' }}
119+
${'ERRORcode'} | ${{ ...wft('ERROR'), foundWord: 'error', dictName: 'en_us' }}
120+
${'apple-pie'} | ${{ ...wft('pie'), dictName: 'en_us' }}
121+
${"can't"} | ${{ ...wft("can't"), dictName: 'en_us' }}
122+
${'canNOT'} | ${{ ...wft('canNOT'), foundWord: 'cannot', dictName: 'en_us' }}
123+
${'baz'} | ${{ ...wft('baz'), foundWord: 'baz', dictName: '[words]', dictSource: expectedConfigURL.href }}
124+
`('traceWord check found $word', ({ word, expected }) => {
125+
const r = traceWord(word, dicts, baseSettings);
126+
const matching = r.filter((r) => r.word === expected.word && r.found === expected.found);
127+
expect(matching).toEqual(ac([oc(expected)]));
128+
});
129+
});
130+
73131
function wf(word: string, found: boolean): WordSplits {
74132
return { word, found };
75133
}

packages/cspell-lib/src/lib/textValidation/traceWord.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { CSpellSettingsWithSourceTrace } from '@cspell/cspell-types';
22

33
import { getSources } from '../Settings/index.js';
44
import type {
5+
FindOptions,
56
FindResult,
67
HasOptions,
78
SpellingDictionary,
@@ -52,16 +53,18 @@ export interface TraceResult extends Array<DictionaryTraceResult> {
5253

5354
export interface TraceOptions extends Pick<CSpellSettingsWithSourceTrace, 'source' | 'allowCompoundWords'> {
5455
ignoreCase?: boolean;
56+
compoundSeparator?: string | undefined;
5557
}
5658

5759
export function traceWord(
5860
word: string,
5961
dictCollection: SpellingDictionaryCollection,
6062
config: TraceOptions,
6163
): TraceResult {
62-
const opts: HasOptions = {
64+
const opts: FindOptions = {
6365
ignoreCase: config.ignoreCase ?? true,
6466
useCompounds: config.allowCompoundWords || false,
67+
compoundSeparator: '•',
6568
};
6669

6770
const splits = split({ text: word, offset: 0 }, 0, checkWord);

packages/cspell-lib/src/lib/trace.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ describe('Verify trace', () => {
4343
${'café'} | ${undefined} | ${undefined} | ${true} | ${undefined} | ${'en_us'} | ${true} | ${true} | ${false} | ${false} | ${'café'}
4444
${'errorcode'} | ${undefined} | ${undefined} | ${true} | ${undefined} | ${'en_us'} | ${true} | ${false} | ${false} | ${false} | ${undefined}
4545
${'errorcode'} | ${undefined} | ${undefined} | ${true} | ${true} | ${'en_us'} | ${true} | ${true} | ${false} | ${false} | ${'error+code'}
46-
${'errorcode'} | ${'cpp'} | ${undefined} | ${true} | ${true} | ${'cpp'} | ${true} | ${true} | ${false} | ${false} | ${'errorcode'}
47-
${'errorcode'} | ${'cpp'} | ${undefined} | ${true} | ${undefined} | ${'cpp'} | ${true} | ${true} | ${false} | ${false} | ${'errorcode'}
46+
${'errorcode'} | ${'cpp'} | ${undefined} | ${true} | ${true} | ${'cpp'} | ${true} | ${true} | ${false} | ${false} | ${'error•code'}
47+
${'errorcode'} | ${'cpp'} | ${undefined} | ${true} | ${undefined} | ${'cpp'} | ${true} | ${true} | ${false} | ${false} | ${'error•code'}
4848
${'hte'} | ${undefined} | ${undefined} | ${true} | ${undefined} | ${'en_us'} | ${true} | ${false} | ${false} | ${false} | ${undefined}
4949
${'hte'} | ${undefined} | ${undefined} | ${true} | ${undefined} | ${'[flagWords]'} | ${true} | ${true} | ${true} | ${false} | ${'hte'}
5050
${'Colour'} | ${undefined} | ${undefined} | ${true} | ${undefined} | ${'[ignoreWords]'} | ${true} | ${true} | ${false} | ${true} | ${'colour'}

packages/cspell-lib/src/lib/trace.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface TraceOptions {
2424
locale?: LocaleId;
2525
ignoreCase?: boolean;
2626
allowCompoundWords?: boolean;
27+
compoundSeparator?: string | undefined;
2728
}
2829

2930
export interface TraceWordResult extends Array<TraceResult> {
@@ -49,7 +50,7 @@ export async function* traceWordsAsync(
4950
settingsOrConfig: CSpellSettings | ICSpellConfigFile,
5051
options: TraceOptions | undefined,
5152
): AsyncIterableIterator<TraceWordResult> {
52-
const { languageId, locale: language, ignoreCase = true, allowCompoundWords } = options || {};
53+
const { languageId, locale: language, ignoreCase = true, allowCompoundWords, compoundSeparator } = options || {};
5354

5455
const settings = satisfiesCSpellConfigFile(settingsOrConfig)
5556
? await resolveConfigFileImports(settingsOrConfig)
@@ -92,7 +93,7 @@ export async function* traceWordsAsync(
9293
const setOfActiveDicts = new Set(activeDictionaries);
9394

9495
function processWord(word: string): TraceWordResult {
95-
const results = traceWord(word, dicts, { ...config, ignoreCase });
96+
const results = traceWord(word, dicts, { ...config, ignoreCase, compoundSeparator });
9697

9798
const r = results.map((r) => ({
9899
...r,

packages/cspell-tools/src/__snapshots__/app.test.ts.snap

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,7 @@ exports[`Validate the application > app -V 1`] = `
3636
}
3737
`;
3838

39-
exports[`Validate the application > app -V 2`] = `
40-
"9.4.0
41-
"
42-
`;
43-
44-
exports[`Validate the application > app -V 3`] = `""`;
39+
exports[`Validate the application > app -V 2`] = `""`;
4540

4641
exports[`Validate the application > app compile compound 1`] = `
4742
"

packages/cspell-tools/src/app.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,9 @@ describe('Validate the application', () => {
235235
await expect(app.run(commander, argv('-V'))).rejects.toThrow(Commander.CommanderError);
236236
expect(mock.mock.calls.length).toBe(1);
237237
expect(consoleSpy.consoleOutput()).toMatchSnapshot();
238-
expect(std.stdout).toMatchSnapshot();
238+
const versionTest = /^\d+\.\d+\.\d+.*/;
239+
const version = std.stdout.trim();
240+
expect(versionTest.test(version)).toBe(true);
239241
expect(std.stderr).toMatchSnapshot();
240242
});
241243

0 commit comments

Comments
 (0)