Skip to content

Commit

Permalink
fix: Add suggestions to hover (#2896)
Browse files Browse the repository at this point in the history
Jason3S authored Oct 24, 2023

Verified

This commit was signed with the committer’s verified signature.
JounQin JounQin
1 parent 7ef1332 commit cf88324
Showing 13 changed files with 117 additions and 37 deletions.
2 changes: 1 addition & 1 deletion packages/_server/src/SuggestionsGenerator.mts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CompoundWordsMethod, type SpellingDictionaryCollection, type SuggestionResult, type SuggestOptions } from 'cspell-lib';

import type { Suggestion } from './api.js';
import type { CSpellUserSettings } from './config/cspellConfig/index.mjs';
import type { Suggestion } from './models/Suggestion.mjs';

const defaultNumSuggestions = 10;

2 changes: 1 addition & 1 deletion packages/_server/src/api/apiModels.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import type { PublishDiagnosticsParams } from 'vscode-languageserver';

import type { ConfigScopeVScode, ConfigTarget } from '../config/configTargets.mjs';
import type * as config from '../config/cspellConfig/index.mjs';
import type { Suggestion } from '../models/Suggestion.mjs';
import type { Suggestion } from './models/Suggestion.mjs';

export type {
ConfigKind,
2 changes: 2 additions & 0 deletions packages/_server/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -2,3 +2,5 @@ export * from './api.js';
// export * from './api.old.js';
export * from './apiModels.js';
export * from './CommandsToClient.js';
export { SpellCheckerDiagnosticData } from './models/DiagnosticData.mjs';
export { Suggestion } from './models/Suggestion.mjs';
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import type { IssueType } from 'cspell-lib';

import type { Suggestion } from './Suggestion.mjs';

export interface DiagnosticData {
export interface SpellCheckerDiagnosticData {
issueType?: IssueType | undefined;
suggestions?: Suggestion[] | undefined;
}
File renamed without changes.
8 changes: 3 additions & 5 deletions packages/_server/src/codeActions.mts
Original file line number Diff line number Diff line change
@@ -9,15 +9,13 @@ import type { TextDocument } from 'vscode-languageserver-textdocument';
import type { Diagnostic } from 'vscode-languageserver-types';
import { CodeAction, CodeActionKind, TextEdit } from 'vscode-languageserver-types';

import type { UriString, WorkspaceConfigForDocument } from './api.js';
import type { SpellCheckerDiagnosticData, Suggestion, UriString, WorkspaceConfigForDocument } from './api.js';
import { clientCommands as cc } from './commands.mjs';
import type { ConfigScope, ConfigTarget, ConfigTargetCSpell, ConfigTargetDictionary, ConfigTargetVSCode } from './config/configTargets.mjs';
import { ConfigKinds, ConfigScopes } from './config/configTargets.mjs';
import { calculateConfigTargets } from './config/configTargetsHelper.mjs';
import type { CSpellUserSettings } from './config/cspellConfig/index.mjs';
import { isUriAllowed } from './config/documentSettings.mjs';
import type { DiagnosticData } from './models/DiagnosticData.mjs';
import type { Suggestion } from './models/Suggestion.mjs';
import type { GetSettingsResult } from './SuggestionsGenerator.mjs';
import { SuggestionGenerator } from './SuggestionsGenerator.mjs';
import { uniqueFilter } from './utils/index.mjs';
@@ -32,10 +30,10 @@ function extractText(textDocument: TextDocument, range: LangServerRange) {

const debugTargets = false;

function extractDiagnosticData(diag: Diagnostic): DiagnosticData {
function extractDiagnosticData(diag: Diagnostic): SpellCheckerDiagnosticData {
const { data } = diag;
if (!data || typeof data !== 'object' || Array.isArray(data)) return {};
return data as DiagnosticData;
return data as SpellCheckerDiagnosticData;
}

export interface CodeActionHandlerDependencies {
41 changes: 28 additions & 13 deletions packages/_server/src/server.mts
Original file line number Diff line number Diff line change
@@ -18,7 +18,14 @@ import type {
PublishDiagnosticsParams,
ServerCapabilities,
} from 'vscode-languageserver/node.js';
import { CodeActionKind, createConnection, ProposedFeatures, TextDocuments, TextDocumentSyncKind } from 'vscode-languageserver/node.js';
import {
CodeActionKind,
createConnection,
DiagnosticSeverity,
ProposedFeatures,
TextDocuments,
TextDocumentSyncKind,
} from 'vscode-languageserver/node.js';
import { TextDocument } from 'vscode-languageserver-textdocument';

import type * as Api from './api.js';
@@ -36,6 +43,7 @@ import {
isUriBlocked,
stringifyPatterns,
} from './config/documentSettings.mjs';
import { isScmUri } from './config/docUriHelper.mjs';
import type { TextDocumentUri } from './config/vscode.config.mjs';
import { createProgressNotifier } from './progressNotifier.mjs';
import { createServerApi } from './serverApi.mjs';
@@ -411,13 +419,16 @@ export function run(): void {

function sendDiagnostics(result: ValidationResult) {
log(`Send Diagnostics v${result.version}`, result.uri);
const diags: Required<PublishDiagnosticsParams> = {
uri: result.uri,
version: result.version,
diagnostics: result.diagnostics,
};

const { uri, version, diagnostics } = result;

const diags: Required<PublishDiagnosticsParams> = { uri, version, diagnostics };

const diagsForVSCode = result.hideHints
? { ...diags, diagnostics: diags.diagnostics.filter((d) => d.severity !== DiagnosticSeverity.Hint) }
: diags;
catchPromise(clientServerApi.clientNotification.onDiagnostics(diags));
catchPromise(connection.sendDiagnostics(diags), 'sendDiagnostics');
catchPromise(connection.sendDiagnostics(diagsForVSCode), 'sendDiagnostics');
}

async function shouldValidateDocument(textDocument: TextDocument, settings: CSpellUserSettings): Promise<boolean> {
@@ -522,27 +533,30 @@ export function run(): void {
const { doc, settings } = dsp;
const { uri, version } = doc;

const hideHints = !isScmUri(uri) && !!settings.decorateIssues;
const result: ValidationResult = { uri, version, hideHints, diagnostics: [] };

try {
if (!isUriAllowed(uri, settings.allowedSchemas)) {
const schema = uri.split(':')[0];
log(`Schema not allowed (${schema}), skipping:`, uri);
return { uri, version, diagnostics: [] };
return result;
}
if (isStale(doc)) {
return { uri, version, diagnostics: [] };
return result;
}
const shouldCheck = await shouldValidateDocument(doc, settings);
if (!shouldCheck) {
log('validateTextDocument skip:', uri);
return { uri, version, diagnostics: [] };
return result;
}
log(`getSettingsToUseForDocument start ${doc.version}`, uri);
const settingsToUse = await getSettingsToUseForDocument(doc);
log(`getSettingsToUseForDocument middle ${doc.version}`, uri);
configWatcher.processSettings(settingsToUse);
log(`getSettingsToUseForDocument done ${doc.version}`, uri);
if (isStale(doc)) {
return { uri, version, diagnostics: [] };
return result;
}
if (settingsToUse.enabled) {
logInfo(`Validate File: v${doc.version}`, uri);
@@ -552,12 +566,12 @@ export function run(): void {
dictionaryWatcher.processSettings(settings);
const diagnostics: Diagnostic[] = await Validator.validateTextDocument(doc, settings);
log(`validateTextDocument done: v${doc.version}`, uri);
return { uri, version, diagnostics };
return { ...result, diagnostics };
}
} catch (e) {
logError(`validateTextDocument: ${JSON.stringify(e)}`);
}
return { uri, version, diagnostics: [] };
return result;
}

isValidationBusy = true;
@@ -674,6 +688,7 @@ interface TextDocumentInfo {

interface ValidationResult extends PublishDiagnosticsParams {
version: number;
hideHints: boolean;
}

interface OnChangeParam extends DidChangeConfigurationParams {
4 changes: 2 additions & 2 deletions packages/_server/src/validator.mts
Original file line number Diff line number Diff line change
@@ -3,10 +3,10 @@ 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 { CSpellUserSettings } from './config/cspellConfig/index.mjs';
import { isScmUri } from './config/docUriHelper.mjs';
import { diagnosticSource } from './constants.mjs';
import type { DiagnosticData } from './models/DiagnosticData.mjs';

export { createTextDocument, validateText } from 'cspell-lib';

@@ -52,7 +52,7 @@ 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: DiagnosticData = { issueType, suggestions: sugs };
const data: SpellCheckerDiagnosticData = { issueType, suggestions: sugs };
return { severity, range, message: diagMessage, source: diagSource, data };
})
.filter((diag) => !!diag.severity);
1 change: 1 addition & 0 deletions packages/client/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ export type {
GetConfigurationForDocumentResult,
NamedPattern,
PatternMatch,
SpellCheckerDiagnosticData,
SpellCheckerSettingsProperties,
} from './server';
export { normalizeLocale } from './server';
1 change: 1 addition & 0 deletions packages/client/src/client/server/server.ts
Original file line number Diff line number Diff line change
@@ -25,6 +25,7 @@ export type {
NamedPattern,
OnSpellCheckDocumentStep,
PatternMatch,
SpellCheckerDiagnosticData,
SpellCheckerSettingsProperties,
SplitTextIntoWordsResult,
WorkspaceConfigForDocumentRequest,
51 changes: 50 additions & 1 deletion packages/client/src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
CodeAction,
Command,
ConfigurationScope,
Diagnostic,
Disposable,
@@ -12,7 +13,7 @@ import type {
Uri,
} from 'vscode';
import { commands, FileType, Position, Range, Selection, TextEditorRevealType, window, workspace, WorkspaceEdit } from 'vscode';
import type { TextEdit as LsTextEdit } from 'vscode-languageclient/node';
import type { Position as LsPosition, Range as LsRange, TextEdit as LsTextEdit } from 'vscode-languageclient/node';

import type { ClientSideCommandHandlerApi, SpellCheckerSettingsProperties } from './client';
import * as di from './di';
@@ -180,6 +181,8 @@ function handlerApplyTextEdits() {
return async function handleApplyTextEdits(uri: string, documentVersion: number, edits: LsTextEdit[]): Promise<void> {
const client = di.get('client').client;

console.warn('handleApplyTextEdits %o', { uri, documentVersion, edits });

const doc = workspace.textDocuments.find((doc) => doc.uri.toString() === uri);

if (!doc) return;
@@ -720,3 +723,49 @@ function findEditor(uri?: Uri): TextEditor | undefined {

return undefined;
}

export function createTextEditCommand(
title: string,
uri: string | Uri,
documentVersion: number,
edits: LsTextEdit[] | TextEdit[],
): Command {
const normalizedEdits: LsTextEdit[] = edits.map(toLsTextEdit);
return {
title,
command: 'cSpell.editText',
arguments: [uri.toString(), documentVersion, normalizedEdits],
};
}

/**
* Create a href URL that will execute a command.
*/
export function commandUri(command: Command): string;
export function commandUri(command: string, ...params: unknown[]): string;
export function commandUri(command: string | Command, ...params: unknown[]): string {
if (typeof command === 'string') {
return `command:${command}?${encodeURIComponent(JSON.stringify(params))}`;
}
return `command:${command.command}?${command.arguments ? encodeURIComponent(JSON.stringify(command.arguments)) : ''}`;
}

function toLsPosition(p: LsPosition | Position): LsPosition {
const { line, character } = p;
return { line, character };
}

function toLsRange(range: LsRange | Range): LsRange {
return {
start: toLsPosition(range.start),
end: toLsPosition(range.end),
};
}

function toLsTextEdit(edit: LsTextEdit | TextEdit): LsTextEdit {
const { range, newText } = edit;
return {
range: toLsRange(range),
newText,
};
}
26 changes: 18 additions & 8 deletions packages/client/src/decorate.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createDisposableList } from 'utils-disposables';
import type { DecorationOptions, Diagnostic, DiagnosticChangeEvent, TextDocument, TextEditor, TextEditorDecorationType, Uri } from 'vscode';
import type { DecorationOptions, DiagnosticChangeEvent, TextDocument, TextEditor, TextEditorDecorationType, Uri } from 'vscode';
import vscode, { ColorThemeKind, DiagnosticSeverity, MarkdownString } from 'vscode';

import type { CSpellUserSettings } from './client';
import { commandUri, createTextEditCommand } from './commands';
import type { Disposable } from './disposable';
import type { IssueTracker } from './issueTracker';
import type { IssueTracker, SpellingDiagnostic } from './issueTracker';

export class SpellingIssueDecorator implements Disposable {
private decorationType: TextEditorDecorationType | undefined;
@@ -82,24 +83,33 @@ export class SpellingIssueDecorator implements Disposable {
}
}

function diagToDecorationOptions(diag: Diagnostic, doc: TextDocument): DecorationOptions {
function diagToDecorationOptions(diag: SpellingDiagnostic, doc: TextDocument): DecorationOptions {
const { range } = diag;
const suggestions = diag.data?.suggestions;
const text = doc.getText(range);

const commandSuggest = commandUri('cSpell.suggestSpellingCorrections', doc.uri, range, text);
const commandAdd = commandUri('cSpell.addWordToDictionary', text);
const hoverMessage = new MarkdownString(diag.message)
const hoverMessage = new MarkdownString(diag.message);

hoverMessage
.appendText(' ')
.appendMarkdown(markdownLink('Suggest', commandSuggest, 'Show suggestions.'))
.appendText(', ')
.appendMarkdown(markdownLink('Add', commandAdd, 'Add word to dictionary.'));

if (suggestions?.length) {
for (const suggestion of suggestions) {
const { word } = suggestion;
const cmd = createTextEditCommand('fix', doc.uri, doc.version, [{ range, newText: word }]);
hoverMessage.appendMarkdown('\n- ' + markdownLink(word, commandUri(cmd), `Fix with ${word}`) + '\n');
}
}

hoverMessage.isTrusted = true;
return { range, hoverMessage };
}

function commandUri(command: string, ...params: unknown[]): string {
return `command:${command}?${encodeURIComponent(JSON.stringify(params))}`;
}

function markdownLink(text: string, uri: string, hover?: string) {
const hoverText = hover ? ` "${hover}"` : '';
return `[${text}](${uri}${hoverText})`;
14 changes: 9 additions & 5 deletions packages/client/src/issueTracker.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { createDisposableList } from 'utils-disposables';
import type { Diagnostic, DiagnosticChangeEvent, TextDocument, Uri } from 'vscode';
import { workspace } from 'vscode';

import type { CSpellClient, DiagnosticsFromServer } from './client';
import type { CSpellClient, DiagnosticsFromServer, SpellCheckerDiagnosticData } from './client';
import { createEmitter } from './Subscribables';

type UriString = string;
@@ -17,10 +17,10 @@ export class IssueTracker {
this.disposables.push(workspace.onDidCloseTextDocument((doc) => this.handleDocClose(doc)));
}

public getDiagnostics(uri: Uri): Diagnostic[];
public getDiagnostics(): [Uri, Diagnostic[]][];
public getDiagnostics(uri?: Uri): Diagnostic[] | [Uri, Diagnostic[]][] {
if (!uri) return [...this.issues.values()].map((d) => [d.uri, d.diagnostics] as [Uri, Diagnostic[]]);
public getDiagnostics(uri: Uri): SpellingDiagnostic[];
public getDiagnostics(): [Uri, SpellingDiagnostic[]][];
public getDiagnostics(uri?: Uri): SpellingDiagnostic[] | [Uri, SpellingDiagnostic[]][] {
if (!uri) return [...this.issues.values()].map((d) => [d.uri, d.diagnostics] as [Uri, SpellingDiagnostic[]]);
return this.issues.get(uri.toString())?.diagnostics || [];
}

@@ -44,3 +44,7 @@ export class IssueTracker {
this.issues.delete(uri);
}
}

export interface SpellingDiagnostic extends Diagnostic {
data?: SpellCheckerDiagnosticData;
}

0 comments on commit cf88324

Please sign in to comment.