Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 53 additions & 53 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {
ExtensionContext,
Hover,
languages,
MarkdownString,
Range,
Uri,
window,
} from "vscode";
import { createConverter } from "vscode-languageclient/lib/common/codeConverter";
Expand All @@ -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<string, MarkdownString>();
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<string>();
const converter = createConverter();

Expand All @@ -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<string>, 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();
}
18 changes: 10 additions & 8 deletions src/format/formatDiagnostic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
<span>
${formatDiagnosticMessage(identSentences(newDiagnostic.message), format)}
</span>
`;
return logger.measure(`formatDiagnostic: '${diagnostic.message}'`, () => {
const newDiagnostic = embedSymbolLinks(diagnostic);
return d/*html*/ `
${title(diagnostic)}
<span>
${formatDiagnosticMessage(identSentences(newDiagnostic.message), format)}
</span>
`;
});
}
5 changes: 3 additions & 2 deletions src/format/prettify.ts
Original file line number Diff line number Diff line change
@@ -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",
});
}));
}
62 changes: 62 additions & 0 deletions src/logger/index.ts
Original file line number Diff line number Diff line change
@@ -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<LogOutputChannel['info']>) {
logger().info(...args)
}

export function trace(...args: Parameters<LogOutputChannel['trace']>) {
logger().trace(...args)
}

export function debug(...args: Parameters<LogOutputChannel['debug']>) {
logger().debug(...args)
}
export function warn(...args: Parameters<LogOutputChannel['warn']>) {
logger().warn(...args)
}

export function error(...args: Parameters<LogOutputChannel['error']>) {
logger().error(...args)
}

type LogLevel = 'info' | 'trace' | 'debug' | 'warn' | 'error';

const defaultThresholds: Record<LogLevel, number> = {
error: 5000,
warn: 1000,
info: 100,
debug: 50,
trace: 0,
};

export function measure<T = unknown>(name: string, task: () => T, logLevelThresholds: Partial<Record<LogLevel, number>> = {}): 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;
}
}
14 changes: 9 additions & 5 deletions src/provider/hoverProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
},
};
12 changes: 8 additions & 4 deletions src/provider/selectedTextHoverProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand All @@ -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
Expand Down Expand Up @@ -65,7 +69,7 @@ export function registerSelectedTextHoverProvider(context: ExtensionContext) {
);
}

const debugHoverHeader = d/*html*/ `
const debugHoverHeader = d/*html*/ `
<span style="color:#f96363;">
<span class="codicon codicon-debug"></span>
Formatted selected text (debug only)
Expand Down
11 changes: 4 additions & 7 deletions src/provider/uriStore.ts
Original file line number Diff line number Diff line change
@@ -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[]
>();