Skip to content

Commit

Permalink
feat: Support Word Tracing (#2987)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S authored Dec 29, 2023
1 parent a8a1e18 commit 001645b
Show file tree
Hide file tree
Showing 27 changed files with 820 additions and 399 deletions.
1 change: 1 addition & 0 deletions docs/_includes/generated-docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@
| `cSpell.toggleEnableForGlobal` | Toggle Spell Checking in User Settings |
| `cSpell.toggleEnableForWorkspace` | Toggle Spell Checking for Workspace |
| `cSpell.toggleEnableSpellChecker` | Toggle Spell Checking |
| `cSpell.toggleTraceMode` | Toggle Trace Mode |
241 changes: 121 additions & 120 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,12 @@
"category": "Spell",
"title": "Insert Words Directive",
"icon": "$(comment-discussion)"
},
{
"command": "cSpell.toggleTraceMode",
"category": "Spell",
"title": "Toggle Trace Mode",
"icon": "$(search)"
}
],
"languages": [
Expand Down Expand Up @@ -3386,10 +3392,10 @@
"vitest": "^1.1.0"
},
"dependencies": {
"@cspell/cspell-bundled-dicts": "^8.2.3",
"@cspell/cspell-types": "^8.2.3",
"@cspell/cspell-bundled-dicts": "^8.2.4",
"@cspell/cspell-types": "^8.2.4",
"@types/react": "^17.0.73",
"cspell": "^8.2.3",
"cspell": "^8.2.4",
"regexp-worker": "^2.0.2"
},
"comment-resolutions": {
Expand Down
4 changes: 2 additions & 2 deletions packages/__cspell-helper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"yargs": "^17.7.2"
},
"dependencies": {
"@cspell/cspell-types": "^8.2.3",
"cspell-lib": "^8.2.3"
"@cspell/cspell-types": "^8.2.4",
"cspell-lib": "^8.2.4"
},
"engines": {
"node": ">18.0.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/_integrationTests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@cspell/cspell-types": "^8.2.3",
"@cspell/cspell-types": "^8.2.4",
"@types/chai": "^4.3.11",
"@types/decompress": "^4.2.7",
"@types/glob": "^8.1.0",
Expand Down
14 changes: 7 additions & 7 deletions packages/_server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@
"yargs": "^17.7.2"
},
"dependencies": {
"@cspell/cspell-bundled-dicts": "^8.2.3",
"@cspell/cspell-pipe": "^8.2.3",
"@cspell/cspell-types": "^8.2.3",
"@cspell/cspell-bundled-dicts": "^8.2.4",
"@cspell/cspell-pipe": "^8.2.4",
"@cspell/cspell-types": "^8.2.4",
"@internal/common-utils": "file:../__utils",
"cspell-config-lib": "^8.2.3",
"cspell-gitignore": "^8.2.3",
"cspell-glob": "^8.2.3",
"cspell-lib": "^8.2.3",
"cspell-config-lib": "^8.2.4",
"cspell-gitignore": "^8.2.4",
"cspell-glob": "^8.2.4",
"cspell-lib": "^8.2.4",
"gensequence": "^6.0.0",
"json-rpc-api": "file:../json-rpc-api",
"rxjs": "^7.8.1",
Expand Down
98 changes: 98 additions & 0 deletions packages/_server/src/DocumentValidationController.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { createTextDocument, DocumentValidator } from 'cspell-lib';
import { DisposableList } from 'utils-disposables';
import type { TextDocumentChangeEvent, TextDocuments } from 'vscode-languageserver';
import type { TextDocument } from 'vscode-languageserver-textdocument';

import type { TextDocumentInfoWithText, TextDocumentRef } from './api.js';
import type { CSpellUserSettings } from './config/cspellConfig/index.mjs';
import type { DocumentSettings } from './config/documentSettings.mjs';
import { defaultCheckLimit } from './constants.mjs';

interface DocValEntry {
uri: string;
settings: Promise<CSpellUserSettings>;
docVal: Promise<DocumentValidator>;
}

export class DocumentValidationController {
private docValMap = new Map<string, DocValEntry>();
private disposables = new DisposableList();

constructor(
readonly documentSettings: DocumentSettings,
readonly documents: TextDocuments<TextDocument>,
) {
this.disposables.push(
documents.onDidClose((e) => this.handleOnDidClose(e)),
documents.onDidChangeContent((e) => this.handleOnDidChangeContent(e)),
);
}

get(doc: TextDocumentRef) {
return this.docValMap.get(doc.uri);
}

getDocumentValidator(docInfo: TextDocumentInfoWithText | TextDocument) {
const uri = docInfo.uri;
const docValEntry = this.docValMap.get(uri);
if (docValEntry) return docValEntry.docVal;

const entry = this.createDocValEntry(docInfo);
this.docValMap.set(uri, entry);
return entry.docVal;
}

private createDocValEntry(docInfo: TextDocumentInfoWithText | TextDocument) {
const uri = docInfo.uri;
const settings = this.documentSettings.getSettings(docInfo);
const docVal = createDocumentValidator(docInfo, settings);
const entry: DocValEntry = { uri, settings, docVal };
return entry;
}

clear() {
this.docValMap.clear();
}

dispose() {
this.clear();
this.disposables.dispose();
}

private handleOnDidClose(e: TextDocumentChangeEvent<TextDocument>) {
this.docValMap.delete(e.document.uri);
}

private handleOnDidChangeContent(e: TextDocumentChangeEvent<TextDocument>) {
this._handleOnDidChangeContent(e).catch(() => undefined);
}

private async _handleOnDidChangeContent(e: TextDocumentChangeEvent<TextDocument>) {
const { document } = e;
const entry = this.docValMap.get(document.uri);
if (!entry) return;
const { settings, docVal } = entry;
const updatedSettings = await this.documentSettings.getSettings(document);
const [_settings, _docVal, _curSettings] = await Promise.all([settings, docVal, updatedSettings] as const);
if (_settings !== _curSettings) {
this.docValMap.set(document.uri, this.createDocValEntry(document));
return;
}
await _docVal.updateDocumentText(document.getText());
}
}

export async function createDocumentValidator(
textDocument: TextDocument | TextDocumentInfoWithText,
pSettings: Promise<CSpellUserSettings> | CSpellUserSettings,
): Promise<DocumentValidator> {
const settings = await pSettings;
const limit = (settings.checkLimit || defaultCheckLimit) * 1024;
const content = ('getText' in textDocument ? textDocument.getText() : textDocument.text).slice(0, limit);
const { uri, languageId, version } = textDocument;
const docInfo = { uri, content, languageId, version };
const doc = createTextDocument(docInfo);
const docVal = new DocumentValidator(doc, { noConfigSearch: true }, settings);
await docVal.prepare();
return docVal;
}
10 changes: 10 additions & 0 deletions packages/_server/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ import { createClientApi, createServerApi } from 'json-rpc-api';
import type {
GetConfigurationForDocumentRequest,
GetConfigurationForDocumentResult,
GetSpellCheckingOffsetsResult,
IsSpellCheckEnabledResult,
OnSpellCheckDocumentStep,
PublishDiagnostics,
SpellingSuggestionsResult,
SplitTextIntoWordsResult,
TextDocumentInfo,
TextDocumentRef,
TraceWordRequest,
TraceWordResult,
WorkspaceConfigForDocumentRequest,
WorkspaceConfigForDocumentResponse,
} from './apiModels.js';
Expand All @@ -34,6 +38,12 @@ export interface ServerRequestsAPI {
isSpellCheckEnabled(req: TextDocumentInfo): IsSpellCheckEnabledResult;
splitTextIntoWords(req: string): SplitTextIntoWordsResult;
spellingSuggestions(word: string, doc?: TextDocumentInfo): SpellingSuggestionsResult;
/**
* Calculate the text ranges that should be spell checked.
* @param doc The document to be spell checked.
*/
getSpellCheckingOffsets(doc: TextDocumentRef): GetSpellCheckingOffsetsResult;
traceWord(req: TraceWordRequest): TraceWordResult;
}

/** Notifications that can be sent to the server */
Expand Down
28 changes: 28 additions & 0 deletions packages/_server/src/api/apiModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ export interface SpellingSuggestionsResult {
suggestions: Suggestion[];
}

export interface GetSpellCheckingOffsetsResult {
/**
* The text offsets of the text in the document that should be spell checked.
* The offsets are start/end pairs.
*/
offsets: number[];
}

export interface GetConfigurationForDocumentRequest extends Partial<TextDocumentInfo> {
/** used to calculate configTargets, configTargets will be empty if undefined. */
workspaceConfig?: WorkspaceConfigForDocument;
Expand Down Expand Up @@ -205,3 +213,23 @@ export type VSCodeSettingsCspell = {
};

export type PublishDiagnostics = Required<PublishDiagnosticsParams>;

export interface TraceWordRequest {
uri: DocumentUri;
word: string;
}
export interface WordTrace {
word: string;
found: boolean;
foundWord: string | undefined;
forbidden: boolean;
noSuggest: boolean;
dictName: string;
dictSource: string;
errors: string | undefined;
}

export interface TraceWordResult {
traces?: WordTrace[] | undefined;
errors?: string | undefined;
}
9 changes: 3 additions & 6 deletions packages/_server/src/config/documentSettings.mts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { handleSpecialUri } from './docUriHelper.mjs';
import type { TextDocumentUri } from './vscode.config.mjs';
import { getConfiguration, getWorkspaceFolders } from './vscode.config.mjs';
import { createWorkspaceNamesResolver, resolveSettings } from './WorkspacePathResolver.mjs';
import { findMatchingFoldersForUri } from '../utils/matchingFoldersForUri.mjs';

// The settings interface describe the server relevant settings part
export type SettingsCspell = VSCodeSettingsCspell;
Expand Down Expand Up @@ -474,7 +475,7 @@ export class DocumentSettings {

public async matchingFoldersForUri(docUri: string): Promise<WorkspaceFolder[]> {
const folders = await this.folders;
return _matchingFoldersForUri(folders, docUri);
return findMatchingFoldersForUri(folders, docUri);
}

private createCache<K, T>(loader: (key: K) => T): AutoLoadCache<K, T> {
Expand Down Expand Up @@ -554,10 +555,6 @@ export function isLanguageEnabled(languageId: string, settings: CSpellUserSettin
return checkOnly && starEnabled !== true ? !!enabled : enabled !== false;
}

function _matchingFoldersForUri(folders: WorkspaceFolder[], docUri: string): WorkspaceFolder[] {
return folders.filter(({ uri }) => docUri.startsWith(uri)).sort((a, b) => b.uri.length - a.uri.length);
}

function _bestMatchingFolderForUri(folders: WorkspaceFolder[], docUri: string | undefined, defaultFolder: WorkspaceFolder): WorkspaceFolder;
function _bestMatchingFolderForUri(
folders: WorkspaceFolder[],
Expand All @@ -570,7 +567,7 @@ function _bestMatchingFolderForUri(
defaultFolder?: WorkspaceFolder,
): WorkspaceFolder | undefined {
if (!docUri) return defaultFolder;
const matches = _matchingFoldersForUri(folders, docUri);
const matches = findMatchingFoldersForUri(folders, docUri);
return matches[0] || defaultFolder;
}

Expand Down
1 change: 1 addition & 0 deletions packages/_server/src/constants.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ import type { DiagnosticSource, ExtensionId } from './api.js';

export const extensionId: ExtensionId = 'cSpell';
export const diagnosticSource: DiagnosticSource = extensionId;
export const defaultCheckLimit = 500;
29 changes: 2 additions & 27 deletions packages/_server/src/progressNotifier.test.mts
Original file line number Diff line number Diff line change
@@ -1,43 +1,18 @@
import type { ExcludeDisposableHybrid } from 'utils-disposables';
import { injectDisposable } from 'utils-disposables';
import { describe, expect, test, vi } from 'vitest';
import { TextDocument } from 'vscode-languageserver-textdocument';

import type { MessageConnection, ServerSideApi } from './api.js';
import { createProgressNotifier } from './progressNotifier.mjs';
import { createServerApi } from './serverApi.mjs';
import { createMockServerSideApi } from './test/test.api.js';

vi.mock('./serverApi');

const mockedCreateClientApi = vi.mocked(createServerApi);
// const mockedCreateConnection = jest.mocked(createConnection);

mockedCreateClientApi.mockImplementation(() => {
const mock: ServerSideApi = injectDisposable<ExcludeDisposableHybrid<ServerSideApi>>(
{
clientRequest: {
onWorkspaceConfigForDocumentRequest: vi.fn(),
vfsReadDirectory: vi.fn(() => Promise.resolve([])),
vfsReadFile: vi.fn(() => Promise.resolve({ uri: '', content: '' })),
vfsStat: vi.fn(() => Promise.resolve({ type: 0, size: 0, mtime: 0 })),
},
clientNotification: {
onSpellCheckDocument: vi.fn(),
onDiagnostics: vi.fn(),
},
serverRequest: {
getConfigurationForDocument: { subscribe: vi.fn() },
isSpellCheckEnabled: { subscribe: vi.fn() },
splitTextIntoWords: { subscribe: vi.fn() },
spellingSuggestions: { subscribe: vi.fn() },
},
serverNotification: {
notifyConfigChange: { subscribe: vi.fn() },
registerConfigurationFile: { subscribe: vi.fn() },
},
},
() => undefined,
);
const mock: ServerSideApi = createMockServerSideApi();
return mock;
});

Expand Down
Loading

0 comments on commit 001645b

Please sign in to comment.