diff --git a/src/extension.ts b/src/extension.ts index f15650c..1bd8876 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,8 +1,9 @@ import { ExtensionContext, + Hover, languages, MarkdownString, - Range, + Uri, window, } from "vscode"; import { createConverter } from "vscode-languageclient/lib/common/codeConverter"; @@ -12,10 +13,15 @@ import { hoverProvider } from "./provider/hoverProvider"; import { registerSelectedTextHoverProvider } from "./provider/selectedTextHoverProvider"; import { uriStore } from "./provider/uriStore"; import { has } from "./utils"; +import * as logger from "./logger"; -const cache = new Map(); +const cache = new Map(); +const CACHE_SIZE_MAX = 1000; +const supportedDiagnosticSources = ["ts", "ts-plugin", "deno-ts", "js", "glint"]; export function activate(context: ExtensionContext) { + logger.info('activating pretty-ts-errors'); + const registeredLanguages = new Set(); const converter = createConverter(); @@ -24,73 +30,67 @@ export function activate(context: ExtensionContext) { context.subscriptions.push( languages.onDidChangeDiagnostics(async (e) => { e.uris.forEach((uri) => { - const diagnostics = languages.getDiagnostics(uri); - - const items: { - range: Range; - contents: MarkdownString[]; - }[] = []; + logger.measure(`uri: '${uri.toString()}'`, () => { + const diagnostics = languages.getDiagnostics(uri); + const supportedDiagnostics = diagnostics.filter(diagnostic => diagnostic.source && has(supportedDiagnosticSources, diagnostic.source)) - let hasTsDiagnostic = false; - - diagnostics - .filter((diagnostic) => - diagnostic.source - ? has( - ["ts", "ts-plugin", "deno-ts", "js", "glint"], - diagnostic.source - ) - : false - ) - .forEach(async (diagnostic) => { - // formatDiagnostic converts message based on LSP Diagnostic type, not VSCode Diagnostic type, so it can be used in other IDEs. - // Here we convert VSCode Diagnostic to LSP Diagnostic to make formatDiagnostic recognize it. + const items = supportedDiagnostics.map(diagnostic => { let formattedMessage = cache.get(diagnostic.message); - if (!formattedMessage) { - const markdownString = new MarkdownString( + // formatDiagnostic converts message based on LSP Diagnostic type, not VSCode Diagnostic type, so it can be used in other IDEs. + // Here we convert VSCode Diagnostic to LSP Diagnostic to make formatDiagnostic recognize it. + formattedMessage = new MarkdownString( formatDiagnostic(converter.asDiagnostic(diagnostic), prettify) ); + formattedMessage.isTrusted = true; + formattedMessage.supportHtml = true; - markdownString.isTrusted = true; - markdownString.supportHtml = true; - - formattedMessage = markdownString; - cache.set(diagnostic.message, formattedMessage); - - if (cache.size > 100) { - const firstCacheKey = cache.keys().next().value; + if (cache.size > CACHE_SIZE_MAX) { + const firstCacheKey = cache.keys().next().value!; cache.delete(firstCacheKey); } + cache.set(diagnostic.message, formattedMessage); } - items.push({ + return { range: diagnostic.range, contents: [formattedMessage], - }); - - hasTsDiagnostic = true; + }; }); - uriStore[uri.fsPath] = items; - - if (hasTsDiagnostic) { - const editor = window.visibleTextEditors.find( - (editor) => editor.document.uri.toString() === uri.toString() - ); - if (editor && !registeredLanguages.has(editor.document.languageId)) { - registeredLanguages.add(editor.document.languageId); - context.subscriptions.push( - languages.registerHoverProvider( - { - language: editor.document.languageId, - }, - hoverProvider - ) - ); + if (items.length > 0) { + uriStore.set(uri.fsPath, items); + ensureHoverProviderIsRegistered(uri, registeredLanguages, context); } - } + }); }); + }) ); } + +function ensureHoverProviderIsRegistered(uri: Uri, registeredLanguages: Set, context: ExtensionContext) { + const editor = window.visibleTextEditors.find( + (editor) => editor.document.uri.toString() === uri.toString() + ); + if (editor && !registeredLanguages.has(editor.document.languageId)) { + registeredLanguages.add(editor.document.languageId); + context.subscriptions.push( + languages.registerHoverProvider( + { + language: editor.document.languageId, + }, + hoverProvider + ) + ); + } +} + +export function deactivate() { + logger.info('deactivating pretty-ts-errors'); + logger.trace('clearing cache'); + cache.clear(); + logger.trace('clearing uriStore') + uriStore.clear(); + logger.dispose(); +} diff --git a/src/format/formatDiagnostic.ts b/src/format/formatDiagnostic.ts index e428bd9..f96b662 100644 --- a/src/format/formatDiagnostic.ts +++ b/src/format/formatDiagnostic.ts @@ -4,17 +4,19 @@ import { d } from "../utils"; import { embedSymbolLinks } from "./embedSymbolLinks"; import { formatDiagnosticMessage } from "./formatDiagnosticMessage"; import { identSentences } from "./identSentences"; +import * as logger from '../logger'; export function formatDiagnostic( diagnostic: Diagnostic, format: (type: string) => string ) { - const newDiagnostic = embedSymbolLinks(diagnostic); - - return d/*html*/ ` - ${title(diagnostic)} - - ${formatDiagnosticMessage(identSentences(newDiagnostic.message), format)} - - `; + return logger.measure(`formatDiagnostic: '${diagnostic.message}'`, () => { + const newDiagnostic = embedSymbolLinks(diagnostic); + return d/*html*/ ` + ${title(diagnostic)} + + ${formatDiagnosticMessage(identSentences(newDiagnostic.message), format)} + + `; + }); } diff --git a/src/format/prettify.ts b/src/format/prettify.ts index 8becddb..55682a8 100644 --- a/src/format/prettify.ts +++ b/src/format/prettify.ts @@ -1,12 +1,13 @@ import parserTypescript from "prettier/parser-typescript"; import { format } from "prettier/standalone"; +import * as logger from '../logger'; export function prettify(text: string) { - return format(text, { + return logger.measure(`prettify: ${text}`, () => format(text, { plugins: [parserTypescript], parser: "typescript", printWidth: 60, singleAttributePerLine: false, arrowParens: "avoid", - }); + })); } diff --git a/src/logger/index.ts b/src/logger/index.ts new file mode 100644 index 0000000..d2fcdd9 --- /dev/null +++ b/src/logger/index.ts @@ -0,0 +1,62 @@ +import { LogOutputChannel, window } from 'vscode'; + +let instance: null | LogOutputChannel = null; + +function logger(): LogOutputChannel { + if (instance !== null) { + return instance; + } + instance = window.createOutputChannel('Pretty TypeScript Errors', { log: true }); + return instance; +} + +export function info(...args: Parameters) { + logger().info(...args) +} + +export function trace(...args: Parameters) { + logger().trace(...args) +} + +export function debug(...args: Parameters) { + logger().debug(...args) +} +export function warn(...args: Parameters) { + logger().warn(...args) +} + +export function error(...args: Parameters) { + logger().error(...args) +} + +type LogLevel = 'info' | 'trace' | 'debug' | 'warn' | 'error'; + +const defaultThresholds: Record = { + error: 5000, + warn: 1000, + info: 100, + debug: 50, + trace: 0, +}; + +export function measure(name: string, task: () => T, logLevelThresholds: Partial> = {}): T { + const start = performance.now(); + const result = task(); + const end = performance.now(); + const duration = end - start; + logLevelThresholds = Object.assign({}, defaultThresholds, logLevelThresholds); + const thresholds = Object.entries(logLevelThresholds) as [LogLevel, number][]; + // sort thresholds from high to low + // { info: 100, warn: 1000, trace: 0 } => [[warn, 1000], [info, 100], [trace, 0]] + thresholds.sort(([_a, a], [_b, b]) => b - a); + const level: LogLevel = thresholds.find(([_, threshold]) => duration > threshold)?.[0] || 'trace'; + logger()[level](`${name} took ${duration.toFixed(3)}ms`); + return result; +} + +export function dispose() { + if (instance !== null) { + instance.dispose(); + instance = null; + } +} diff --git a/src/provider/hoverProvider.ts b/src/provider/hoverProvider.ts index 5e41754..eef8e70 100644 --- a/src/provider/hoverProvider.ts +++ b/src/provider/hoverProvider.ts @@ -3,19 +3,23 @@ import { uriStore } from "./uriStore"; export const hoverProvider: HoverProvider = { provideHover(document, position) { - const itemsInUriStore = uriStore[document.uri.fsPath]; + const itemsInUriStore = uriStore.get(document.uri.fsPath); if (!itemsInUriStore) { return null; } - const itemInRange = itemsInUriStore.filter((item) => - item.range.contains(position) + const itemsInRange = itemsInUriStore.filter((item) => + item.range!.contains(position) ); + if (itemsInRange.length === 0) { + return; + } + return { - range: itemInRange?.[0]?.range, - contents: itemInRange.flatMap((item) => item.contents), + range: itemsInRange[0].range, + contents: itemsInRange.flatMap((item) => item.contents), }; }, }; diff --git a/src/provider/selectedTextHoverProvider.ts b/src/provider/selectedTextHoverProvider.ts index 01804f9..9a1e63e 100644 --- a/src/provider/selectedTextHoverProvider.ts +++ b/src/provider/selectedTextHoverProvider.ts @@ -16,12 +16,11 @@ import { d } from "../utils"; * It format selected text and help test things visually easier. */ export function registerSelectedTextHoverProvider(context: ExtensionContext) { - const converter = createConverter(); - if (context.extensionMode !== ExtensionMode.Development) { return; } + const converter = createConverter(); context.subscriptions.push( languages.registerHoverProvider( { @@ -31,8 +30,13 @@ export function registerSelectedTextHoverProvider(context: ExtensionContext) { { provideHover(document, position) { const editor = window.activeTextEditor; + + if (!editor) { + return; + } + const range = document.getWordRangeAtPosition(position); - const message = document.getText(editor!.selection); + const message = document.getText(editor.selection); const contents = range && message @@ -65,7 +69,7 @@ export function registerSelectedTextHoverProvider(context: ExtensionContext) { ); } -const debugHoverHeader = d/*html*/ ` +const debugHoverHeader = d/*html*/ ` Formatted selected text (debug only) diff --git a/src/provider/uriStore.ts b/src/provider/uriStore.ts index ec87bcc..1d849ef 100644 --- a/src/provider/uriStore.ts +++ b/src/provider/uriStore.ts @@ -1,9 +1,6 @@ -import { MarkdownString, Range, Uri } from "vscode"; +import { Hover, Uri } from "vscode"; -export const uriStore: Record< +export const uriStore = new Map< Uri["path"], - { - range: Range; - contents: MarkdownString[]; - }[] -> = {}; + Hover[] +>();