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
30 changes: 24 additions & 6 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,36 @@ labels: bug
assignees: ""
---

**Describe the bug**
### **Describe the bug**

A clear and concise description of what the bug is.

**Expected behavior**
### **Expected behavior**

A clear and concise description of what you expected to happen.

**Original error**
### **Original error**

If this bug is related to an error that is not formatting well, please
attach the original error in a code block:
<code>

```
Type 'number' is not assignable to type 'string'.ts(2322)
</code>
```

### **Logs**

Add the logs to help debugging what went wrong. See [these instructions](../../docs/hide-original-errors.md) on how to find and export the logs.

Either add it as an external file or put them in between these `<pre><code>` tags below:

<details>
<summary>Logs</summary>
</pre></code>
<!-- replace this comment with your log output -->
</code></pre>
</details>

### **Screenshots**

**Screenshots**
If applicable, add screenshots to help explain your problem.
15 changes: 11 additions & 4 deletions apps/vscode-extension/src/commands/copyError.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { commands, env, window, type ExtensionContext } from "vscode";
import { execute } from "./execute";

const COMMAND_ID = "prettyTsErrors.copyError";

export function registerCopyError(context: ExtensionContext) {
context.subscriptions.push(
commands.registerCommand(
"prettyTsErrors.copyError",
async (errorMessage: string) => {
commands.registerCommand(COMMAND_ID, async (errorMessage: unknown) =>
execute(COMMAND_ID, async () => {
if (typeof errorMessage !== "string") {
throw new Error("cannot write non-string value to clipboard", {
cause: errorMessage,
});
}
await env.clipboard.writeText(errorMessage);
window.showInformationMessage("Copied error message to clipboard!");
}
})
)
);
}
24 changes: 24 additions & 0 deletions apps/vscode-extension/src/commands/execute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { window } from "vscode";
import { logger } from "../logger";

/**
* A wrapper function to execute command tasks while providing user feedback and logging on errors.
*/
export async function execute(
commandName: string,
task: (...args: unknown[]) => unknown | Promise<unknown>
) {
try {
return await task();
} catch (error) {
if (error instanceof Error) {
logger.error(error);
} else if (typeof error === "string") {
logger.error(error);
} else {
logger.error("caught non-string or error value: ", error);
}
window.showErrorMessage(`Failed to execute command: '${commandName}'`);
throw error;
}
}
24 changes: 24 additions & 0 deletions apps/vscode-extension/src/commands/openMarkdownPreview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { ExtensionContext } from "vscode";
import { commands } from "vscode";
import { MarkdownWebviewProvider } from "../provider/markdownWebviewProvider";
import { tryEnsureUri } from "./validate";
import { execute } from "./execute";

const COMMAND_ID = "prettyTsErrors.openMarkdownPreview";

export function registerOpenMarkdownPreview(context: ExtensionContext) {
const provider = new MarkdownWebviewProvider(context);
context.subscriptions.push(
commands.registerCommand(COMMAND_ID, async (maybeUriLike: unknown) =>
execute(COMMAND_ID, async () => {
const { isValidUri, uri } = tryEnsureUri(maybeUriLike);
if (!isValidUri) {
throw new Error("cannot open markdown preview with an invalid uri", {
cause: maybeUriLike,
});
}
await provider.openMarkdownPreview(uri);
})
)
);
}
128 changes: 75 additions & 53 deletions apps/vscode-extension/src/commands/revealSelection.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,94 @@
import {
type ExtensionContext,
type Tab,
commands,
Range,
TabInputText,
TabInputWebview,
Uri,
ViewColumn,
window,
} from "vscode";
import { MarkdownWebviewProvider } from "../provider/markdownWebviewProvider";
import { tryEnsureRange, tryEnsureUri } from "./validate";
import { execute } from "./execute";

const COMMAND_ID = "prettyTsErrors.revealSelection";

export function registerRevealSelection(context: ExtensionContext) {
context.subscriptions.push(
commands.registerCommand(
"prettyTsErrors.revealSelection",
async (uri: Uri, range: Range) => {
// ensure these are real instances
uri = Uri.from({ ...uri });
range = new Range(
range.start.line,
range.start.character,
range.end.line,
range.end.character
);

// default behaviour is to use the active view column
let viewColumn = ViewColumn.Active;

// detect if the active tab is our preview webview
let isFromMarkdownPreviewWebview = false;
const activeTab = window.tabGroups.activeTabGroup.activeTab;
if (activeTab && activeTab.input instanceof TabInputWebview) {
// For an unknown reason this string is prefixed with something like `mainthread-${viewType}`
// endsWith should handle a full match and the prefixed versions
if (
activeTab.input.viewType.endsWith(MarkdownWebviewProvider.viewType)
) {
isFromMarkdownPreviewWebview = true;
COMMAND_ID,
async (maybeUriLike: unknown, maybeRangeLike: unknown) =>
execute(COMMAND_ID, async () => {
const { isValidUri, uri } = tryEnsureUri(maybeUriLike);
const { isValidRange, range } = tryEnsureRange(maybeRangeLike);
if (!isValidUri || !isValidRange) {
throw new Error(
"cannot reveal selection with invalid range or uri",
{
cause: {
range: maybeRangeLike,
uri: maybeUriLike,
},
}
);
}
}

if (isFromMarkdownPreviewWebview) {
// find a tab group where the file is open, then use that view column for the `vscode.open` command
const tabs = window.tabGroups.all.flatMap(
(tabGroup) => tabGroup.tabs
);
const tabWithFileOpen = tabs.find((tab) => {
if (tab.input instanceof TabInputText) {
return tab.input.uri.toString() === uri.toString();
}
return false;
const viewColumn = determineViewColumn(uri);
return commands.executeCommand("vscode.open", uri, {
selection: range,
viewColumn,
});
if (tabWithFileOpen) {
viewColumn = tabWithFileOpen.group.viewColumn;
} else {
// If markdown preview is not open on 1, open the link in 1, else open the link in 2
viewColumn =
activeTab!.group.viewColumn !== 1
? ViewColumn.One
: ViewColumn.Two;
}
}

return commands.executeCommand("vscode.open", uri, {
selection: range,
viewColumn,
});
}
})
)
);
}

/**
* Determine the view column to use for the `vscode.open` command:
* - if the command is not called from a markdown webview preview, default to `ViewColumn.Active`, which will defer the decision to VS Code
* - else check if the file is open somewhere and use its view column
* - else use the opposite of where the preview resides (left if the preview is right, right if the preview is left)
*
* This seems a bit complex, but without it VS Code will open a new view column to the right and open the given uri in it, regardless of the current layout and open files.
* Using this algorithm makes it feel more intuitive and less stupid.
*/
function determineViewColumn(uri: Uri): ViewColumn {
if (!isFromMarkdownPreviewWebview()) {
return ViewColumn.Active;
}
const tab = findTabWithFileOpen(uri);
if (tab) {
return tab.group.viewColumn;
}
// If markdown preview is not open on 1, open the link in 1, else open the link in 2
const activeTab = window.tabGroups.activeTabGroup.activeTab;
return activeTab!.group.viewColumn !== 1 ? ViewColumn.One : ViewColumn.Two;
}

/**
* Search for an open tab with the given `uri` and return it if it exists
*/
function findTabWithFileOpen(uri: Uri): Tab | undefined {
const tabs = window.tabGroups.all.flatMap((tabGroup) => tabGroup.tabs);
return tabs.find((tab) => {
if (tab.input instanceof TabInputText) {
return tab.input.uri.toString() === uri.toString();
}
return false;
});
}

/**
* Returns `true` if the active tab is a pretty-ts-errors markdown preview webview
*/
function isFromMarkdownPreviewWebview(): boolean {
const activeTab = window.tabGroups.activeTabGroup.activeTab;
if (activeTab && activeTab.input instanceof TabInputWebview) {
// For an unknown reason this string is prefixed with something like `mainthread-${viewType}`
// endsWith should handle a full match and the prefixed versions
if (activeTab.input.viewType.endsWith(MarkdownWebviewProvider.viewType)) {
return true;
}
}
return false;
}
79 changes: 79 additions & 0 deletions apps/vscode-extension/src/commands/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Range, Uri } from "vscode";

export function tryEnsureUri(
maybeUriLike: unknown
): { isValidUri: true; uri: Uri } | { isValidUri: false; uri?: undefined } {
if (maybeUriLike instanceof Uri) {
return { isValidUri: true, uri: maybeUriLike };
}
if (typeof maybeUriLike === "string") {
try {
return { isValidUri: true, uri: Uri.parse(maybeUriLike, true) };
} catch (error) {
return { isValidUri: false };
}
}
if (isUriLike(maybeUriLike)) {
return { isValidUri: true, uri: Uri.from(maybeUriLike) };
}
return { isValidUri: false };
}

type UriLike = Parameters<typeof Uri.from>[0];

function isUriLike(value: unknown): value is UriLike {
return (
typeof value === "object" &&
value != null &&
"scheme" in value &&
typeof value.scheme === "string"
);
}

export function tryEnsureRange(
maybeRangeLike: unknown
):
| { isValidRange: true; range: Range }
| { isValidRange: false; range?: undefined } {
if (maybeRangeLike instanceof Range) {
return { isValidRange: true, range: maybeRangeLike };
}
if (isRangeLike(maybeRangeLike)) {
return {
isValidRange: true,
range: new Range(
maybeRangeLike.start.line,
maybeRangeLike.start.character,
maybeRangeLike.end.line,
maybeRangeLike.end.character
),
};
}
return { isValidRange: false };
}

type RangeLike = { start: PositionLike; end: PositionLike };

function isRangeLike(value: unknown): value is RangeLike {
return (
typeof value === "object" &&
value != null &&
"start" in value &&
isPositionLike(value.start) &&
"end" in value &&
isPositionLike(value.end)
);
}

type PositionLike = { line: number; character: number };

function isPositionLike(value: unknown): value is PositionLike {
return (
typeof value === "object" &&
value !== null &&
"line" in value &&
typeof value.line === "number" &&
"character" in value &&
typeof value.character === "number"
);
}
Loading