From e303d1efcd50e9180626afabce7297af595884ec Mon Sep 17 00:00:00 2001 From: Jason Dent Date: Tue, 3 Dec 2024 16:53:15 -0700 Subject: [PATCH] fix: Improve error messages when a file is blocked. (#3860) --- .../workspaces/single/.vscode/settings.json | 3 + fixtures/workspaces/single/REPORT.md | 12 +++ package.json | 4 +- .../_server/spell-checker-config.schema.json | 8 +- packages/_server/src/api/api.ts | 8 ++ packages/_server/src/api/apiModels.ts | 15 +++- .../SpellCheckerShouldCheckDocSettings.mts | 4 +- packages/_server/src/server.mts | 20 +++-- packages/_server/src/serverApi.mts | 1 + packages/_server/src/test/test.api.ts | 1 + packages/_server/src/utils/analysis.mts | 80 ++++++++++++------- packages/_server/src/utils/analysis.test.mts | 48 ++++++++--- packages/client/src/client/client.mts | 13 ++- packages/client/src/client/server/server.mts | 3 + packages/client/src/extension.mts | 34 +++++++- packages/client/src/vscode/tabs.mts | 6 ++ packages/webview-api/src/models/settings.ts | 3 +- .../webview-ui/src/views/CSpellInfo.svelte | 12 ++- 18 files changed, 216 insertions(+), 59 deletions(-) create mode 100644 fixtures/workspaces/single/.vscode/settings.json create mode 100644 fixtures/workspaces/single/REPORT.md diff --git a/fixtures/workspaces/single/.vscode/settings.json b/fixtures/workspaces/single/.vscode/settings.json new file mode 100644 index 0000000000..e2e77ef8fd --- /dev/null +++ b/fixtures/workspaces/single/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.blockCheckingWhenTextChunkSizeGreaterThan": 400 +} diff --git a/fixtures/workspaces/single/REPORT.md b/fixtures/workspaces/single/REPORT.md new file mode 100644 index 0000000000..5db946902f --- /dev/null +++ b/fixtures/workspaces/single/REPORT.md @@ -0,0 +1,12 @@ +# Weekly Report + +Image: [long url](https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/29e25571-eb24-4381-9a2d-bde0ba52be2e/df3uxma-90078aec-f043-423b-8adf-68b0db323607.png?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNhNWYwZDQxNWVhMGQyNmUwIiwiaXNzIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCIsIm9iaiI6W1t7InBhdGgiOiJcL2ZcLzI5ZTI1NTcxLWViMjQtNDM4MS05YTJkLWJkZTBiYTUyYmUyZVwvZGYzdXhtYS05MDA3OGFlYy1mMDQzLTQyM2ItOGFkZi02OGIwZGIzMjM2MDcucG5nIn1dXSwiYXVkIjpbInVybjpzZXJ2aWNlOmZpbGUuZG93bmxvYWQiXX0.Pap7EkIxDlgZ1dFLyEK_MOlPIQGjvJVm5T8adKtnAn0) + +See VS Code Setting: [cSpell.blockCheckingWhenTextChunkSizeGreaterThan](vscode://settings/cSpell.blockCheckingWhenTextChunkSizeGreaterThan) + +This file is expected to be checked. + +Possible very long word: +token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNhNWYwZDQxNWVhMGQyNmUwIiwiaXNzIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCIsIm9iaiI6W1t7InBhdGgiOiJcL2ZcLzI5ZTI1NTcxLWViMjQtNDM4MS05YTJkLWJkZTBiYTUyYmUyZVwvZGYzdXhtYS05MDA3OGFlYy1mMDQzLTQyM2ItOGFkZi02OGIwZGIzMjM2MDcucG5nIn1dXSwiYXVkIjpbInVybjpzZXJ2aWNlOmZpbGUuZG93bmxvYWQiXX0.Pap7EkIxDlgZ1dFLyEK_MOlPIQGjvJVm5T8adKtnAn0 + +tokken spellinng. diff --git a/package.json b/package.json index efdd3388dd..6c333e36e9 100644 --- a/package.json +++ b/package.json @@ -3704,7 +3704,7 @@ "properties": { "cSpell.blockCheckingWhenAverageChunkSizeGreaterThan": { "default": 80, - "markdownDescription": "The maximum average length of chunks of text without word breaks.\n\n\nA chunk is the characters between absolute word breaks.\nAbsolute word breaks match: `/[\\s,{}[\\]]/`\n\n\n**Error Message:** _Average Word Size is Too High._\n\n\nIf you are seeing this message, it means that the file contains mostly long lines\nwithout many word breaks.", + "markdownDescription": "The maximum average length of chunks of text without word breaks.\n\n\nA chunk is the characters between absolute word breaks.\nAbsolute word breaks match: `/[\\s,{}[\\]]/`\n\n\n**Error Message:** _Average word length is too long._\n\n\nIf you are seeing this message, it means that the file contains mostly long lines\nwithout many word breaks.", "scope": "language-overridable", "type": "number" }, @@ -3716,7 +3716,7 @@ }, "cSpell.blockCheckingWhenTextChunkSizeGreaterThan": { "default": 500, - "markdownDescription": "The maximum length of a chunk of text without word breaks.\n\n\nIt is used to prevent spell checking of generated files.\n\n\nA chunk is the characters between absolute word breaks.\nAbsolute word breaks match: `/[\\s,{}[\\]]/`, i.e. spaces or braces.\n\n\n**Error Message:** _Maximum Word Length is Too High._\n\n\nIf you are seeing this message, it means that the file contains a very long line\nwithout many word breaks.", + "markdownDescription": "The maximum length of a chunk of text without word breaks.\n\n\nIt is used to prevent spell checking of generated files.\n\n\nA chunk is the characters between absolute word breaks.\nAbsolute word breaks match: `/[\\s,{}[\\]]/`, i.e. spaces or braces.\n\n\n**Error Message:** _Maximum word length exceeded._\n\n\nIf you are seeing this message, it means that the file contains a very long line\nwithout many word breaks.", "scope": "language-overridable", "type": "number" }, diff --git a/packages/_server/spell-checker-config.schema.json b/packages/_server/spell-checker-config.schema.json index 624962e1a7..d39b42a5e1 100644 --- a/packages/_server/spell-checker-config.schema.json +++ b/packages/_server/spell-checker-config.schema.json @@ -3377,8 +3377,8 @@ "properties": { "cSpell.blockCheckingWhenAverageChunkSizeGreaterThan": { "default": 80, - "description": "The maximum average length of chunks of text without word breaks.\n\n\nA chunk is the characters between absolute word breaks. Absolute word breaks match: `/[\\s,{}[\\]]/`\n\n\n**Error Message:** _Average Word Size is Too High._\n\n\nIf you are seeing this message, it means that the file contains mostly long lines without many word breaks.", - "markdownDescription": "The maximum average length of chunks of text without word breaks.\n\n\nA chunk is the characters between absolute word breaks.\nAbsolute word breaks match: `/[\\s,{}[\\]]/`\n\n\n**Error Message:** _Average Word Size is Too High._\n\n\nIf you are seeing this message, it means that the file contains mostly long lines\nwithout many word breaks.", + "description": "The maximum average length of chunks of text without word breaks.\n\n\nA chunk is the characters between absolute word breaks. Absolute word breaks match: `/[\\s,{}[\\]]/`\n\n\n**Error Message:** _Average word length is too long._\n\n\nIf you are seeing this message, it means that the file contains mostly long lines without many word breaks.", + "markdownDescription": "The maximum average length of chunks of text without word breaks.\n\n\nA chunk is the characters between absolute word breaks.\nAbsolute word breaks match: `/[\\s,{}[\\]]/`\n\n\n**Error Message:** _Average word length is too long._\n\n\nIf you are seeing this message, it means that the file contains mostly long lines\nwithout many word breaks.", "scope": "language-overridable", "type": "number" }, @@ -3391,8 +3391,8 @@ }, "cSpell.blockCheckingWhenTextChunkSizeGreaterThan": { "default": 500, - "description": "The maximum length of a chunk of text without word breaks.\n\n\nIt is used to prevent spell checking of generated files.\n\n\nA chunk is the characters between absolute word breaks. Absolute word breaks match: `/[\\s,{}[\\]]/`, i.e. spaces or braces.\n\n\n**Error Message:** _Maximum Word Length is Too High._\n\n\nIf you are seeing this message, it means that the file contains a very long line without many word breaks.", - "markdownDescription": "The maximum length of a chunk of text without word breaks.\n\n\nIt is used to prevent spell checking of generated files.\n\n\nA chunk is the characters between absolute word breaks.\nAbsolute word breaks match: `/[\\s,{}[\\]]/`, i.e. spaces or braces.\n\n\n**Error Message:** _Maximum Word Length is Too High._\n\n\nIf you are seeing this message, it means that the file contains a very long line\nwithout many word breaks.", + "description": "The maximum length of a chunk of text without word breaks.\n\n\nIt is used to prevent spell checking of generated files.\n\n\nA chunk is the characters between absolute word breaks. Absolute word breaks match: `/[\\s,{}[\\]]/`, i.e. spaces or braces.\n\n\n**Error Message:** _Maximum word length exceeded._\n\n\nIf you are seeing this message, it means that the file contains a very long line without many word breaks.", + "markdownDescription": "The maximum length of a chunk of text without word breaks.\n\n\nIt is used to prevent spell checking of generated files.\n\n\nA chunk is the characters between absolute word breaks.\nAbsolute word breaks match: `/[\\s,{}[\\]]/`, i.e. spaces or braces.\n\n\n**Error Message:** _Maximum word length exceeded._\n\n\nIf you are seeing this message, it means that the file contains a very long line\nwithout many word breaks.", "scope": "language-overridable", "type": "number" }, diff --git a/packages/_server/src/api/api.ts b/packages/_server/src/api/api.ts index 02c1507f19..7e01b193c3 100644 --- a/packages/_server/src/api/api.ts +++ b/packages/_server/src/api/api.ts @@ -22,6 +22,7 @@ import type { GetConfigurationTargetsResult, GetSpellCheckingOffsetsResult, IsSpellCheckEnabledResult, + OnBlockFile, OnDocumentConfigChange, OnSpellCheckDocumentStep, PublishDiagnostics, @@ -102,6 +103,13 @@ export interface ClientNotificationsAPI { * @param notification - The notification. */ onDocumentConfigChange(notification: OnDocumentConfigChange): void; + + /** + * Notify the client that a file is blocked from being spell checked. + * @param uri - the uri of the document. + * @param block - the reason the file is blocked. + */ + onBlockFile(notification: OnBlockFile): void; } export interface SpellCheckerServerAPI extends RpcAPI { diff --git a/packages/_server/src/api/apiModels.ts b/packages/_server/src/api/apiModels.ts index dff4ffac49..522c1f910d 100644 --- a/packages/_server/src/api/apiModels.ts +++ b/packages/_server/src/api/apiModels.ts @@ -20,7 +20,10 @@ export type { Position, Range } from 'vscode-languageserver-types'; export interface BlockedFileReason { code: string; message: string; - documentationRefUri?: UriString; + documentationRefUri: UriString; + settingsUri: UriString; + settingsID: string; + notificationMessage: string; } export type UriString = string; @@ -369,3 +372,13 @@ export interface IsSpellCheckingEnabledForUrisResponse { export interface OnDocumentConfigChange { uris: DocumentUri[]; } + +/** + * Notify the client that a file is blocked from being spell checked. + */ +export interface OnBlockFile { + /** the uri of the document being blocked */ + uri: DocumentUri; + /** the reason the file is blocked. */ + reason: BlockedFileReason; +} diff --git a/packages/_server/src/config/cspellConfig/SpellCheckerShouldCheckDocSettings.mts b/packages/_server/src/config/cspellConfig/SpellCheckerShouldCheckDocSettings.mts index 47de2638fb..ed1d666871 100644 --- a/packages/_server/src/config/cspellConfig/SpellCheckerShouldCheckDocSettings.mts +++ b/packages/_server/src/config/cspellConfig/SpellCheckerShouldCheckDocSettings.mts @@ -26,7 +26,7 @@ export interface SpellCheckerShouldCheckDocSettings { * Absolute word breaks match: `/[\s,{}[\]]/`, i.e. spaces or braces. * * - * **Error Message:** _Maximum Word Length is Too High._ + * **Error Message:** _Maximum word length exceeded._ * * * If you are seeing this message, it means that the file contains a very long line @@ -45,7 +45,7 @@ export interface SpellCheckerShouldCheckDocSettings { * Absolute word breaks match: `/[\s,{}[\]]/` * * - * **Error Message:** _Average Word Size is Too High._ + * **Error Message:** _Average word length is too long._ * * * If you are seeing this message, it means that the file contains mostly long lines diff --git a/packages/_server/src/server.mts b/packages/_server/src/server.mts index a64ea440d6..d57de31532 100644 --- a/packages/_server/src/server.mts +++ b/packages/_server/src/server.mts @@ -53,6 +53,7 @@ import type { PartialServerSideHandlers } from './serverApi.mjs'; import { createServerApi } from './serverApi.mjs'; import { createOnSuggestionsHandler } from './suggestionsServer.mjs'; import { handleTraceRequest } from './trace.js'; +import type { MinifiedReason } from './utils/analysis.mjs'; import { defaultIsTextLikelyMinifiedOptions, isTextLikelyMinified } from './utils/analysis.mjs'; import { catchPromise } from './utils/catchPromise.mjs'; import { debounce as simpleDebounce } from './utils/debounce.mjs'; @@ -524,10 +525,6 @@ export function run(): void { blockCheckingWhenAverageChunkSizeGreaterThan = defaultIsTextLikelyMinifiedOptions.blockCheckingWhenAverageChunkSizeGreaterThan, blockCheckingWhenTextChunkSizeGreaterThan = defaultIsTextLikelyMinifiedOptions.blockCheckingWhenTextChunkSizeGreaterThan, } = settings; - if (blockedFiles.has(uri)) { - log(`File is blocked ${blockedFiles.get(uri)?.message}`, uri); - return true; - } const isMiniReason = textDocument.getText && isTextLikelyMinified(textDocument.getText(), { @@ -537,9 +534,14 @@ export function run(): void { }); if (isMiniReason) { + const notify = !blockedFiles.has(uri); blockedFiles.set(uri, isMiniReason); - // connection.window.showInformationMessage(`File not spell checked:\n${isMiniReason}\n\"${uriToName(toUri(uri))}"`); log(`File is blocked: ${isMiniReason.message}`, uri); + if (notify) { + notifyUserAboutBlockedFile(uri, isMiniReason); + } + } else { + blockedFiles.delete(uri); } return !!isMiniReason; @@ -771,6 +773,14 @@ export function run(): void { disposables.push(() => v.unsubscribe()); return v; } + + async function notifyUserAboutBlockedFile(uri: string, reason: MinifiedReason) { + try { + clientServerApi.clientNotification.onBlockFile({ uri, reason }); + } catch (e) { + logError(`notifyUserAboutBlockedFile ${e}`); + } + } } interface TextDocumentInfo { diff --git a/packages/_server/src/serverApi.mts b/packages/_server/src/serverApi.mts index 4697a196dd..0b6db30b15 100644 --- a/packages/_server/src/serverApi.mts +++ b/packages/_server/src/serverApi.mts @@ -33,6 +33,7 @@ export function createServerApi(connection: MessageConnection, handlers: Partial onSpellCheckDocument: true, onDiagnostics: true, onDocumentConfigChange: true, + onBlockFile: true, }, }; return createServerSideApi(connection, api, logger); diff --git a/packages/_server/src/test/test.api.ts b/packages/_server/src/test/test.api.ts index bce748cdeb..6b5262dc41 100644 --- a/packages/_server/src/test/test.api.ts +++ b/packages/_server/src/test/test.api.ts @@ -24,6 +24,7 @@ export function createMockServerSideApi() { onSpellCheckDocument: vi.fn(), onDiagnostics: vi.fn(), onDocumentConfigChange: vi.fn(), + onBlockFile: vi.fn(), }, clientRequest: { onWorkspaceConfigForDocumentRequest: vi.fn(), diff --git a/packages/_server/src/utils/analysis.mts b/packages/_server/src/utils/analysis.mts index 6f410e7e58..7099e6f2b2 100644 --- a/packages/_server/src/utils/analysis.mts +++ b/packages/_server/src/utils/analysis.mts @@ -1,29 +1,41 @@ -import { genSequence } from 'gensequence'; +import { opFilter, opMap, opTake, pipe } from '@cspell/cspell-pipe/sync'; import type { BlockedFileReason } from '../api.js'; -export interface MinifiedReason extends BlockedFileReason { - documentationRefUri: string; -} +export type MinifiedReason = BlockedFileReason; export const ReasonLineLength: MinifiedReason = { code: 'Lines_too_long.', message: 'Lines are too long.', + notificationMessage: + 'For performance reasons, the spell checker does not check documents where the line length is greater than ${limit}.', + settingsUri: 'vscode://settings/cSpell.blockCheckingWhenLineLengthGreaterThan', + settingsID: 'cSpell.blockCheckingWhenLineLengthGreaterThan', documentationRefUri: - 'https://streetsidesoftware.github.io/vscode-spell-checker/docs/configuration/performance/#cspellblockcheckingwhenlinelengthgreaterthan', + 'https://streetsidesoftware.com/vscode-spell-checker/docs/configuration/performance/#cspellblockcheckingwhenlinelengthgreaterthan', }; export const ReasonAverageWordsSize: MinifiedReason = { code: 'Word_Size_Too_High.', - message: 'Average Word Size is Too High.', + message: 'Average word length is too long.', + notificationMessage: + 'For performance reasons, the spell checker does not check documents where the average block ' + + 'of text without spaces or word breaks is greater than ${limit}.', + settingsUri: 'vscode://settings/cSpell.blockCheckingWhenAverageChunkSizeGreaterThan', + settingsID: 'cSpell.blockCheckingWhenAverageChunkSizeGreaterThan', documentationRefUri: - 'https://streetsidesoftware.github.io/vscode-spell-checker/docs/configuration/performance/#cspellblockcheckingwhenaveragechunksizegreaterthan', + 'https://streetsidesoftware.com/vscode-spell-checker/docs/configuration/performance/#cspellblockcheckingwhenaveragechunksizegreaterthan', }; export const ReasonMaxWordsSize: MinifiedReason = { code: 'Maximum_Word_Length_Exceeded', - message: 'Maximum Word Length Exceeded.', + message: 'Maximum word length exceeded.', + notificationMessage: + 'For performance reasons, the spell checker does not check documents with very long blocks of text ' + + 'without spaces or word breaks. The limit is currently ${limit}.', + settingsUri: 'vscode://settings/cSpell.blockCheckingWhenTextChunkSizeGreaterThan', + settingsID: 'cSpell.blockCheckingWhenTextChunkSizeGreaterThan', documentationRefUri: - 'https://streetsidesoftware.github.io/vscode-spell-checker/docs/configuration/performance/#cspellblockcheckingwhentextchunksizegreaterthan', + 'https://streetsidesoftware.com/vscode-spell-checker/docs/configuration/performance/#cspellblockcheckingwhentextchunksizegreaterthan', }; export interface IsTextLikelyMinifiedOptions { @@ -45,6 +57,15 @@ export const defaultIsTextLikelyMinifiedOptions: IsTextLikelyMinifiedOptions = { blockCheckingWhenAverageChunkSizeGreaterThan: 80, }; +export function hydrateReason(reason: MinifiedReason, limit: number): MinifiedReason { + return { + ...reason, + notificationMessage: reason.notificationMessage.replaceAll('${limit}', limit.toString()), + }; +} + +const ignoreUrls = /\b[a-z]{3,}:\/[-/a-z0-9@:%._+~#=?&]+/gi; + /** * Check if a document is minified making spell checking difficult and slow. * @@ -52,32 +73,37 @@ export const defaultIsTextLikelyMinifiedOptions: IsTextLikelyMinifiedOptions = { * @returns true - if the file might be minified. */ export function isTextLikelyMinified(text: string, options: IsTextLikelyMinifiedOptions): MinifiedReason | false { - const lineBreaks = [0].concat( - genSequence(text.matchAll(/\n/g)) - .map((a) => a.index || 0) - .take(100) - .toArray(), - ); - if (lineBreaks.length < 100) lineBreaks.push(text.length); + const first100 = getFirstNLinesWithText(text, 100).map((a) => a.replace(ignoreUrls, '')); - const first100 = genSequence(lineBreaks) - .scan((a, b) => [a[1], b], [0, 0]) - .map(([a, b]) => text.slice(a, b).trim()) - .filter((a) => !!a) - .toArray(); - - const over1k = genSequence(first100).first((a) => a.length > options.blockCheckingWhenLineLengthGreaterThan); - if (over1k) return ReasonLineLength; + const over1k = first100.find((a) => a.length > options.blockCheckingWhenLineLengthGreaterThan); + if (over1k) { + return hydrateReason(ReasonLineLength, options.blockCheckingWhenLineLengthGreaterThan); + } const sampleText = first100.join('\n'); - const chunks = [...sampleText.matchAll(/[\s,{}[\]]+/g)].map((a) => a.index || 0); + const chunks = [...sampleText.matchAll(/[\s,{}[\]/]+/g)].map((a) => a.index || 0); chunks.push(sampleText.length); const wordCount = chunks.length; const avgChunkSize = sampleText.length / wordCount; - if (avgChunkSize > options.blockCheckingWhenAverageChunkSizeGreaterThan) return ReasonAverageWordsSize; + if (avgChunkSize > options.blockCheckingWhenAverageChunkSizeGreaterThan) { + return hydrateReason(ReasonAverageWordsSize, options.blockCheckingWhenAverageChunkSizeGreaterThan); + } const maxChunkSize = chunks.reduce((a, b) => [b, Math.max(a[1], b - a[0])], [0, 0])[1]; - if (maxChunkSize > options.blockCheckingWhenTextChunkSizeGreaterThan) return ReasonMaxWordsSize; + if (maxChunkSize > options.blockCheckingWhenTextChunkSizeGreaterThan) { + return hydrateReason(ReasonMaxWordsSize, options.blockCheckingWhenTextChunkSizeGreaterThan); + } return false; } + +export function getFirstNLinesWithText(text: string, n: number): string[] { + return [ + ...pipe( + text.matchAll(/^.*$/gm), + opMap((a) => a[0].trim()), + opFilter((a) => !!a), + opTake(n), + ), + ]; +} diff --git a/packages/_server/src/utils/analysis.test.mts b/packages/_server/src/utils/analysis.test.mts index 770c7a3443..9c6e12a2b9 100644 --- a/packages/_server/src/utils/analysis.test.mts +++ b/packages/_server/src/utils/analysis.test.mts @@ -3,7 +3,7 @@ import * as Path from 'path'; import { describe, expect, test } from 'vitest'; import type { IsTextLikelyMinifiedOptions } from './analysis.mjs'; -import { isTextLikelyMinified, ReasonAverageWordsSize, ReasonLineLength, ReasonMaxWordsSize } from './analysis.mjs'; +import { hydrateReason, isTextLikelyMinified, ReasonAverageWordsSize, ReasonLineLength, ReasonMaxWordsSize } from './analysis.mjs'; const sampleWebpack = FS.readFileSync(Path.join(__dirname, '../../dist/main.cjs'), 'utf8').replace(/\n/g, ' '); @@ -25,6 +25,11 @@ const lines = [ 'reprehenderit. eu nulla do Lorem mollit ut incididunt excepteur. Labore voluptate ex est occaecat. Proident laborum incididunt', 'Adipisicing irure nisi enim ipsum mollit culpa officia do pariatur adipisicing. In sint esse quis do velit sint commodo sit labore', 'commodo quis. Dolore Lorem quis ullamco incididunt cupidatat ex duis dolore officia.', + 'Image: [long url](https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/29e25571-eb24-4381-9a2d-bde0ba52be2e/df3uxma-90078aec-' + + 'f043-423b-8adf-68b0db323607.png?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNhNWYwZDQ' + + 'xNWVhMGQyNmUwIiwiaXNzIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCIsIm9iaiI6W1t7InBhdGgiOiJcL2ZcLzI5ZTI1NTcxLWViM' + + 'jQtNDM4MS05YTJkLWJkZTBiYTUyYmUyZVwvZGYzdXhtYS05MDA3OGFlYy1mMDQzLTQyM2ItOGFkZi02OGIwZGIzMjM2MDcucG5nIn1dXSwiYXVkIjpbInVybjpzZXJ' + + '2aWNlOmZpbGUuZG93bmxvYWQiXX0.Pap7EkIxDlgZ1dFLyEK_MOlPIQGjvJVm5T8adKtnAn0)', // cspell:disable-line ]; const longLine = lines.slice(0, 5).join(' '); @@ -42,6 +47,30 @@ function sampleLines200() { return genLines(200); } +// cspell:ignore bmxv uxma +const sampleText = ` +# Weekly Report + +Image: [long url](https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/\ +29e25571-eb24-4381-9a2d-bde0ba52be2e/df3uxma-90078aec-f043-423b-8adf-68b0db323607.png?\ +token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNh\ +NWYwZDQxNWVhMGQyNmUwIiwiaXNzIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCI\ +sIm9iaiI6W1t7InBhdGgiOiJcL2ZcLzI5ZTI1NTcxLWViMjQtNDM4MS05YTJkLWJkZTBiYTUyYmUyZVwvZGYzdX\ +htYS05MDA3OGFlYy1mMDQzLTQyM2ItOGFkZi02OGIwZGIzMjM2MDcucG5nIn1dXSwiYXVkIjpbInVybjpzZXJ2a\ +WNlOmZpbGUuZG93bmxvYWQiXX0.Pap7EkIxDlgZ1dFLyEK_MOlPIQGjvJVm5T8adKtnAn0) + +See VS Code Setting: [cSpell.blockCheckingWhenTextChunkSizeGreaterThan](vscode://settings/cSpell.blockCheckingWhenTextChunkSizeGreaterThan) + +This file is expected to be checked. + +Possible very long word: +token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNh\ +NWYwZDQxNWVhMGQyNmUwIiwiaXNzIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCI\ +sIm9iaiI6W1t7InBhdGgiOiJcL2ZcLzI5ZTI1NTcxLWViMjQtNDM4MS05YTJkLWJkZTBiYTUyYmUyZVwvZGYzdX\ +htYS05MDA3OGFlYy1mMDQzLTQyM2ItOGFkZi02OGIwZGIzMjM2MDcucG5nIn1dXSwiYXVkIjpbInVybjpzZXJ2a\ +WNlOmZpbGUuZG93bmxvYWQiXX0.Pap7EkIxDlgZ1dFLyEK_MOlPIQGjvJVm5T8adKtnAn0 +`; + function genSampleParagraphs(count: number) { const paragraph1 = lines.slice(0, 3).join(' '); const paragraph2 = lines.slice(3, 8).join(' '); @@ -76,15 +105,16 @@ describe('analysis', () => { ${'{}\n'} | ${{}} | ${false} ${sampleText50()} | ${{}} | ${false} ${sampleLines200()} | ${{}} | ${false} - ${sampleText50().replace(/ /g, '-')} | ${{}} | ${ReasonAverageWordsSize} - ${sampleText50().replace(/\s/g, ',')} | ${{}} | ${ReasonLineLength} + ${sampleText50().replace(/ /g, '-')} | ${{}} | ${hydrateReason(ReasonAverageWordsSize, 40)} + ${sampleText50().replace(/\s/g, ',')} | ${{}} | ${hydrateReason(ReasonLineLength, 1000)} ${sampleText200()} | ${{}} | ${false} - ${sampleText200().replace(/ /g, '-')} | ${{}} | ${ReasonAverageWordsSize} - ${sampleText200().replace(/\s/g, ',')} | ${{}} | ${ReasonLineLength} - ${sampleWebpack} | ${{}} | ${ReasonLineLength} - ${[sampleText50(), sampleLongLine(), sampleText50()].join('\n')} | ${{}} | ${ReasonMaxWordsSize} - `('isTextLikelyMinified $#', ({ text, opts, expected }) => { + ${sampleText200().replace(/ /g, '-')} | ${{}} | ${hydrateReason(ReasonAverageWordsSize, 40)} + ${sampleText200().replace(/\s/g, ',')} | ${{}} | ${hydrateReason(ReasonLineLength, 1000)} + ${sampleWebpack} | ${{}} | ${hydrateReason(ReasonLineLength, 1000)} + ${[sampleText50(), sampleLongLine(), sampleText50()].join('\n')} | ${{}} | ${hydrateReason(ReasonMaxWordsSize, 180)} + ${sampleText} | ${{}} | ${hydrateReason(ReasonMaxWordsSize, 180)} + `('isTextLikelyMinified $text', ({ text, opts, expected }) => { const options = Object.assign({}, defaultOptions, opts); - expect(isTextLikelyMinified(text, options)).toBe(expected); + expect(isTextLikelyMinified(text, options)).toEqual(expected); }); }); diff --git a/packages/client/src/client/client.mts b/packages/client/src/client/client.mts index ad439b3cb4..21327bc24a 100644 --- a/packages/client/src/client/client.mts +++ b/packages/client/src/client/client.mts @@ -4,6 +4,7 @@ import type { ConfigFieldSelector, ConfigurationFields, GetConfigurationTargetsResult, + OnBlockFile, OnDocumentConfigChange, SpellingSuggestionsResult, WorkspaceConfigForDocument, @@ -72,6 +73,7 @@ export class CSpellClient implements Disposable { private disposables = createDisposableList(); private broadcasterOnSpellCheckDocument = createBroadcaster(); private broadcasterOnDocumentConfigChange = createBroadcaster(); + private broadcasterOnBlockFile = createBroadcaster(); private ready = new Resolvable(); /** @@ -315,6 +317,10 @@ export class CSpellClient implements Disposable { return this.broadcasterOnDocumentConfigChange.listen(fn); } + public onBlockFile(fn: (p: OnBlockFile) => void): Disposable { + return this.broadcasterOnBlockFile.listen(fn); + } + public async requestSpellingSuggestionsCodeActions(doc: TextDocument, range: Range, diagnostics: Diagnostic[]): Promise { const params: CodeActionParams = { textDocument: VSCodeLangClientTextDocumentIdentifier.create(doc.uri.toString()), @@ -357,8 +363,11 @@ export class CSpellClient implements Disposable { } private registerHandleNotificationsFromServer() { - this.registerDisposable(this.serverApi.onSpellCheckDocument((p) => this.broadcasterOnSpellCheckDocument.send(p))); - this.registerDisposable(this.serverApi.onDocumentConfigChange((p) => this.broadcasterOnDocumentConfigChange.send(p))); + this.registerDisposable( + this.serverApi.onSpellCheckDocument((p) => this.broadcasterOnSpellCheckDocument.send(p)), + this.serverApi.onDocumentConfigChange((p) => this.broadcasterOnDocumentConfigChange.send(p)), + this.serverApi.onBlockFile((p) => this.broadcasterOnBlockFile.send(p)), + ); } } diff --git a/packages/client/src/client/server/server.mts b/packages/client/src/client/server/server.mts index 787e8b61eb..6c82ce784b 100644 --- a/packages/client/src/client/server/server.mts +++ b/packages/client/src/client/server/server.mts @@ -63,6 +63,7 @@ interface ExtensionSide { onDiagnostics: ClientSideApi['clientNotification']['onDiagnostics']['subscribe']; onDocumentConfigChange: ClientSideApi['clientNotification']['onDocumentConfigChange']['subscribe']; onSpellCheckDocument: ClientSideApi['clientNotification']['onSpellCheckDocument']['subscribe']; + onBlockFile: ClientSideApi['clientNotification']['onBlockFile']['subscribe']; onWorkspaceConfigForDocumentRequest: ClientSideApi['clientRequest']['onWorkspaceConfigForDocumentRequest']['subscribe']; } export interface ServerApi extends ServerSide, ExtensionSide, Disposable {} @@ -99,6 +100,7 @@ export function createServerApi(client: LanguageClient): ServerApi { onSpellCheckDocument: true, onDiagnostics: true, onDocumentConfigChange: true, + onBlockFile: true, }, clientRequests: { onWorkspaceConfigForDocumentRequest: true, @@ -124,6 +126,7 @@ export function createServerApi(client: LanguageClient): ServerApi { checkDocument: log2Sfn(serverRequest.checkDocument, 'checkDocument'), onSpellCheckDocument: (fn) => clientNotification.onSpellCheckDocument.subscribe(log2Cfn(fn, 'onSpellCheckDocument')), onDocumentConfigChange: (fn) => clientNotification.onDocumentConfigChange.subscribe(log2Cfn(fn, 'onDocumentConfigChange')), + onBlockFile: (fn) => clientNotification.onBlockFile.subscribe(log2Cfn(fn, 'onBlockFile')), onDiagnostics: (fn) => clientNotification.onDiagnostics.subscribe(log2Cfn(fn, 'onDiagnostics')), onWorkspaceConfigForDocumentRequest: (fn) => clientRequest.onWorkspaceConfigForDocumentRequest.subscribe(log2Cfn(fn, 'onWorkspaceConfigForDocumentRequest')), diff --git a/packages/client/src/extension.mts b/packages/client/src/extension.mts index 87c35c44cb..88658ff825 100644 --- a/packages/client/src/extension.mts +++ b/packages/client/src/extension.mts @@ -1,5 +1,6 @@ +import { uriToName } from '@internal/common-utils'; import { logger } from '@internal/common-utils/log'; -import type { ConfigFieldSelector, ConfigurationFields } from 'code-spell-checker-server/api'; +import type { ConfigFieldSelector, ConfigurationFields, OnBlockFile } from 'code-spell-checker-server/api'; import { createDisposableList } from 'utils-disposables'; import type { ExtensionContext } from 'vscode'; import * as vscode from 'vscode'; @@ -31,6 +32,7 @@ import { createLanguageStatus } from './statusbar/languageStatus.mjs'; import { createEventLogger, updateDocumentRelatedContext } from './storage/index.mjs'; import { logErrors, silenceErrors } from './util/errors.js'; import { performance } from './util/perf.js'; +import { isUriInAnyTab } from './vscode/tabs.mjs'; import { activate as activateWebview } from './webview/index.mjs'; performance.mark('cspell_done_import'); @@ -40,15 +42,13 @@ let currLogLevel: CSpellSettings['logLevel'] = undefined; modules.init(); -export async function activate(context: ExtensionContext): Promise { +export function activate(context: ExtensionContext): Promise { performance.mark('cspell_activate_start'); di.set('extensionContext', context); const eventLogger = createEventLogger(context.globalStorageUri); di.set('eventLogger', eventLogger); eventLogger.logActivate(); - const logOutput = vscode.window.createOutputChannel('Code Spell Checker', { log: true }); - const dLogger = bindLoggerToOutput(logOutput); setOutputChannelLogLevel(); const eIssueTracker = new vscode.EventEmitter(); @@ -57,6 +57,13 @@ export async function activate(context: ExtensionContext): Promise activateIssueViewer(context, pIssueTracker); activateFileIssuesViewer(context, pIssueTracker); + return _activate(context, eIssueTracker); +} + +async function _activate(context: ExtensionContext, eIssueTracker: vscode.EventEmitter): Promise { + const logOutput = vscode.window.createOutputChannel('Code Spell Checker', { log: true }); + const dLogger = bindLoggerToOutput(logOutput); + // Get the cSpell Client const client = await CSpellClient.create(context); context.subscriptions.push(client, logOutput, dLogger); @@ -153,6 +160,7 @@ export async function activate(context: ExtensionContext): Promise vscode.workspace.onDidChangeConfiguration(handleOnDidChangeConfiguration), createLanguageStatus({ areIssuesVisible: () => decorator.visible, onDidChangeVisibility: decorator.onDidChangeVisibility }), registerActionsMenu({ areIssuesVisible: () => decorator.visible }), + client.onBlockFile(notifyUserOfBlockedFile), ); await registerCspellInlineCompletionProviders(context.subscriptions).catch(() => undefined); @@ -292,3 +300,21 @@ function setOutputChannelLogLevel(level?: CSpellSettings['logLevel']) { const logLevel = level ?? getLogLevel(); logger.level = logLevel; } + +async function notifyUserOfBlockedFile(onBlockFile: OnBlockFile) { + try { + const { uri, reason } = onBlockFile; + if (!isUriInAnyTab(uri)) return; + + const actions: vscode.MessageItem[] = [{ title: 'Ok' }, { title: 'Open Settings' }]; + const result = await vscode.window.showInformationMessage( + `File "${uriToName(vscode.Uri.parse(uri))}" not spell checked:\n${reason.notificationMessage}\n`, + ...actions, + ); + if (result?.title === 'Open Settings') { + await vscode.commands.executeCommand('workbench.action.openSettings', reason.settingsID); + } + } catch { + // ignore + } +} diff --git a/packages/client/src/vscode/tabs.mts b/packages/client/src/vscode/tabs.mts index ecff4d5e6d..40698e4da0 100644 --- a/packages/client/src/vscode/tabs.mts +++ b/packages/client/src/vscode/tabs.mts @@ -37,3 +37,9 @@ export function extractUrisFromTabs(): Uri[] { export function findAllOpenUrisInTabs(): Uri[] { return extractUrisFromTabs(); } + +export function isUriInAnyTab(uri: Uri | string): boolean { + const sUri = uri.toString(); + const tabs = findTabsWithUriInput(); + return tabs.some((tab) => tab.uri.toString() === sUri); +} diff --git a/packages/webview-api/src/models/settings.ts b/packages/webview-api/src/models/settings.ts index 451643fcdc..011083674a 100644 --- a/packages/webview-api/src/models/settings.ts +++ b/packages/webview-api/src/models/settings.ts @@ -75,7 +75,8 @@ export interface GitignoreInfo { export interface BlockedFileReason { code: string; message: string; - documentationRefUri?: string; + documentationRefUri: string; + settingsUri: string; } export interface IsSpellCheckEnabledResult { diff --git a/packages/webview-ui/src/views/CSpellInfo.svelte b/packages/webview-ui/src/views/CSpellInfo.svelte index e975ca57b9..5d3344ba7a 100644 --- a/packages/webview-ui/src/views/CSpellInfo.svelte +++ b/packages/webview-ui/src/views/CSpellInfo.svelte @@ -18,6 +18,7 @@ interface DisplayInfo { key: string; value: string | undefined; + url?: string; } const maxDelay = 10000; @@ -64,7 +65,8 @@ (fileConfig?.fileIsInWorkspace === false && { key: 'In Workspace', value: 'No' }) || undefined, (blocked && { key: 'Blocked Message', value: blocked.message }) || undefined, (blocked && { key: 'Blocked Code', value: blocked.code }) || undefined, - (blocked && { key: 'Blocked Dock Ref Uri', value: blocked.documentationRefUri }) || undefined, + (blocked && { key: 'Blocked Help', value: 'See Documentation', url: blocked.documentationRefUri }) || undefined, + (blocked && { key: 'Blocked Settings', value: 'VS Code Settings', url: blocked.settingsUri }) || undefined, ); return info.filter((a): a is DisplayInfo => !!a?.value); @@ -109,7 +111,13 @@
{#each fileInfo as entry}
{entry.key}:
-
{entry.value}
+
+ {#if entry.url} + {entry.value} + {:else} + {entry.value} + {/if} +
{/each}