Skip to content

Commit ae398de

Browse files
committed
added ability to open files
1 parent de17e3e commit ae398de

10 files changed

Lines changed: 200 additions & 83 deletions

File tree

vscode/bus/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "sqlmesh-extension-bus",
33
"private": true,
44
"version": "0.0.1",
5+
"type": "module",
56
"scripts": {
67
"build": "tsc",
78
"dev": "tsc -w",

vscode/bus/src/callbacks.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
1-
type Callback = {
1+
export type Callback = {
22
openFile: {
33
path: string
44
}
5-
}
5+
formatProject: {}
6+
}
7+
8+
/**
9+
* A tuple type representing a callback event with its associated payload.
10+
* The first element is the callback key (e.g., 'openFile', 'formatProject').
11+
* The second element is the payload type associated with that key.
12+
*
13+
* Example:
14+
* const openFileEvent: CallbackEvent<'openFile'> = ['openFile', { path: '/path/to/file' }];
15+
* const formatProjectEvent: CallbackEvent<'formatProject'> = ['formatProject', {}];
16+
*/
17+
export type CallbackEvent = {
18+
[K in keyof Callback]: { key: K; payload: Callback[K] }
19+
}[keyof Callback];
Lines changed: 130 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,152 @@
11
import {
2-
CancellationToken,
3-
commands,
4-
Disposable,
5-
TextEditor,
6-
Uri,
7-
WebviewView,
8-
WebviewViewProvider,
9-
WebviewViewResolveContext,
10-
window,
11-
} from "vscode"
12-
import { isProduction } from "../is_dev"
2+
CancellationToken,
3+
commands,
4+
Disposable,
5+
TextEditor,
6+
Uri,
7+
WebviewView,
8+
WebviewViewProvider,
9+
WebviewViewResolveContext,
10+
window,
11+
workspace,
12+
} from "vscode";
13+
import { isProduction } from "../is_dev";
14+
import { type CallbackEvent } from "@bus/callbacks";
15+
import { getWorkspaceFolders } from "../common/vscodeapi";
1316

14-
export class LineagePanel implements WebviewViewProvider, Disposable {
15-
public static readonly viewType = "sqlmesh.lineage"
16-
17-
private panel: WebviewView | undefined
18-
private getServerUrl: () => string
19-
private _extensionUri: Uri
20-
21-
public constructor(
22-
extensionUri: Uri,
23-
getServerUrl: () => string
24-
) {
25-
this._extensionUri = extensionUri
26-
this.getServerUrl = getServerUrl
27-
window.onDidChangeActiveTextEditor((event: TextEditor | undefined) => {
28-
if (this.panel) {
29-
this.panel.webview.html = this.getHtml()
30-
}
31-
})
32-
}
17+
export class LineagePanel implements WebviewViewProvider, Disposable {
18+
public static readonly viewType = "sqlmesh.lineage";
3319

34-
private getPanel() {
35-
return this.panel
36-
}
20+
private panel: WebviewView | undefined;
21+
private getServerUrl: () => string;
22+
private _extensionUri: Uri;
3723

38-
public resolveWebviewView(
39-
webviewView: WebviewView,
40-
_context: WebviewViewResolveContext,
41-
_token: CancellationToken,
42-
) {
43-
if (this.panel) {
44-
webviewView = this.panel
45-
}
46-
this.panel = webviewView
24+
public constructor(extensionUri: Uri, getServerUrl: () => string) {
25+
this._extensionUri = extensionUri;
26+
this.getServerUrl = getServerUrl;
27+
window.onDidChangeActiveTextEditor((event: TextEditor | undefined) => {
28+
if (this.panel) {
29+
const { externalUrl, externalAuthority } =
30+
this.externalUrlAndAutority();
31+
this.panel.webview.html = this.getHtml(externalUrl, externalAuthority);
32+
}
33+
});
34+
}
35+
36+
private getPanel() {
37+
return this.panel;
38+
}
39+
40+
public resolveWebviewView(
41+
webviewView: WebviewView,
42+
_context: WebviewViewResolveContext,
43+
_token: CancellationToken
44+
) {
45+
if (this.panel) {
46+
webviewView = this.panel;
47+
}
48+
this.panel = webviewView;
4749

48-
webviewView.webview.options = {
49-
// Allow scripts in the webview
50-
enableScripts: true,
51-
localResourceRoots: [
52-
this._extensionUri
53-
]
54-
}
50+
webviewView.webview.options = {
51+
// Allow scripts in the webview
52+
enableScripts: true,
53+
localResourceRoots: [this._extensionUri],
54+
};
5555

5656
// Set content options for external URL access
57-
const externalUrl = this.getServerUrl();
58-
const externalAuthority = new URL(externalUrl).origin;
59-
60-
webviewView.webview.html = this.getHtml()
61-
}
57+
// Set up message listener for events from the iframe
58+
webviewView.webview.onDidReceiveMessage(
59+
async (message) => {
60+
console.log("message received", message);
61+
if (message && message.key) {
62+
if (message.key === "vscode_callback") {
63+
const payload: CallbackEvent = message.payload;
64+
// Handle callback events from the iframe
65+
switch (payload.key) {
66+
case "openFile":
67+
console.log("opening file ", payload.payload.path);
68+
const workspaceFolders = getWorkspaceFolders();
69+
if (workspaceFolders.length != 1) {
70+
throw new Error("Only one workspace folder is supported");
71+
}
72+
const workspaceFolder = workspaceFolders[0];
73+
const fullPath = Uri.joinPath(
74+
workspaceFolder.uri,
75+
payload.payload.path
76+
);
77+
console.log("fullPath", fullPath);
78+
const document = await workspace.openTextDocument(fullPath);
79+
await window.showTextDocument(document);
80+
break;
81+
case "formatProject":
82+
console.log("formatProject", message.payload);
83+
break;
84+
default:
85+
console.log(`Unhandled message type in key: ${message.key}`);
86+
}
87+
} else {
88+
console.log("Unhandled message type: ", message);
89+
}
90+
}
91+
},
92+
undefined,
93+
[]
94+
);
95+
const { externalUrl, externalAuthority } = this.externalUrlAndAutority();
96+
webviewView.webview.html = this.getHtml(externalUrl, externalAuthority);
97+
}
6298

63-
getHtml() {
64-
65-
const isProd = isProduction()
66-
67-
const externalUrl = !isProd ? "http://localhost:5173/lineage" : this.getServerUrl() + "/lineage"
99+
externalUrlAndAutority(): { externalUrl: string; externalAuthority: string } {
100+
const isProd = isProduction();
101+
const externalUrl = !isProd
102+
? "http://localhost:5173/lineage"
103+
: this.getServerUrl() + "/lineage";
68104
const externalAuthority = new URL(externalUrl).origin;
69-
105+
return { externalUrl, externalAuthority };
106+
}
107+
108+
getHtml(externalUrl: string, externalAuthority: string) {
70109
// The CSP is too restrictive - it only allows frame-src but no other resources
71110
// Adding connect-src for API calls, img-src for images, and style-src for CSS
72111
return `
73112
<!DOCTYPE html>
74113
<html>
75114
<head>
115+
<script>
116+
// Listen for messages from the iframe and forward them to the extension
117+
window.addEventListener('message', (event) => {
118+
// Forward messages from the iframe to the extension
119+
const message = event.data;
120+
if (message && message.key) {
121+
// Post the message to the extension host
122+
vscode.postMessage(message);
123+
}
124+
});
125+
126+
// Define a function to handle events from the extension and send them to the iframe
127+
const vscode = acquireVsCodeApi();
128+
window.addEventListener('message', (event) => {
129+
// Check if the message is from the extension
130+
if (event.source === window && event.data) {
131+
// Forward the message to the iframe
132+
const iframe = document.querySelector('iframe');
133+
if (iframe && iframe.contentWindow) {
134+
iframe.contentWindow.postMessage(event.data, '*');
135+
}
136+
}
137+
});
138+
</script>
76139
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; frame-src ${externalAuthority}; connect-src ${externalAuthority}; img-src ${externalAuthority} data:; script-src 'unsafe-inline' ${externalAuthority}; style-src 'unsafe-inline' ${externalAuthority};">
77140
</head>
78141
<body>
79142
<iframe src="${externalUrl}" style="width:100%; height:100vh;" frameborder="0" allow="clipboard-read; clipboard-write"></iframe>
80143
</body>
81-
</html> `
144+
</html> `;
82145
}
83146

84-
85-
dispose() {
86-
// WebviewView doesn't have a dispose method
87-
// We can clear references
88-
this.panel = undefined;
89-
}
90-
}
147+
dispose() {
148+
// WebviewView doesn't have a dispose method
149+
// We can clear references
150+
this.panel = undefined;
151+
}
152+
}

vscode/extension/tsconfig.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@
1414
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
1515
// "noUnusedParameters": true, /* Report errors on unused parameters. */
1616
"paths": {
17-
"@sqlmesh/extension-bus": ["../bus/src"]
17+
"@bus/*": ["../bus/src/*"]
1818
}
1919
},
20+
"include": [
21+
"src/**/*",
22+
"../bus/src/**/*"
23+
],
2024
"exclude": [
2125
"node_modules", "../node_modules"
2226
]

vscode/extension/webpack.config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ const extensionConfig = {
2525
},
2626
resolve: {
2727
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
28-
extensions: ['.ts', '.js']
28+
extensions: ['.ts', '.js'],
29+
alias: {
30+
'@bus': path.resolve(__dirname, '../bus/src')
31+
}
2932
},
3033
module: {
3134
rules: [

vscode/react/src/components/graph/ModelNode.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,11 @@ export default function ModelNode({
8686
const handleClick = useCallback(
8787
(e: React.MouseEvent) => {
8888
e.stopPropagation()
89-
90-
handleClickModel?.(id)
89+
if (handleClickModel) {
90+
console.log('handleClickModel', id)
91+
console.log('handleClickModel', models)
92+
handleClickModel(id)
93+
}
9194
},
9295
[handleClickModel, id, data.isInteractive],
9396
)
@@ -141,11 +144,11 @@ export default function ModelNode({
141144
activeNodes.has(id) ||
142145
(withConnected && connectedNodes.has(id))
143146
: connectedNodes.has(id)
144-
const isInteractive =
145-
mainNode !== id &&
146-
isNotNil(handleClickModel) &&
147-
isFalse(isCTE) &&
148-
isFalse(isModelUnknown)
147+
const isInteractive = true
148+
// mainNode !== id &&
149+
// isNotNil(handleClickModel) &&
150+
// isFalse(isCTE) &&
151+
// isFalse(isModelUnknown)
149152
const shouldDisableColumns = isFalse(isModelSQL)
150153

151154
return (

vscode/react/src/hooks/vscode.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { Callback } from "@bus/callbacks";
2+
3+
export const useVSCode = (): <K extends keyof Callback>(callbackName: K, payload: Callback[K]) => void => {
4+
return (callbackName, payload) => {
5+
// Check if we're in an iframe, if so, we are in a webview
6+
const isIframe = window !== window.parent;
7+
8+
if (isIframe) {
9+
// Use a different variable name to avoid conflict with the parameter
10+
const eventPayload = {
11+
key: callbackName,
12+
payload: payload,
13+
}
14+
window.parent.postMessage({
15+
key: "vscode_callback",
16+
payload: eventPayload,
17+
}, '*');
18+
} else {
19+
console.log("vscode callback", callbackName, payload);
20+
}
21+
}
22+
}

vscode/react/src/routes/lineage.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { useApiModelLineage, useApiModels } from '@/api'
99
import LineageFlowProvider from '@/components/graph/context'
1010
import { ModelLineage } from '@/components/graph/ModelLineage'
11+
import { useVSCode } from '@/hooks/vscode'
1112

1213
export const Route = createFileRoute('/lineage')({
1314
component: Wrappper,
@@ -71,11 +72,16 @@ export function LineageComponentFromWeb({
7172
models,
7273
}: {
7374
selectedModel: string,
74-
models: Record<string, Model[]>;
75+
models: Record<string, Model>;
7576
}): JSX.Element {
76-
function handleClickModel(modelName: string): void {
77-
const model = models[modelName]
78-
console.log(model)
77+
const vscode = useVSCode()
78+
function handleClickModel(id: string): void {
79+
const decodedId = decodeURIComponent(id);
80+
const model = Object.values(models).find((m: Model) => m.fqn === decodedId);
81+
if (!model) {
82+
throw new Error('Model not found');
83+
}
84+
vscode('openFile', { path: model.path });
7985
}
8086

8187
function handleError(error: any): void {

vscode/react/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"baseUrl": ".",
2424
"paths": {
2525
"@/*": ["./src/*"],
26+
"@bus/*": ["../bus/src/*"]
2627
}
2728
}
2829
}

vscode/react/vite.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default defineConfig({
1414
resolve: {
1515
alias: {
1616
'@': resolve(__dirname, './src'),
17+
'@bus': resolve(__dirname, '../bus/src'),
1718
},
1819
},
1920
server: {

0 commit comments

Comments
 (0)