Skip to content

Commit

Permalink
feat: Add insert directive commands (#2968)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S committed Dec 8, 2023
1 parent d1b3ac3 commit 0c3fe26
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 21 deletions.
4 changes: 4 additions & 0 deletions docs/_includes/generated-docs/commands.md
Expand Up @@ -31,6 +31,10 @@
| `cSpell.goToNextSpellingIssueAndSuggest` | Go to Next Spelling Issue and Suggest |
| `cSpell.goToPreviousSpellingIssue` | Go to Previous Spelling Issue |
| `cSpell.goToPreviousSpellingIssueAndSuggest` | Go to Previous Spelling Issue and Suggest |
| `cSpell.insertDisableLineDirective` | Insert Disable Current Line Directive |
| `cSpell.insertDisableNextLineDirective` | Insert Disable Next Line Directive |
| `cSpell.insertIgnoreWordsDirective` | Insert Ignore Words Directive |
| `cSpell.insertWordsDirective` | Insert Words Directive |
| `cSpell.issueViewer.item.addWordToDictionary` | Add Word to Dictionary<br>**When:**<br> `view == cspell-info.issuesView` |
| `cSpell.issueViewer.item.autoFixSpellingIssues` | Fix issue with preferred suggestion in the current document.<br>**When:**<br> `view == cspell-info.issuesView` |
| `cSpell.issueViewer.item.openSuggestionsForIssue` | Show Suggestions<br>**When:**<br> `view == cspell-info.issuesView` |
Expand Down
24 changes: 24 additions & 0 deletions package.json
Expand Up @@ -434,6 +434,30 @@
"title": "Add Word to Dictionary",
"icon": "$(book)",
"enablement": "view == cspell-info.issuesView"
},
{
"command": "cSpell.insertDisableNextLineDirective",
"category": "Spell",
"title": "Insert Disable Next Line Directive",
"icon": "$(comment-discussion)"
},
{
"command": "cSpell.insertDisableLineDirective",
"category": "Spell",
"title": "Insert Disable Current Line Directive",
"icon": "$(comment-discussion)"
},
{
"command": "cSpell.insertIgnoreWordsDirective",
"category": "Spell",
"title": "Insert Ignore Words Directive",
"icon": "$(comment-discussion)"
},
{
"command": "cSpell.insertWordsDirective",
"category": "Spell",
"title": "Insert Words Directive",
"icon": "$(comment-discussion)"
}
],
"languages": [
Expand Down
25 changes: 24 additions & 1 deletion packages/_server/src/config/documentSettings.mts
Expand Up @@ -109,6 +109,7 @@ const defaultCheckOnlyEnabledFileTypes = true;
type ClearFn = () => void;

const fileConfigsToImport = '.vscode.configs.to.import.cspell.config.json';
const fileConfigLocalImport = '.vscode.configs.import.cspell.config.json';
const fileVSCodeSettings = '.vscode.folder.settings.json';

export class DocumentSettings {
Expand All @@ -123,6 +124,7 @@ export class DocumentSettings {
private readonly importedSettings = this.createLazy(() => this._importSettings());
private _version = 0;
private gitIgnore = new GitIgnore();
private loader = getDefaultConfigLoader();

constructor(
readonly connection: Connection,
Expand Down Expand Up @@ -322,6 +324,21 @@ export class DocumentSettings {

readonly extractTargetDictionaries = extractTargetDictionaries;

private async fetchLocalImportSettings(
uri: URL,
useLocallyInstalledCSpellDictionaries: boolean | undefined,
): Promise<CSpellUserSettings> {
const cSpellConfigSettings: CSpellUserSettings = {
id: 'VSCode-Config-Imports',
name: 'VS Code Settings Local Imports',
import: useLocallyInstalledCSpellDictionaries ? ['@cspell/cspell-bundled-dicts'] : [],
readonly: true,
};

const configFile = this.loader.createCSpellConfigFile(uri, cSpellConfigSettings);
return this.loader.mergeConfigFileWithImports(configFile);
}

private async fetchSettingsFromVSCode(uri?: string): Promise<CSpellUserSettings> {
const { cSpell, search } = await this.fetchVSCodeConfiguration(uri || '');
const { exclude = {} } = search;
Expand Down Expand Up @@ -352,7 +369,7 @@ export class DocumentSettings {
if (uriSpecial && uri !== uriSpecial) {
return this.fetchSettingsForUri(uriSpecial.toString());
}
const loader = getDefaultConfigLoader();
const loader = this.loader;
const folders = await this.folders;
const useUriForConfig = docUri || folders[0]?.uri || defaultRootUri;
const useURLForConfig = new URL(fileVSCodeSettings, useUriForConfig);
Expand All @@ -362,6 +379,10 @@ export class DocumentSettings {
const vscodeCSpellConfigSettingsForDocument = await this.resolveWorkspacePaths(vscodeCSpellConfigSettingsRel, useUriForConfig);
const vscodeCSpellConfigFileForDocument = loader.createCSpellConfigFile(useURLForConfig, vscodeCSpellConfigSettingsForDocument);
const vscodeCSpellSettings: CSpellUserSettings = await loader.mergeConfigFileWithImports(vscodeCSpellConfigFileForDocument);
const localDictionarySettings: CSpellUserSettings = await this.fetchLocalImportSettings(
new URL(fileConfigLocalImport, useUriForConfig),
vscodeCSpellConfigSettingsRel.useLocallyInstalledCSpellDictionaries,
);
const settings = vscodeCSpellConfigSettingsForDocument.noConfigSearch ? undefined : await searchForConfig(searchForFsPath);
const rootFolder = this.rootSchemaAndDomainFolderForUri(docUri);
const folder = await this.findMatchingFolder(docUri, folders[0] || rootFolder);
Expand All @@ -370,6 +391,7 @@ export class DocumentSettings {
const mergedSettingsFromVSCode = mergeSettings(await this.importedSettings(), vscodeCSpellSettings);
const mergedSettings = mergeSettings(
await Promise.resolve(this.defaultSettings),
localDictionarySettings,
filterMergeFields(
mergedSettingsFromVSCode,
vscodeCSpellSettings['mergeCSpellSettings'] || !settings,
Expand Down Expand Up @@ -656,6 +678,7 @@ export function extractCSpellFileConfigurations(settings: CSpellUserSettings): C
.filter(({ source }) => !regExIsOwnedByExtension.test(source.filename))
.filter(({ source }) => !source.filename.endsWith(fileConfigsToImport))
.filter(({ source }) => !source.filename.endsWith(fileVSCodeSettings))
.filter(({ source }) => !source.filename.endsWith(fileConfigLocalImport))
.reverse();

return configs;
Expand Down
45 changes: 30 additions & 15 deletions packages/client/src/autocomplete.ts
Expand Up @@ -11,7 +11,7 @@ import type { Disposable } from './disposable';
import type { SpellingDiagnostic } from './issueTracker';
import { getSettingFromVSConfig } from './settings/vsConfig';

const regExCSpellInDocDirective = /\b(?:spell-?checker|c?spell)::?(.*)/gi;
const regExCSpellInDocDirective = /\b(?:spell-?checker|c?spell|LocalWords)::?(.*)/gi;
const regExCSpellDirectiveKey = /(?<=\b(?:spell-?checker|c?spell)::?)(?!:)\s*(.*)/i;
// const regExInFileSettings = [regExCSpellInDocDirective, /\b(LocalWords:?.*)/g];

Expand Down Expand Up @@ -150,9 +150,16 @@ class CSpellInlineDirectiveCompletionProvider implements InlineCompletionItemPro
};

const regDir = new RegExp(regExCSpellDirectiveKey);
regDir.lastIndex = match.index || 0;
const matchIndex = match.index || 0;
regDir.lastIndex = matchIndex;
const matchDir = regDir.exec(linePrefix);
if (!matchDir) return undefined;
if (!matchDir) {
const directiveLocalWords = 'LocalWords:';
if (match[0].startsWith(directiveLocalWords)) {
return generateWordInlineCompletionItems(document, position, lineText, matchIndex + directiveLocalWords.length);
}
return undefined;
}

const directive = matchDir[1];
const startChar = (matchDir.index || 0) + matchDir[0].length - directive.length;
Expand All @@ -166,7 +173,7 @@ class CSpellInlineDirectiveCompletionProvider implements InlineCompletionItemPro
}

if (directive.startsWith('words') || directive.startsWith('ignore')) {
return generateWordInlineCompletionItems(document, position, lineText, startChar);
return generateWordInlineCompletionItems(document, position, lineText, getDirectiveStart(lineText, startChar));
}

const parts = directive.split(/\s+/);
Expand Down Expand Up @@ -288,15 +295,8 @@ function generateWordInlineCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
line: string,
startIndexForDirective: number,
startDirChar: number,
): InlineCompletionList | undefined {
const regDir = new RegExp(regExCSpellDirectiveKey, 's');
regDir.lastIndex = startIndexForDirective;
const matchDirective = regDir.exec(line);
if (!matchDirective) return undefined;

const directive = matchDirective[1];
const startDirChar = (matchDirective.index || 0) + matchDirective[0].length - directive.length;
const curIndex = position.character;

const endIndex = findNextNonWordChar(line, startDirChar);
Expand All @@ -311,20 +311,35 @@ function generateWordInlineCompletionItems(
const suffix = regExHasSpaceAfter.exec(line) ? '' : ' ';
const lastWordBreak = line.lastIndexOf(' ', curIndex - 1) + 1;
const prefix = lastWordBreak <= startDirChar ? ' ' : '';
const words = sortIssuesBy(document, position, issues);
const wordPrefix = line.slice(prefix ? curIndex : lastWordBreak, curIndex);
const words = sortIssuesBy(document, position, issues, wordPrefix);
// console.log('words: %o', { words, directive, curIndex, endIndex, lastWordBreak, prefix, suffix });
const range = new Range(position.line, lastWordBreak, position.line, curIndex);

return new InlineCompletionList(words.map((insertText) => new InlineCompletionItem(prefix + insertText + suffix, range)));
}

function sortIssuesBy(document: TextDocument, position: Position, issues: SpellingDiagnostic[]): string[] {
function getDirectiveStart(line: string, startIndexForDirective: number): number {
const regDir = new RegExp(regExCSpellDirectiveKey, 's');
regDir.lastIndex = startIndexForDirective;
const matchDirective = regDir.exec(line);
if (!matchDirective) return 0;

const directive = matchDirective[1];
const startDirChar = (matchDirective.index || 0) + matchDirective[0].length - directive.length;

return startDirChar;
}

function sortIssuesBy(document: TextDocument, position: Position, issues: SpellingDiagnostic[], wordPrefix: string): string[] {
// Look for close by issues first, otherwise sort alphabetically.

const numLines = 3;
const line = position.line;
const nearbyRange = new Range(Math.max(line - numLines, 0), 0, line + numLines, 0);
const nearbyIssues = issues.filter((i) => nearbyRange.contains(i.range));
const nearbyIssues = issues
.filter((i) => nearbyRange.contains(i.range))
.filter((i) => document.getText(i.range).startsWith(wordPrefix));
if (nearbyIssues.length) {
nearbyIssues.sort((a, b) => Math.abs(a.range.start.line - line) - Math.abs(b.range.start.line - line));
const words = new Set(nearbyIssues.map((i) => document.getText(i.range)));
Expand Down
92 changes: 87 additions & 5 deletions packages/client/src/commands.ts
@@ -1,5 +1,5 @@
import type { Command, ConfigurationScope, Diagnostic, Disposable, TextDocument, TextEdit, Uri } from 'vscode';
import { commands, FileType, Position, Range, Selection, TextEditorRevealType, window, workspace } from 'vscode';
import type { Command, ConfigurationScope, Diagnostic, Disposable, TextDocument, TextEdit, TextEditor, TextEditorEdit, Uri } from 'vscode';
import { commands, FileType, Position, Range, Selection, SnippetString, TextEditorRevealType, window, workspace } from 'vscode';
import type { Position as LsPosition, Range as LsRange, TextEdit as LsTextEdit } from 'vscode-languageclient/node';

import { addWordToFolderDictionary, addWordToTarget, addWordToUserDictionary, addWordToWorkspaceDictionary, fnWTarget } from './addWords';
Expand Down Expand Up @@ -68,7 +68,7 @@ const commandsFromServer: ClientSideCommandHandlerApi = {

type CommandHandler = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key in string]: (...params: any[]) => void | Promise<void>;
[key in string]: (...params: any[]) => void | Promise<void> | Promise<unknown>;
};

const prompt = onCommandUseDiagsSelectionOrPrompt;
Expand Down Expand Up @@ -151,6 +151,11 @@ export const commandHandlers = {
'cSpell.issueViewer.item.openSuggestionsForIssue': handlerResolvedLater,
'cSpell.issueViewer.item.autoFixSpellingIssues': handlerResolvedLater,
'cSpell.issueViewer.item.addWordToDictionary': handlerResolvedLater,

'cSpell.insertDisableNextLineDirective': handleInsertDisableNextLineDirective,
'cSpell.insertDisableLineDirective': handleInsertDisableLineDirective,
'cSpell.insertIgnoreWordsDirective': handleInsertIgnoreWordsDirective,
'cSpell.insertWordsDirective': handleInsertWordsDirective,
} as const satisfies CommandHandler;

type ImplementedCommandHandlers = typeof commandHandlers;
Expand All @@ -161,9 +166,16 @@ export const knownCommands = Object.fromEntries(
) as Record<ImplementedCommandNames, ImplementedCommandNames>;

export function registerCommands(): Disposable[] {
const registeredHandlers = Object.entries(commandHandlers).map(([cmd, fn]) => registerCmd(cmd, fn));
const skipRegister = new Set<string>();
const registeredHandlers = Object.entries(commandHandlers)
.filter(([cmd]) => !skipRegister.has(cmd))
.map(([cmd, fn]) => registerCmd(cmd, fn));
const registeredFromServer = Object.entries(commandsFromServer).map(([cmd, fn]) => registerCmd(cmd, fn));
return [...registeredHandlers, ...registeredFromServer];
return [
...registeredHandlers,
...registeredFromServer,
// commands.registerTextEditorCommand(knownCommands['cSpell.insertDisableNextLineDirective'], handleInsertDisableNextLineDirective),
];
}

function handlerResolvedLater() {}
Expand Down Expand Up @@ -499,3 +511,73 @@ async function handleSelectRange(uri?: Uri, range?: Range): Promise<void> {
// editor.selection = new Selection(range.start, range.end);
await window.showTextDocument(uri, { selection: range });
}

const snippedBlockCommentStart = '${BLOCK_COMMENT_START/^(<!--)$/$1-/}';
const snippedBlockCommentEnd = '${BLOCK_COMMENT_END/^(-->)$/-$1/}';

function handleInsertDisableNextLineDirective(textEditor?: TextEditor, _edit?: TextEditorEdit): Promise<boolean | undefined> {
return handleErrors(async () => {
const editor = textEditor || window.activeTextEditor;
if (!editor) return;
const { document, selection } = editor;
const { line } = selection.active;
const textLine = document.lineAt(line);
const prefix = textLine.text.slice(0, textLine.firstNonWhitespaceCharacterIndex);
const suffix = textLine.text.length > textLine.firstNonWhitespaceCharacterIndex ? '\n' : '';
return await editor.insertSnippet(
new SnippetString(`${prefix}${snippedBlockCommentStart} cspell:disable-next-line ${snippedBlockCommentEnd}` + suffix),
new Range(textLine.range.start, textLine.range.start),
);
}, 'handleInsertDisableNextLineDirective');
}

function handleInsertDisableLineDirective(textEditor?: TextEditor, _edit?: TextEditorEdit): Promise<boolean | undefined> {
return handleErrors(async () => {
const editor = textEditor || window.activeTextEditor;
if (!editor) return;
const { document, selection } = editor;
const { line } = selection.active;
const textLine = document.lineAt(line);
const prefix = textLine.text.endsWith(' ') ? '' : ' ';
return await editor.insertSnippet(
new SnippetString(`${prefix}${snippedBlockCommentStart} cspell:disable-line ${snippedBlockCommentEnd}\n`),
new Range(textLine.range.end, textLine.range.end),
);
}, 'handleInsertDisableLineDirective');
}

function handleInsertIgnoreWordsDirective(textEditor?: TextEditor, _edit?: TextEditorEdit): Promise<boolean | undefined> {
return handleErrors(async () => {
const editor = textEditor || window.activeTextEditor;
if (!editor) return;
const { document, selection } = editor;
const { line } = selection.active;
const textLine = document.lineAt(line);
const prefix = textLine.text.slice(0, textLine.firstNonWhitespaceCharacterIndex);
const suffix = textLine.text.length > textLine.firstNonWhitespaceCharacterIndex ? '\n' : '';
return await editor.insertSnippet(
new SnippetString(
`${prefix}${snippedBlockCommentStart} cspell:ignore \${0:$TM_CURRENT_WORD} ${snippedBlockCommentEnd}` + suffix,
),
new Range(textLine.range.start, textLine.range.start),
);
}, 'handleInsertIgnoreWordsDirective');
}

function handleInsertWordsDirective(textEditor?: TextEditor, _edit?: TextEditorEdit): Promise<boolean | undefined> {
return handleErrors(async () => {
const editor = textEditor || window.activeTextEditor;
if (!editor) return;
const { document, selection } = editor;
const { line } = selection.active;
const textLine = document.lineAt(line);
const prefix = textLine.text.slice(0, textLine.firstNonWhitespaceCharacterIndex);
const suffix = textLine.text.length > textLine.firstNonWhitespaceCharacterIndex ? '\n' : '';
return await editor.insertSnippet(
new SnippetString(
`${prefix}${snippedBlockCommentStart} cspell:words \${0:$TM_CURRENT_WORD} ${snippedBlockCommentEnd}` + suffix,
),
new Range(textLine.range.start, textLine.range.start),
);
}, 'handleInsertWordsDirective');
}

0 comments on commit 0c3fe26

Please sign in to comment.