Skip to content
Open
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
94 changes: 94 additions & 0 deletions apps/vscode-extension/src/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { has } from "@pretty-ts-errors/utils";
import { formatDiagnostic } from "@pretty-ts-errors/vscode-formatter";
import {
ExtensionContext,
languages,
MarkdownString,
Range,
window,
Uri,
} from "vscode";
import { createConverter } from "vscode-languageclient/lib/common/codeConverter";
import { hoverProvider } from "./provider/hoverProvider";
import { uriStore } from "./provider/uriStore";

const cache = new Map();
const CACHE_SIZE_MAX = 100;
const supportedDiagnosticSources = [
"ts",
"ts-plugin",
"deno-ts",
"js",
"glint",
];
const registeredLanguages = new Set<string>();

export function registerOnDidChangeDiagnostics(context: ExtensionContext) {
const converter = createConverter();
context.subscriptions.push(
languages.onDidChangeDiagnostics(async (e) => {
e.uris.forEach((uri) => {
const diagnostics = languages.getDiagnostics(uri);
const supportedDiagnostics = diagnostics.filter(
(diagnostic) =>
diagnostic.source &&
has(supportedDiagnosticSources, diagnostic.source)
);

const items: {
range: Range;
contents: MarkdownString[];
}[] = supportedDiagnostics.map((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.
let formattedMessage = cache.get(diagnostic.message);

if (!formattedMessage) {
const markdownString = new MarkdownString(
formatDiagnostic(converter.asDiagnostic(diagnostic))
);

markdownString.isTrusted = true;
markdownString.supportHtml = true;

formattedMessage = markdownString;
cache.set(diagnostic.message, formattedMessage);

if (cache.size > CACHE_SIZE_MAX) {
const firstCacheKey = cache.keys().next().value;
cache.delete(firstCacheKey);
}
}

return {
range: diagnostic.range,
contents: [formattedMessage],
};
});

uriStore.set(uri.fsPath, items);

if (items.length > 0) {
ensureHoverProviderIsRegistered(uri, context);
}
});
})
);
}

function ensureHoverProviderIsRegistered(uri: Uri, 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
)
);
}
}
98 changes: 9 additions & 89 deletions apps/vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,15 @@
import { has } from "@pretty-ts-errors/utils";
import { formatDiagnostic } from "@pretty-ts-errors/vscode-formatter";
import {
ExtensionContext,
languages,
MarkdownString,
Range,
window,
} from "vscode";
import { createConverter } from "vscode-languageclient/lib/common/codeConverter";
import { hoverProvider } from "./provider/hoverProvider";
import { ExtensionContext } from "vscode";
import { registerSelectedTextHoverProvider } from "./provider/selectedTextHoverProvider";
import { uriStore } from "./provider/uriStore";

const cache = new Map();
import { registerOnDidChangeDiagnostics } from "./diagnostics";
import { logger } from "./logger";

export function activate(context: ExtensionContext) {
const registeredLanguages = new Set<string>();
const converter = createConverter();

logger.info("activating");
context.subscriptions.push(logger);
registerSelectedTextHoverProvider(context);
registerOnDidChangeDiagnostics(context);
}

context.subscriptions.push(
languages.onDidChangeDiagnostics(async (e) => {
e.uris.forEach((uri) => {
const diagnostics = languages.getDiagnostics(uri);

const items: {
range: Range;
contents: MarkdownString[];
}[] = [];

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.
let formattedMessage = cache.get(diagnostic.message);

if (!formattedMessage) {
const markdownString = new MarkdownString(
formatDiagnostic(converter.asDiagnostic(diagnostic))
);

markdownString.isTrusted = true;
markdownString.supportHtml = true;

formattedMessage = markdownString;
cache.set(diagnostic.message, formattedMessage);

if (cache.size > 100) {
const firstCacheKey = cache.keys().next().value;
cache.delete(firstCacheKey);
}
}

items.push({
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
)
);
}
}
});
})
);
export function deactivate() {
logger.info("deactivating");
}
91 changes: 91 additions & 0 deletions apps/vscode-extension/src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { LogOutputChannel, window } from "vscode";

let instance: null | LogOutputChannel = null;

function getLogger(): LogOutputChannel {
if (instance !== null) {
return instance;
}
instance = window.createOutputChannel("Pretty TypeScript Errors", {
log: true,
});
return instance;
}

function info(...args: Parameters<LogOutputChannel["info"]>) {
getLogger().info(...args);
}

function trace(...args: Parameters<LogOutputChannel["trace"]>) {
getLogger().trace(...args);
}

function debug(...args: Parameters<LogOutputChannel["debug"]>) {
getLogger().debug(...args);
}
function warn(...args: Parameters<LogOutputChannel["warn"]>) {
getLogger().warn(...args);
}

function error(...args: Parameters<LogOutputChannel["error"]>) {
getLogger().error(...args);
}

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

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

/**
* Both in the browser and Node >= 16 (vscode 1.77 has node >= 16) have `performance` available as a global
* But `@types/node` is missing its global declaration, this fixes the type error we get from using it
*/
declare const performance: import("perf_hooks").Performance;

/**
* Measures the time it took to run `task` and reports it to the `logger` based on `logLevelThresholds`.
*
* NOTE: supports synchronous `task`s only
* @see {@link defaultThresholds} for the default thresholds
*/
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";
getLogger()[level](`${name} took ${duration.toFixed(3)}ms`);
return result;
}

function dispose() {
if (instance !== null) {
instance.dispose();
instance = null;
}
}

export const logger = {
trace,
debug,
info,
warn,
error,
measure,
dispose,
};
2 changes: 1 addition & 1 deletion apps/vscode-extension/src/provider/hoverProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { uriStore } from "./uriStore";

export const hoverProvider: HoverProvider = {
provideHover(document, position, _token) {
const itemsInUriStore = uriStore[document.uri.fsPath];
const itemsInUriStore = uriStore.get(document.uri.fsPath);

if (!itemsInUriStore) {
return null;
Expand Down
10 changes: 7 additions & 3 deletions apps/vscode-extension/src/provider/selectedTextHoverProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ import { createConverter } from "vscode-languageclient/lib/common/codeConverter"
* 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 @@ -29,6 +28,11 @@ export function registerSelectedTextHoverProvider(context: ExtensionContext) {
{
provideHover(document, position) {
const editor = window.activeTextEditor;

if (!editor) {
return;
}

const range = document.getWordRangeAtPosition(position);
const message = editor ? document.getText(editor.selection) : "";

Expand Down Expand Up @@ -61,7 +65,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
4 changes: 2 additions & 2 deletions apps/vscode-extension/src/provider/uriStore.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { MarkdownString, Range, Uri } from "vscode";

export const uriStore: Record<
export const uriStore = new Map<
Uri["path"],
{
range: Range;
contents: MarkdownString[];
}[]
> = {};
>();