Skip to content

Commit

Permalink
feat: Support preferred suggestions (#3885)
Browse files Browse the repository at this point in the history
To support AutoFix, it is necessary to distinguish which suggestions are preferred. Preferred suggestions are AutoFix candidates.
  • Loading branch information
Jason3S committed Nov 30, 2022
1 parent 9668bef commit 27b0fa8
Show file tree
Hide file tree
Showing 16 changed files with 165 additions and 68 deletions.
32 changes: 20 additions & 12 deletions packages/cspell-lib/api/api.d.ts
Expand Up @@ -477,6 +477,22 @@ declare class ImportError extends Error {

declare function combineTextAndLanguageSettings(settings: CSpellUserSettings, text: string, languageId: string | string[]): CSpellSettingsInternal;

interface ExtendedSuggestion {
word: string;
isPreferred?: boolean;
}

interface ValidationResult extends TextOffset, Pick<Issue, 'message' | 'issueType'> {
line: TextOffset;
isFlagged?: boolean | undefined;
isFound?: boolean | undefined;
}

interface ValidationIssue extends ValidationResult {
suggestions?: string[];
suggestionsEx?: ExtendedSuggestion[];
}

interface MatchRange {
startPos: number;
endPos: number;
Expand All @@ -496,11 +512,6 @@ interface IncludeExcludeOptions {
ignoreRegExpList?: RegExp[];
includeRegExpList?: RegExp[];
}
interface ValidationResult extends TextOffset, Pick<Issue, 'message' | 'issueType'> {
line: TextOffset;
isFlagged?: boolean | undefined;
isFound?: boolean | undefined;
}
type LineValidatorFn = (line: LineSegment) => Iterable<ValidationResult>;
interface LineSegment {
line: TextOffsetRO;
Expand All @@ -521,9 +532,9 @@ interface TextValidator {
lineValidator: LineValidator;
}

interface ValidationIssue extends ValidationResult {
suggestions?: string[];
}
type Offset = number;
type SimpleRange = readonly [Offset, Offset];

interface ValidateTextOptions {
/**
* Generate suggestions where there are spelling issues.
Expand All @@ -544,9 +555,6 @@ interface ValidateTextOptions {
*/
declare function validateText(text: string, settings: CSpellUserSettings, options?: ValidateTextOptions): Promise<ValidationIssue[]>;

type Offset = number;
type SimpleRange = readonly [Offset, Offset];

interface DocumentValidatorOptions extends ValidateTextOptions {
/**
* Optional path to a configuration file.
Expand Down Expand Up @@ -613,7 +621,7 @@ declare class DocumentValidator {
private catchError;
private errorCatcherWrapper;
private _parse;
private suggest;
private getSuggestions;
private genSuggestions;
getFinalizedDocSettings(): CSpellSettingsInternal;
/**
Expand Down
4 changes: 4 additions & 0 deletions packages/cspell-lib/src/Models/Suggestion.ts
@@ -0,0 +1,4 @@
export interface ExtendedSuggestion {
word: string;
isPreferred?: boolean;
}
7 changes: 7 additions & 0 deletions packages/cspell-lib/src/Models/ValidationIssue.ts
@@ -0,0 +1,7 @@
import type { ExtendedSuggestion } from './Suggestion';
import type { ValidationResult } from './ValidationResult';

export interface ValidationIssue extends ValidationResult {
suggestions?: string[];
suggestionsEx?: ExtendedSuggestion[];
}
7 changes: 7 additions & 0 deletions packages/cspell-lib/src/Models/ValidationResult.ts
@@ -0,0 +1,7 @@
import { TextOffset as TextOffsetRW, Issue } from '@cspell/cspell-types';

export interface ValidationResult extends TextOffsetRW, Pick<Issue, 'message' | 'issueType'> {
line: TextOffsetRW;
isFlagged?: boolean | undefined;
isFound?: boolean | undefined;
}
32 changes: 28 additions & 4 deletions packages/cspell-lib/src/Settings/InDocSettings.ts
@@ -1,6 +1,7 @@
import { opAppend, opFilter, opMap, pipeSync } from '@cspell/cspell-pipe/sync';
import type { CSpellUserSettings } from '@cspell/cspell-types';
import { genSequence, Sequence } from 'gensequence';
import { ExtendedSuggestion } from '../Models/Suggestion';
import { getSpellDictInterface } from '../SpellingDictionary';
import * as Text from '../util/text';
import { clean, isDefined } from '../util/util';
Expand Down Expand Up @@ -66,6 +67,12 @@ const preferredDirectives = [
];

const allDirectives = new Set(preferredDirectives.concat(officialDirectives));
const allDirectiveSuggestions: ExtendedSuggestion[] = [
...pipeSync(
allDirectives,
opMap((word) => ({ word }))
),
];

const dictInDocSettings = getSpellDictInterface().createSpellingDictionary(
allDirectives,
Expand All @@ -90,6 +97,7 @@ export interface DirectiveIssue {
text: string;
message: string;
suggestions: string[];
suggestionsEx: ExtendedSuggestion[];
}

export function getInDocumentSettings(text: string): CSpellUserSettings {
Expand Down Expand Up @@ -156,21 +164,37 @@ function parseSettingMatchValidation(matchArray: RegExpMatchArray): DirectiveIss
// No matches were found, let make some suggestions.
const dictSugs = dictInDocSettings
.suggest(text, { ignoreCase: false })
.map((sug) => sug.word)
.filter((a) => !noSuggestDirectives.has(a));
const sugs = new Set(pipeSync(dictSugs, opAppend(allDirectives)));
const suggestions = [...sugs].slice(0, 8);
.map(({ word, isPreferred }) => (isPreferred ? { word, isPreferred } : { word }))
.filter((a) => !noSuggestDirectives.has(a.word));
const sugs = pipeSync(dictSugs, opAppend(allDirectiveSuggestions), filterUniqueSuggestions);
const suggestionsEx = [...sugs].slice(0, 8);
const suggestions = suggestionsEx.map((s) => s.word);

const issue: DirectiveIssue = {
range: [start, end],
text,
message: issueMessages.unknownDirective,
suggestions,
suggestionsEx,
};

return issue;
}

function* filterUniqueSuggestions(sugs: Iterable<ExtendedSuggestion>): Iterable<ExtendedSuggestion> {
const map = new Map<string, ExtendedSuggestion>();

for (const sug of sugs) {
const existing = map.get(sug.word);
if (existing) {
if (sug.isPreferred) {
existing.isPreferred = true;
}
}
yield sug;
}
}

function parseSettingMatch(matchArray: RegExpMatchArray): CSpellUserSettings[] {
const [, match = ''] = matchArray;
const possibleSetting = match.trim();
Expand Down
9 changes: 2 additions & 7 deletions packages/cspell-lib/src/textValidation/ValidationTypes.ts
@@ -1,4 +1,5 @@
import { MappedText, TextOffset as TextOffsetRW, Issue } from '@cspell/cspell-types';
import type { MappedText, TextOffset as TextOffsetRW } from '@cspell/cspell-types';
import type { ValidationResult } from '../Models/ValidationResult';

export type TextOffsetRO = Readonly<TextOffsetRW>;

Expand Down Expand Up @@ -29,12 +30,6 @@ export interface WordRangeAcc {
rangePos: number;
}

export interface ValidationResult extends TextOffsetRW, Pick<Issue, 'message' | 'issueType'> {
line: TextOffsetRW;
isFlagged?: boolean | undefined;
isFound?: boolean | undefined;
}

export type ValidationResultRO = Readonly<ValidationResult>;

export type LineValidatorFn = (line: LineSegment) => Iterable<ValidationResult>;
Expand Down
Expand Up @@ -18,6 +18,23 @@ exports[`Validator validateText with suggestions 1`] = `
"with",
"Witt",
],
"suggestionsEx": [
{
"word": "witty",
},
{
"word": "witt",
},
{
"word": "witch",
},
{
"word": "with",
},
{
"word": "Witt",
},
],
"text": "witth",
},
{
Expand All @@ -36,6 +53,23 @@ exports[`Validator validateText with suggestions 1`] = `
"fe's",
"feal",
],
"suggestionsEx": [
{
"word": "few",
},
{
"word": "fewer",
},
{
"word": "few's",
},
{
"word": "fe's",
},
{
"word": "feal",
},
],
"text": "feww",
},
{
Expand All @@ -54,6 +88,23 @@ exports[`Validator validateText with suggestions 1`] = `
"mistaker",
"mistake's",
],
"suggestionsEx": [
{
"word": "mistake",
},
{
"word": "mistakes",
},
{
"word": "mistaken",
},
{
"word": "mistaker",
},
{
"word": "mistake's",
},
],
"text": "mistaks",
},
]
Expand Down
3 changes: 2 additions & 1 deletion packages/cspell-lib/src/textValidation/checkText.ts
@@ -1,13 +1,14 @@
import { CSpellUserSettings } from '@cspell/cspell-types';
import assert from 'assert';
import { isTextDocument, TextDocument } from '../Models/TextDocument';
import type { ValidationIssue } from '../Models/ValidationIssue';
import * as Settings from '../Settings';
import { Document, resolveDocumentToTextDocument } from '../spellCheckFile';
import { MatchRange } from '../util/TextRange';
import { clean } from '../util/util';
import { DocumentValidator, DocumentValidatorOptions } from './docValidator';
import { calcTextInclusionRanges } from './textValidator';
import { validateText, ValidationIssue } from './validator';
import { validateText } from './validator';

/**
* Annotate text with issues and include / exclude zones.
Expand Down
Expand Up @@ -4,7 +4,7 @@ import * as path from 'path';
import { createTextDocument, TextDocument } from '../Models/TextDocument';
import { AutoCache } from '../util/simpleCache';
import { DocumentValidator } from './docValidator';
import { ValidationIssue } from './validator';
import { ValidationIssue } from '../Models/ValidationIssue';

const docCache = new AutoCache(_loadDoc, 100);
const fixturesDir = path.join(__dirname, '../../fixtures');
Expand Down
23 changes: 14 additions & 9 deletions packages/cspell-lib/src/textValidation/docValidator.ts
Expand Up @@ -10,13 +10,14 @@ import {
import assert from 'assert';
import { GlobMatcher } from 'cspell-glob';
import { CSpellSettingsInternal, CSpellSettingsInternalFinalized } from '../Models/CSpellSettingsInternalDef';
import { ExtendedSuggestion } from '../Models/Suggestion';
import { TextDocument, TextDocumentLine, updateTextDocument } from '../Models/TextDocument';
import { ValidationIssue } from '../Models/ValidationIssue';
import { finalizeSettings, loadConfig, mergeSettings, searchForConfig } from '../Settings';
import { loadConfigSync, searchForConfigSync } from '../Settings/Controller/configLoader';
import { DirectiveIssue, validateInDocumentSettings } from '../Settings/InDocSettings';
import { getDictionaryInternal, getDictionaryInternalSync, SpellingDictionaryCollection } from '../SpellingDictionary';
import { toError } from '../util/errors';
import { callOnce } from '../util/Memorizer';
import { AutoCache } from '../util/simpleCache';
import { MatchRange } from '../util/TextRange';
import { createTimer } from '../util/timer';
Expand All @@ -27,7 +28,7 @@ import { createMappedTextSegmenter, SimpleRange } from './parsedText';
import { calcTextInclusionRanges, defaultMaxDuplicateProblems, defaultMaxNumberOfProblems } from './textValidator';
import type { MappedTextValidationResult } from './ValidationTypes';
import { ValidationOptions } from './ValidationTypes';
import { settingsToValidateOptions, ValidateTextOptions, ValidationIssue } from './validator';
import { settingsToValidateOptions, ValidateTextOptions } from './validator';

export interface DocumentValidatorOptions extends ValidateTextOptions {
/**
Expand Down Expand Up @@ -252,8 +253,10 @@ export class DocumentValidator {
const withSugs = issues.map((t) => {
// lazy suggestion calculation.
const text = t.text;
const suggestions = callOnce(() => this.suggest(text));
return Object.defineProperty({ ...t }, 'suggestions', { enumerable: true, get: suggestions });
const suggestionsEx = this.getSuggestions(text);
t.suggestionsEx = suggestionsEx;
t.suggestions = suggestionsEx.map((s) => s.word);
return t;
});

return withSugs;
Expand Down Expand Up @@ -299,11 +302,11 @@ export class DocumentValidator {
const issueType = IssueType.directive;

function toValidationIssue(dirIssue: DirectiveIssue): ValidationIssue {
const { text, range, suggestions, message } = dirIssue;
const { text, range, suggestions, suggestionsEx, message } = dirIssue;
const offset = range[0];
const pos = document.positionAt(offset);
const line = document.getLine(pos.line);
const issue: ValidationIssue = { text, offset, line, suggestions, message, issueType };
const issue: ValidationIssue = { text, offset, line, suggestions, suggestionsEx, message, issueType };
return issue;
}

Expand Down Expand Up @@ -377,11 +380,11 @@ export class DocumentValidator {
return parser.parse(this.document.text, this.document.uri.path).parsedTexts;
}

private suggest(text: string) {
private getSuggestions(text: string): ExtendedSuggestion[] {
return this._suggestions.get(text);
}

private genSuggestions(text: string): string[] {
private genSuggestions(text: string): ExtendedSuggestion[] {
assert(this._preparations, ERROR_NOT_PREPARED);
const settings = this._preparations.docSettings;
const dict = this._preparations.dictionary;
Expand All @@ -393,7 +396,9 @@ export class DocumentValidator {
timeout: settings.suggestionsTimeout,
numChanges: settings.suggestionNumChanges,
});
return dict.suggest(text, sugOptions).map((r) => r.word);
return dict
.suggest(text, sugOptions)
.map(({ word, isPreferred }) => (isPreferred ? { word, isPreferred } : { word }));
}

public getFinalizedDocSettings(): CSpellSettingsInternal {
Expand Down
6 changes: 4 additions & 2 deletions packages/cspell-lib/src/textValidation/index.ts
@@ -1,9 +1,11 @@
export type { ValidationIssue } from '../Models/ValidationIssue';
export type { ValidationResult } from '../Models/ValidationResult';
export { checkText, checkTextDocument, IncludeExcludeFlag } from './checkText';
export type { CheckTextInfo, TextInfoItem } from './checkText';
export { DocumentValidator } from './docValidator';
export type { DocumentValidatorOptions } from './docValidator';
export type { Offset, SimpleRange } from './parsedText';
export { calcTextInclusionRanges } from './textValidator';
export type { CheckOptions, IncludeExcludeOptions, ValidationOptions, ValidationResult } from './ValidationTypes';
export type { CheckOptions, IncludeExcludeOptions, ValidationOptions } from './ValidationTypes';
export { validateText } from './validator';
export type { ValidateTextOptions, ValidationIssue } from './validator';
export type { ValidateTextOptions } from './validator';
Expand Up @@ -2,6 +2,7 @@ import { opConcatMap, opFilter, opMap, pipe, toArray } from '@cspell/cspell-pipe
import { ParsedText } from '@cspell/cspell-types';
import { CachingDictionary, createCachingDictionary, SearchOptions, SpellingDictionary } from 'cspell-dictionary';
import { genSequence, Sequence } from 'gensequence';
import type { ValidationResult } from '../Models/ValidationResult';
import * as RxPat from '../Settings/RegExpPatterns';
import * as Text from '../util/text';
import { clean } from '../util/util';
Expand All @@ -16,7 +17,6 @@ import type {
TextOffsetRO,
TextValidatorFn,
ValidationOptions,
ValidationResult,
ValidationResultRO,
} from './ValidationTypes';

Expand Down
6 changes: 3 additions & 3 deletions packages/cspell-lib/src/textValidation/parsedText.ts
@@ -1,7 +1,7 @@
import { TextOffset, MappedText } from '@cspell/cspell-types';
import { ValidationIssue } from './validator';
import * as TextRange from '../util/TextRange';
import { MappedText, TextOffset } from '@cspell/cspell-types';
import type { ValidationIssue } from '../Models/ValidationIssue';
import { extractTextMapRangeOrigin } from '../util/TextMap';
import * as TextRange from '../util/TextRange';

export type Offset = number;

Expand Down

0 comments on commit 27b0fa8

Please sign in to comment.