Skip to content

Commit

Permalink
fix: Support Quick Fixes with custom decorators (#2897)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S authored Oct 24, 2023
1 parent 09ee972 commit 4e93033
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 90 deletions.
4 changes: 0 additions & 4 deletions packages/_server/src/server.mts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,6 @@ import * as Validator from './validator.mjs';

log('Starting Spell Checker Server');

// type ServerNotificationApiHandlers = {
// [key in keyof Api.ServerNotifyApi]: (...p: Parameters<Api.ServerNotifyApi[key]>) => void | Promise<void>;
// };

const tds = CSpell;

const defaultCheckLimit = Validator.defaultCheckLimit;
Expand Down
11 changes: 8 additions & 3 deletions packages/_server/src/validator.mts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createTextDocument, DocumentValidator } from 'cspell-lib';
import { createTextDocument, DocumentValidator, Text as TextUtil } from 'cspell-lib';
import type { TextDocument } from 'vscode-languageserver-textdocument';
import type { Diagnostic } from 'vscode-languageserver-types';
import { DiagnosticSeverity } from 'vscode-languageserver-types';

import type { SpellCheckerDiagnosticData } from './api.js';
import type { SpellCheckerDiagnosticData, Suggestion } from './api.js';
import type { CSpellUserSettings } from './config/cspellConfig/index.mjs';
import { isScmUri } from './config/docUriHelper.mjs';
import { diagnosticSource } from './constants.mjs';
Expand Down Expand Up @@ -52,13 +52,18 @@ export async function validateTextDocument(textDocument: TextDocument, options:
.map(({ text, range, isFlagged, message, issueType, suggestions, suggestionsEx, severity }) => {
const diagMessage = `"${text}": ${message ?? `${isFlagged ? 'Forbidden' : 'Unknown'} word`}.`;
const sugs = suggestionsEx || suggestions?.map((word) => ({ word }));
const data: SpellCheckerDiagnosticData = { issueType, suggestions: sugs };
const data: SpellCheckerDiagnosticData = { issueType, suggestions: haveSuggestionsMatchCase(text, sugs) };
return { severity, range, message: diagMessage, source: diagSource, data };
})
.filter((diag) => !!diag.severity);
return diags;
}

function haveSuggestionsMatchCase(example: string, suggestions: Suggestion[] | undefined): Suggestion[] | undefined {
if (!suggestions || TextUtil.isLowerCase(example)) return suggestions;
return suggestions.map((sug) => (TextUtil.isLowerCase(sug.word) ? { ...sug, word: TextUtil.matchCase(example, sug.word) } : sug));
}

type SeverityOptions = Pick<CSpellUserSettings, 'diagnosticLevel' | 'diagnosticLevelFlaggedWords' | 'diagnosticLevelSCM'>;

interface Severity {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as client from './client';
import * as client from './index';

describe('vscode-languageclient', () => {
test('client', () => {
Expand Down
56 changes: 56 additions & 0 deletions packages/client/src/codeAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Suggestion } from 'code-spell-checker-server/api';
import type { CodeActionContext, CodeActionProvider, Command, Range, Selection, TextDocument } from 'vscode';
import { CodeAction, CodeActionKind, languages } from 'vscode';

import { requestSpellingSuggestions } from './codeActions/actionSuggestSpellingCorrections';
import { createTextEditCommand } from './commands';
import { filterDiags } from './diags';
import type { IssueTracker, SpellingDiagnostic } from './issueTracker';

export class SpellCheckerCodeActionProvider implements CodeActionProvider {
public static readonly providedCodeActionKinds = [CodeActionKind.QuickFix];

constructor(readonly issueTracker: IssueTracker) {}

async provideCodeActions(
document: TextDocument,
range: Range | Selection,
context: CodeActionContext,
// token: CancellationToken,
): Promise<(CodeAction | Command)[]> {
const contextDiags = filterDiags(context.diagnostics);
if (contextDiags.length) {
// Already handled by the language server.
return [];
}

const diags = this.issueTracker.getDiagnostics(document.uri).filter((diag) => diag.range.contains(range));
if (diags.length !== 1) return [];
const pendingDiags = diags.map((diag) => this.diagToAction(document, diag));
return (await Promise.all(pendingDiags)).flatMap((action) => action);
}

private async diagToAction(doc: TextDocument, diag: SpellingDiagnostic): Promise<(CodeAction | Command)[]> {
const suggestions = diag.data?.suggestions;
if (!suggestions?.length) {
// fetch the result from the server.
const actionsFromServer = await requestSpellingSuggestions(doc, diag.range, [diag]);
return actionsFromServer;
}
return suggestions.map((sug) => suggestionToAction(doc, diag.range, sug));
}
}

function suggestionToAction(doc: TextDocument, range: Range, sug: Suggestion): CodeAction {
const title = `Replace with: ${sug.word}`;
const action = new CodeAction(title, CodeActionKind.QuickFix);
action.isPreferred = sug.isPreferred;
action.command = createTextEditCommand(title, doc.uri, doc.version, [{ range, newText: sug.word }]);
return action;
}

export function registerSpellCheckerCodeActionProvider(issueTracker: IssueTracker) {
return languages.registerCodeActionsProvider('*', new SpellCheckerCodeActionProvider(issueTracker), {
providedCodeActionKinds: SpellCheckerCodeActionProvider.providedCodeActionKinds,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { CodeAction, Diagnostic, QuickPickItem, Range, TextDocument, Uri } from 'vscode';
import { commands, window } from 'vscode';

import * as di from '../di';
import { extractMatchingDiagRanges, getCSpellDiags } from '../diags';
import { toRange } from '../languageServer/clientHelpers';
import type { RangeLike } from '../languageServer/models';
import { ConfigFields, getSettingFromVSConfig } from '../settings';
import { findEditor } from '../util/findEditor';
import { pVoid } from '../util/pVoid';

interface SuggestionQuickPickItem extends QuickPickItem {
_action: CodeAction;
}

export async function actionSuggestSpellingCorrections(docUri?: Uri, rangeLike?: RangeLike, _text?: string): Promise<void> {
// console.log('Args: %o', { docUri, range: rangeLike, _text });
const editor = findEditor(docUri);
const document = editor?.document;
const selection = editor?.selection;
const range = (rangeLike && toRange(rangeLike)) || (selection && document?.getWordRangeAtPosition(selection.active));
const diags = document ? getCSpellDiags(document.uri) : undefined;
const matchingRanges = extractMatchingDiagRanges(document, selection, diags);
const r = matchingRanges?.[0] || range;
const matchingDiags = r && diags?.filter((d) => !!d.range.intersection(r));

if (!document || !selection || !r || !matchingDiags) {
return pVoid(window.showInformationMessage('Nothing to suggest.'), 'actionSuggestSpellingCorrections');
}

const menu = getSettingFromVSConfig(ConfigFields.suggestionMenuType, document);
if (menu === 'quickFix') {
return await commands.executeCommand('editor.action.quickFix');
}

const actions = await requestSpellingSuggestions(document, r, matchingDiags);
if (!actions || !actions.length) {
return pVoid(window.showInformationMessage(`No Suggestions Found for ${document.getText(r)}`), 'actionSuggestSpellingCorrections');
}

const items: SuggestionQuickPickItem[] = actions.map((a) => ({ label: a.title, _action: a }));
const picked = await window.showQuickPick(items);
if (picked && picked._action.command) {
const { command: cmd, arguments: args = [] } = picked._action.command;
commands.executeCommand(cmd, ...args);
}
}

export function requestSpellingSuggestions(document: TextDocument, range: Range, diags: Diagnostic[]) {
return di.get('client').requestSpellingSuggestions(document, range, diags);
}
87 changes: 6 additions & 81 deletions packages/client/src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,17 @@
import type {
CodeAction,
Command,
ConfigurationScope,
Diagnostic,
Disposable,
Location,
QuickPickItem,
QuickPickOptions,
TextDocument,
TextEdit,
TextEditor,
Uri,
} from 'vscode';
import type { Command, ConfigurationScope, Diagnostic, Disposable, Location, QuickPickOptions, TextDocument, TextEdit, Uri } from 'vscode';
import { commands, FileType, Position, Range, Selection, TextEditorRevealType, window, workspace, WorkspaceEdit } from 'vscode';
import type { Position as LsPosition, Range as LsRange, TextEdit as LsTextEdit } from 'vscode-languageclient/node';

import type { ClientSideCommandHandlerApi, SpellCheckerSettingsProperties } from './client';
import { actionSuggestSpellingCorrections } from './codeActions/actionSuggestSpellingCorrections';
import * as di from './di';
import { extractMatchingDiagRanges, extractMatchingDiagTexts, getCSpellDiags } from './diags';
import { extractMatchingDiagTexts, getCSpellDiags } from './diags';
import { toRegExp } from './extensionRegEx/evaluateRegExp';
import { toRange } from './languageServer/clientHelpers';
import type { RangeLike } from './languageServer/models';
import type { ConfigTargetLegacy, TargetsAndScopes } from './settings';
import * as Settings from './settings';
import {
ConfigFields,
ConfigurationTarget,
createConfigFileRelativeToDocumentUri,
getSettingFromVSConfig,
normalizeTarget,
setEnableSpellChecking,
toggleEnableSpellChecker,
Expand Down Expand Up @@ -64,9 +48,10 @@ import {
configurationTargetToDictionaryScope,
dictionaryScopeToConfigurationTarget,
} from './settings/targetAndScope';
import type { OnErrorResolver } from './util/errors';
import { catchErrors, handleErrors, ignoreError } from './util/errors';
import { catchErrors, handleErrors } from './util/errors';
import { findEditor } from './util/findEditor';
import { performance, toMilliseconds } from './util/perf';
import { pVoid } from './util/pVoid';
import { scrollToText } from './util/textEditor';
import { toUri } from './util/uriHelper';
import { findMatchingDocument } from './vscode/findDocument';
Expand Down Expand Up @@ -164,15 +149,6 @@ const commandHandlers: CommandHandler = {
'cSpell.openFileAtLine': openFileAtLine,
};

function pVoid<T>(p: Promise<T> | Thenable<T>, context: string, onErrorHandler: OnErrorResolver = ignoreError): Promise<void> {
const v = Promise.resolve(p).then(() => undefined);
return handleErrors(v, context, onErrorHandler);
}

// function notImplemented(cmd: string) {
// return () => pVoid(window.showErrorMessage(`Not yet implemented "${cmd}"`));
// }

const propertyFixSpellingWithRenameProvider: SpellCheckerSettingsProperties = 'fixSpellingWithRenameProvider';
const propertyUseReferenceProviderWithRename: SpellCheckerSettingsProperties = 'advanced.feature.useReferenceProviderWithRename';
const propertyUseReferenceProviderRemove: SpellCheckerSettingsProperties = 'advanced.feature.useReferenceProviderRemove';
Expand Down Expand Up @@ -576,43 +552,6 @@ function ctx(method: string, target: ConfigurationTarget | undefined, uri: Uri |
return scope ? `${method} ${scope} ${toUri(uri)}` : `${method} ${toUri(uri)}`;
}

interface SuggestionQuickPickItem extends QuickPickItem {
_action: CodeAction;
}

async function actionSuggestSpellingCorrections(docUri?: Uri, rangeLike?: RangeLike, _text?: string): Promise<void> {
// console.log('Args: %o', { docUri, range: rangeLike, _text });
const editor = findEditor(docUri);
const document = editor?.document;
const selection = editor?.selection;
const range = (rangeLike && toRange(rangeLike)) || (selection && document?.getWordRangeAtPosition(selection.active));
const diags = document ? getCSpellDiags(document.uri) : undefined;
const matchingRanges = extractMatchingDiagRanges(document, selection, diags);
const r = matchingRanges?.[0] || range;
const matchingDiags = r && diags?.filter((d) => !!d.range.intersection(r));

if (!document || !selection || !r || !matchingDiags) {
return pVoid(window.showInformationMessage('Nothing to suggest.'), 'actionSuggestSpellingCorrections');
}

const menu = getSettingFromVSConfig(ConfigFields.suggestionMenuType, document);
if (menu === 'quickFix') {
return await commands.executeCommand('editor.action.quickFix');
}

const actions = await di.get('client').requestSpellingSuggestions(document, r, matchingDiags);
if (!actions || !actions.length) {
return pVoid(window.showInformationMessage(`No Suggestions Found for ${document.getText(r)}`), 'actionSuggestSpellingCorrections');
}

const items: SuggestionQuickPickItem[] = actions.map((a) => ({ label: a.title, _action: a }));
const picked = await window.showQuickPick(items);
if (picked && picked._action.command) {
const { command: cmd, arguments: args = [] } = picked._action.command;
commands.executeCommand(cmd, ...args);
}
}

async function createCustomDictionary(): Promise<void> {
const targets = await targetsForTextDocument(window.activeTextEditor?.document);

Expand Down Expand Up @@ -710,20 +649,6 @@ function toConfigToRegExp(regExStr: string | undefined, flags = 'g'): RegExp | u
return undefined;
}

function findEditor(uri?: Uri): TextEditor | undefined {
if (!uri) return window.activeTextEditor;

const uriStr = uri.toString();

for (const editor of window.visibleTextEditors) {
if (editor.document.uri.toString() === uriStr) {
return editor;
}
}

return undefined;
}

export function createTextEditCommand(
title: string,
uri: string | Uri,
Expand Down
6 changes: 5 additions & 1 deletion packages/client/src/diags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ import { isDefined, uniqueFilter } from './util';
export function getCSpellDiags(docUri: Uri | undefined): Diagnostic[] {
const issueTracker = getDependencies().issueTracker;
const diags = (docUri && issueTracker.getDiagnostics(docUri)) || [];
const cSpellDiags = diags.filter((d) => d.source === diagnosticSource);
const cSpellDiags = filterDiags(diags);
return cSpellDiags;
}

export function filterDiags(diags: readonly Diagnostic[], source = diagnosticSource): Diagnostic[] {
return diags.filter((d) => d.source === source);
}

export function extractMatchingDiagText(
doc: TextDocument | undefined,
selection: Selection | undefined,
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Utils as UriUtils } from 'vscode-uri';

import { registerCspellInlineCompletionProviders } from './autocomplete';
import { CSpellClient } from './client';
import { registerSpellCheckerCodeActionProvider } from './codeAction';
import * as commands from './commands';
import { updateDocumentRelatedContext } from './context';
import { SpellingIssueDecorator } from './decorate';
Expand Down Expand Up @@ -84,6 +85,7 @@ export async function activate(context: ExtensionContext): Promise<ExtensionApi>
vscode.window.onDidChangeVisibleTextEditors(handleOnDidChangeVisibleTextEditors),
vscode.languages.onDidChangeDiagnostics(handleOnDidChangeDiagnostics),
decorator,
registerSpellCheckerCodeActionProvider(issueTracker),

...commands.registerCommands(),

Expand Down
16 changes: 16 additions & 0 deletions packages/client/src/util/findEditor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { TextEditor, Uri } from 'vscode';
import { window } from 'vscode';

export function findEditor(uri?: Uri): TextEditor | undefined {
if (!uri) return window.activeTextEditor;

const uriStr = uri.toString();

for (const editor of window.visibleTextEditors) {
if (editor.document.uri.toString() === uriStr) {
return editor;
}
}

return undefined;
}
7 changes: 7 additions & 0 deletions packages/client/src/util/pVoid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { OnErrorResolver } from './errors';
import { handleErrors, ignoreError } from './errors';

export function pVoid<T>(p: Promise<T> | Thenable<T>, context: string, onErrorHandler: OnErrorResolver = ignoreError): Promise<void> {
const v = Promise.resolve(p).then(() => undefined);
return handleErrors(v, context, onErrorHandler);
}

0 comments on commit 4e93033

Please sign in to comment.