Skip to content

Commit

Permalink
feat(console): improve jump to error when runing in vscode (#4012)
Browse files Browse the repository at this point in the history
When opening a hyperlink in vs-code, the default behavior is to open the hyperlink in the currently focused 'Editor Group.' 

However, when we have the Console open side by side with the wing file, this default behavior causes vs-code to open the file as a new tab in the Console panel group (if the group where you have the Console is focused).

This PR adds two new endpoints: `[POST] app.openFileInEditor` and `app.openFileInEditorSubscription`
When clicking on an error link the Console emits an event named `openFileInEditor`.
Code editors like vscode (and potentially others) can subscribe to the new sub endpoint to receive these events.

Previous behaviour (using hyperlinks)

https://github.com/winglang/wing/assets/5547636/305a58ef-1f90-499e-a72d-60a00cefe2ca

New behaviour (using sub)

https://github.com/winglang/wing/assets/5547636/ae9c5d84-c70a-4160-b019-7b9144509797

Weird case when you have 425924593 groups

https://github.com/winglang/wing/assets/5547636/ec52da55-887d-4b46-84a2-8d0b566669b0

resolves #3337

*By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*.
  • Loading branch information
polamoros authored Sep 5, 2023
1 parent 2b6d976 commit 8fc4006
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 43 deletions.
43 changes: 38 additions & 5 deletions apps/vscode-wing/src/console/console-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
ViewColumn,
Uri,
ExtensionContext,
Position,
Range,
} from "vscode";

import {
Expand Down Expand Up @@ -156,6 +158,42 @@ export const createConsoleManager = (
logger.appendLine(err);
},
});

instance.client.onOpenFileInEditor({
onData: async (data) => {
const path = data?.path;
const line = data?.line - 1 || 0;
const column = data?.column || 0;

const openEditor = window.visibleTextEditors.find((editor) => {
return editor.document.uri.fsPath === path;
});

if (!openEditor || !openEditor.viewColumn) {
await commands.executeCommand(
"vscode.open",
Uri.file(path),
new Position(line, column)
);
return;
}

await commands.executeCommand("workbench.action.focusFirstEditorGroup");
for (let i = 0; i < openEditor.viewColumn - 1; i++) {
await commands.executeCommand("workbench.action.focusNextGroup");
}
await window.showTextDocument(openEditor.document, {
selection: new Range(
new Position(line, column),
new Position(line, column)
),
});
},
onError: (err) => {
logger.appendLine(err);
},
});

instances[instance.id] = instance;

await setActiveInstance(instance.id);
Expand Down Expand Up @@ -244,11 +282,6 @@ export const createConsoleManager = (
});

context.subscriptions.push(webviewPanel, explorerView, testsExplorerView);

const node = await resourcesExplorer.getChildren();
if (node[0]?.id) {
await explorerView?.reveal(new ResourceItem(node[0].id));
}
activeInstanceId = instanceId;
};

Expand Down
9 changes: 9 additions & 0 deletions apps/vscode-wing/src/console/services/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface Client {
runAllTests: () => Promise<any>;
listResources: () => Promise<ExplorerItem>;
onInvalidateQuery: (options: SubscriptionOptions) => void;
onOpenFileInEditor: (options: SubscriptionOptions) => void;
close: () => void;
}

Expand Down Expand Up @@ -98,6 +99,13 @@ export const createClient = (host: string): Client => {
return client["app.invalidateQuery"].subscribe(undefined, options);
};

const onOpenFileInEditor = (options: SubscriptionOptions) => {
return client["app.openFileInEditorSubscription"].subscribe(
undefined,
options
);
};

const close = () => {
wsClient.close();
};
Expand All @@ -112,6 +120,7 @@ export const createClient = (host: string): Client => {
runAllTests,
listResources,
onInvalidateQuery,
onOpenFileInEditor,
close,
};
};
1 change: 1 addition & 0 deletions apps/wing-console/console/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type {
TestsStateManager,
TestStatus,
TestItem,
FileLink,
} from "./utils/createRouter.js";
export type { Trace, State } from "./types.js";
export type { LogInterface } from "./utils/LogInterface.js";
Expand Down
31 changes: 30 additions & 1 deletion apps/wing-console/console/server/src/router/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import {
NodeConnection,
ConstructTreeNodeMap,
} from "../utils/constructTreeNodeMap.js";
import { createProcedure, createRouter } from "../utils/createRouter.js";
import {
FileLink,
createProcedure,
createRouter,
} from "../utils/createRouter.js";
import {
isTermsAccepted,
acceptTerms,
Expand Down Expand Up @@ -561,6 +565,31 @@ export const createAppRouter = () => {
.mutation(async ({ ctx, input }) => {
await ctx.hostUtils?.openExternal(input.url);
}),
"app.openFileInEditor": createProcedure
.input(
z.object({
path: z.string(),
line: z.number().optional(),
column: z.number().optional(),
}),
)
.mutation(async ({ ctx, input }) => {
ctx.emitter.emit("openFileInEditor", {
path: input.path,
line: input.line,
column: input.column,
});
}),
"app.openFileInEditorSubscription": createProcedure.subscription(
({ ctx }) => {
return observable<FileLink>((emit) => {
ctx.emitter.on("openFileInEditor", emit.next);
return () => {
ctx.emitter.off("openFileInEditor", emit.next);
};
});
},
),
});

return { router };
Expand Down
7 changes: 7 additions & 0 deletions apps/wing-console/console/server/src/utils/createRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ export interface TestsStateManager {
setTest: (test: TestItem) => void;
}

export interface FileLink {
path: string;
line?: number;
column?: number;
}

export interface RouterContext {
simulator(): Promise<testing.Simulator>;
appDetails(): Promise<{
Expand All @@ -77,6 +83,7 @@ export interface RouterContext {
emitter: Emittery<{
invalidateQuery: string | undefined;
trace: Trace;
openFileInEditor: FileLink;
}>;
appState(): State;
logger: ConsoleLogger;
Expand Down
53 changes: 23 additions & 30 deletions apps/wing-console/console/ui/src/features/console-logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import classNames from "classnames";
import throttle from "lodash.throttle";
import { Fragment, useEffect, useRef, useState } from "react";

import {
OpenFileInEditorButton,
useFileLink,
} from "../shared/use-file-link.js";
import { createHtmlLink } from "../shared/use-file-link.js";

const dateTimeFormat = new Intl.DateTimeFormat(undefined, {
hour: "2-digit",
minute: "2-digit",
Expand All @@ -23,21 +29,6 @@ interface LogEntryProps {
showIcons?: boolean;
}

export const formatAbsolutePaths = (
error: string,
className: string,
expanded: boolean = false,
) => {
return error
.replaceAll(
/\B((?:[a-z]:)?[/\\]\S+):(\d+):(\d+)/gi,
(match, path, line, column) => {
return `<a class="${className}" onclick="event.stopPropagation()" href="vscode://file/${path}:${line}:${column}">${match}</a>`;
},
)
.replaceAll(/(\r\n|\n|\r)/gm, expanded ? "<br />" : "\n");
};

const LogEntryRow = ({
log,
showIcons = true,
Expand Down Expand Up @@ -77,7 +68,7 @@ const LogEntryRow = ({
if (expandableRef.current === null) {
return;
}
const html = formatAbsolutePaths(
const html = createHtmlLink(
log.message,
"text-sky-500 underline hover:text-sky-800",
expanded,
Expand Down Expand Up @@ -154,20 +145,22 @@ const LogEntryRow = ({
/>
</button>
)}
<span
className={classNames(
log.ctx?.messageType === "info" && theme.text2,
log.ctx?.messageType === "title" && theme.text1,
log.ctx?.messageType === "success" &&
"text-green-700 dark:text-green-500",
log.ctx?.messageType === "fail" && "text-red-500",
log.ctx?.messageType === "summary" && [
"font-medium",
theme.text1,
],
)}
ref={expandableRef}
/>
<OpenFileInEditorButton>
<span
className={classNames(
log.ctx?.messageType === "info" && theme.text2,
log.ctx?.messageType === "title" && theme.text1,
log.ctx?.messageType === "success" &&
"text-green-700 dark:text-green-500",
log.ctx?.messageType === "fail" && "text-red-500",
log.ctx?.messageType === "summary" && [
"font-medium",
theme.text1,
],
)}
ref={expandableRef}
/>
</OpenFileInEditorButton>
</div>

{onResourceClick && (
Expand Down
49 changes: 49 additions & 0 deletions apps/wing-console/console/ui/src/shared/use-file-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { PropsWithChildren } from "react";

import { trpc } from "../services/trpc.js";

export const createHtmlLink = (
error: string,
className: string,
expanded: boolean = false,
) => {
return error
.replaceAll(
/\B((?:[a-z]:)?[/\\]\S+):(\d+):(\d+)/gi,
(match, path, line, column) => {
const link = `vscode://file/${path}:${line}:${column}`;
return `<a class="${className}" href="${link}" path="${path}" line="${line}" column="${column}" >${match}</a>`;
},
)
.replaceAll(/(\r\n|\n|\r)/gm, expanded ? "<br />" : "\n");
};

export const OpenFileInEditorButton = ({ children }: PropsWithChildren) => {
const openFileInEditor = trpc["app.openFileInEditor"].useMutation();

return (
<button
className="appearance-none text-left"
onClick={(event) => {
const target = event.target as HTMLElement;
if (target.tagName === "A") {
const path = target.getAttribute("path")!;
const line = target.getAttribute("line")!;
const column = target.getAttribute("column")!;

if (!path || !line || !column) {
return;
}

openFileInEditor.mutate({
path,
line: Number.parseInt(line),
column: Number.parseInt(column),
});
}
}}
>
{children}
</button>
);
};
19 changes: 12 additions & 7 deletions apps/wing-console/console/ui/src/ui/blue-screen-of-death.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import classNames from "classnames";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";

import { formatAbsolutePaths } from "../features/console-logs.js";
import {
OpenFileInEditorButton,
createHtmlLink,
} from "../shared/use-file-link.js";

export const BlueScreenOfDeath = ({
title,
Expand All @@ -22,7 +25,7 @@ export const BlueScreenOfDeath = ({
return;
}
setFormattedPathsError(
formatAbsolutePaths(
createHtmlLink(
error,
"underline text-slate-300 hover:text-slate-400",
true,
Expand All @@ -46,10 +49,12 @@ export const BlueScreenOfDeath = ({
<div className="space-y-4">
<div>{title}</div>
<div className="py-4">
<span
className="outline-none select-text whitespace-pre-wrap"
dangerouslySetInnerHTML={{ __html: formattedPathsError }}
/>
<OpenFileInEditorButton>
<span
className="outline-none select-text whitespace-pre-wrap"
dangerouslySetInnerHTML={{ __html: formattedPathsError }}
/>
</OpenFileInEditorButton>
</div>
{displayLinks && (
<div className="w-full text-center py-4">
Expand Down

0 comments on commit 8fc4006

Please sign in to comment.