From 91496acb9a8a7c354385f4e8e8c285e501c754b7 Mon Sep 17 00:00:00 2001 From: Timothy Pratley Date: Wed, 25 Dec 2024 19:57:31 -0800 Subject: [PATCH 01/10] add initial idea for flares --- src/evaluate.ts | 7 ++++ src/flare-handler.ts | 64 ++++++++++++++++++++++++++++++++++ src/webview.ts | 82 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 src/flare-handler.ts create mode 100644 src/webview.ts diff --git a/src/evaluate.ts b/src/evaluate.ts index 7806aa6f7..2c2e05b5b 100644 --- a/src/evaluate.ts +++ b/src/evaluate.ts @@ -19,6 +19,7 @@ import * as output from './results-output/output'; import * as inspector from './providers/inspector'; import { resultAsComment } from './util/string-result'; import { highlight } from './highlight/src/extension'; +import * as flareHandler from './flare-handler'; let inspectorDataProvider: inspector.InspectorDataProvider; @@ -139,6 +140,8 @@ async function evaluateCodeUpdatingUI( result = value; + flareHandler.inspect(value); + if (showResult) { inspectorDataProvider.addItem(value, false, `[${session.replType}] ${ns}`); output.appendClojureEval(value, { ns, replSessionType: session.replType }, async () => { @@ -732,6 +735,10 @@ async function evaluateInOutputWindow(code: string, sessionType: string, ns: str } } +function inspect(value){ + +} + export default { interruptAllEvaluations, loadDocument, diff --git a/src/flare-handler.ts b/src/flare-handler.ts new file mode 100644 index 000000000..fcb2e6900 --- /dev/null +++ b/src/flare-handler.ts @@ -0,0 +1,64 @@ +import * as vscode from 'vscode'; +import * as webview from './webview'; + +let evalFn: (code: any) => void; +export function setEvalFn(fn: typeof evalFn) { + evalFn = fn; +} + +function isFlare(value: any): boolean { + return ( + typeof value === 'object' && + value !== null && + Object.keys(value).length === 1 && + value.hasOwnProperty('calva/flare') + ); +} + +function getFlareRequest(flare: Record): any { + return Object.values(flare)[0]; +} + +type InfoRequest = { type: 'info'; message: string; items?: string[]; then?: any }; +type WarnRequest = { type: 'warn'; message: string; items?: string[] }; +type ErrorRequest = { type: 'error'; message: string; items?: string[] }; +type WebviewRequest = { type: 'webview'; title?: string; html?: string; url?: string; key?: string; column?: vscode.ViewColumn; opts?: any}; +type DefaultRequest = { type: 'default'; [key: string]: any }; + +type ActRequest = InfoRequest | WarnRequest | ErrorRequest | WebviewRequest | DefaultRequest; + +const actHandlers: Record void> = { + default: (request: DefaultRequest) => { + vscode.window.showErrorMessage(`Unknown flare request type: ${JSON.stringify(request.type)}`); + }, + info: ({ message, items = [], then }: InfoRequest) => { + const p = vscode.window.showInformationMessage(message, ...items); + if (then) { + p.then((selected) => { + evalFn?.([['resolve', then], selected]); + }); + } + }, + warn: ({ message, items = [] }: WarnRequest) => { + vscode.window.showWarningMessage(message, ...items); + }, + error: ({ message, items = [] }: ErrorRequest) => { + vscode.window.showErrorMessage(message, ...items); + }, + webview: (request: WebviewRequest) => { + webview.show(request); + } +}; + +function act(request: ActRequest): void { + const handler = actHandlers[request.type] || actHandlers.default; + handler(request); +} + +export function inspect(x: any): any { + if (isFlare(x)) { + console.log('FLARE'); + act(getFlareRequest(x)); + } + return x; +} diff --git a/src/webview.ts b/src/webview.ts new file mode 100644 index 000000000..c6d3f1d67 --- /dev/null +++ b/src/webview.ts @@ -0,0 +1,82 @@ +import * as vscode from 'vscode'; + +const defaultOpts = { + enableScripts: true +}; + +function insertPanel(state: { webviews: Record }, key: string, panel: vscode.WebviewPanel): void { + state.webviews[key] = panel; +} + +function deleteWebviewPanel(state: { webviews: Record }, key: string): void { + delete state.webviews[key]; +} + +function selectWebviewPanel(state: { webviews: Record }, key: string): vscode.WebviewPanel | undefined { + return state.webviews[key]; +} + +function setHtml(panel: vscode.WebviewPanel, title: string, html: string): vscode.WebviewPanel { + if (panel.title !== title) { + panel.title = title; + } + if (panel.webview.html !== html) { + panel.webview.html = html; + } + panel.reveal(); + return panel; +} + +function urlInIframe(uri: string): string { + return ` + + + + + + + +`; +} + +export function show({ + title = "Webview", + html, + url, + key, + column = vscode.ViewColumn.Beside, + opts = defaultOpts +}: { + title?: string; + html?: string; + url?: string; + key?: string; + column?: vscode.ViewColumn; + opts?: typeof defaultOpts; +}): vscode.WebviewPanel { + const finalHtml = url ? urlInIframe(url) : html || ''; + const appState = db.getAppState(); + if (key) { + const existingPanel = selectWebviewPanel(appState, key); + if (existingPanel) { + return setHtml(existingPanel, title, finalHtml); + } + } + + const panel = vscode.window.createWebviewPanel("calva-webview", title, column, opts); + setHtml(panel, title, finalHtml); + + if (key) { + insertPanel(appState, key, panel); + panel.onDidDispose(() => deleteWebviewPanel(appState, key)); + } + + return panel; +} From 74e738df7af9a972ac1cc3b11574fbcb58e6b926 Mon Sep 17 00:00:00 2001 From: Timothy Pratley Date: Wed, 25 Dec 2024 20:04:52 -0800 Subject: [PATCH 02/10] update changelog --- CHANGELOG.md | 4 +++- src/evaluate.ts | 4 ---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b74cce7..e9fec793f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,11 @@ Changes to Calva. ## [Unreleased] +- [Add flare handler and webview](https://github.com/BetterThanTomorrow/calva/issues/2679) + ## [2.0.482] - 2024-12-03 -- Fix: [Added 'replace-refer-all-with-alias' & 'replace-refer-all-with-refer' actions to calva.](https://github.com/BetterThanTomorrow/calva/issues/2667) +- Fix: [Added 'replace-refer-all-with-alias' & 'replace-refer-all-with-refer' actions to calva.](https://github.com/BetterThanTomorrow/calva/issues/2667) ## [2.0.481] - 2024-10-29 diff --git a/src/evaluate.ts b/src/evaluate.ts index 2c2e05b5b..b060eb22c 100644 --- a/src/evaluate.ts +++ b/src/evaluate.ts @@ -735,10 +735,6 @@ async function evaluateInOutputWindow(code: string, sessionType: string, ns: str } } -function inspect(value){ - -} - export default { interruptAllEvaluations, loadDocument, From 41414aac5667009f4a5368b0f621510718743a01 Mon Sep 17 00:00:00 2001 From: Timothy Pratley Date: Wed, 25 Dec 2024 21:38:59 -0800 Subject: [PATCH 03/10] resolve cyclic dependency inject evaluate --- src/evaluate.ts | 2 +- src/flare-handler.ts | 29 ++++++++++++++--------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/evaluate.ts b/src/evaluate.ts index b060eb22c..b7250f0dc 100644 --- a/src/evaluate.ts +++ b/src/evaluate.ts @@ -140,7 +140,7 @@ async function evaluateCodeUpdatingUI( result = value; - flareHandler.inspect(value); + flareHandler.inspect(value, evaluateCodeUpdatingUI); if (showResult) { inspectorDataProvider.addItem(value, false, `[${session.replType}] ${ns}`); diff --git a/src/flare-handler.ts b/src/flare-handler.ts index fcb2e6900..164471c3e 100644 --- a/src/flare-handler.ts +++ b/src/flare-handler.ts @@ -1,11 +1,6 @@ import * as vscode from 'vscode'; import * as webview from './webview'; -let evalFn: (code: any) => void; -export function setEvalFn(fn: typeof evalFn) { - evalFn = fn; -} - function isFlare(value: any): boolean { return ( typeof value === 'object' && @@ -19,6 +14,12 @@ function getFlareRequest(flare: Record): any { return Object.values(flare)[0]; } +type EvaluateFunction = ( + code: string, + options: any, + selection?: vscode.Selection +) => Promise; + type InfoRequest = { type: 'info'; message: string; items?: string[]; then?: any }; type WarnRequest = { type: 'warn'; message: string; items?: string[] }; type ErrorRequest = { type: 'error'; message: string; items?: string[] }; @@ -27,16 +28,14 @@ type DefaultRequest = { type: 'default'; [key: string]: any }; type ActRequest = InfoRequest | WarnRequest | ErrorRequest | WebviewRequest | DefaultRequest; -const actHandlers: Record void> = { - default: (request: DefaultRequest) => { +const actHandlers: Record void> = { + default: (request: DefaultRequest, evaluate: EvaluateFunction) => { vscode.window.showErrorMessage(`Unknown flare request type: ${JSON.stringify(request.type)}`); }, - info: ({ message, items = [], then }: InfoRequest) => { + info: ({ message, items = [], then }: InfoRequest, evaluate: EvaluateFunction) => { const p = vscode.window.showInformationMessage(message, ...items); if (then) { - p.then((selected) => { - evalFn?.([['resolve', then], selected]); - }); + p.then((selected) => evaluate("(resolve " + then + " " + selected + ")", null)); } }, warn: ({ message, items = [] }: WarnRequest) => { @@ -50,15 +49,15 @@ const actHandlers: Record void> = { } }; -function act(request: ActRequest): void { +function act(request: ActRequest, evaluate: EvaluateFunction): void { const handler = actHandlers[request.type] || actHandlers.default; - handler(request); + handler(request, evaluate); } -export function inspect(x: any): any { +export function inspect(x: any, evaluate: EvaluateFunction): any { if (isFlare(x)) { console.log('FLARE'); - act(getFlareRequest(x)); + act(getFlareRequest(x), evaluate); } return x; } From ae17d076d3e902441d90efa581ded9d715ab948f Mon Sep 17 00:00:00 2001 From: Timothy Pratley Date: Thu, 26 Dec 2024 07:05:04 -0800 Subject: [PATCH 04/10] webview registry can just be an object --- src/flare-handler.ts | 14 +++++++++++--- src/webview.ts | 29 ++++++++++------------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/flare-handler.ts b/src/flare-handler.ts index 164471c3e..c5024d36d 100644 --- a/src/flare-handler.ts +++ b/src/flare-handler.ts @@ -23,7 +23,15 @@ type EvaluateFunction = ( type InfoRequest = { type: 'info'; message: string; items?: string[]; then?: any }; type WarnRequest = { type: 'warn'; message: string; items?: string[] }; type ErrorRequest = { type: 'error'; message: string; items?: string[] }; -type WebviewRequest = { type: 'webview'; title?: string; html?: string; url?: string; key?: string; column?: vscode.ViewColumn; opts?: any}; +type WebviewRequest = { + type: 'webview'; + title?: string; + html?: string; + url?: string; + key?: string; + column?: vscode.ViewColumn; + opts?: any; +}; type DefaultRequest = { type: 'default'; [key: string]: any }; type ActRequest = InfoRequest | WarnRequest | ErrorRequest | WebviewRequest | DefaultRequest; @@ -35,7 +43,7 @@ const actHandlers: Record voi info: ({ message, items = [], then }: InfoRequest, evaluate: EvaluateFunction) => { const p = vscode.window.showInformationMessage(message, ...items); if (then) { - p.then((selected) => evaluate("(resolve " + then + " " + selected + ")", null)); + p.then((selected) => evaluate('(resolve ' + then + ' ' + selected + ')', null)); } }, warn: ({ message, items = [] }: WarnRequest) => { @@ -46,7 +54,7 @@ const actHandlers: Record voi }, webview: (request: WebviewRequest) => { webview.show(request); - } + }, }; function act(request: ActRequest, evaluate: EvaluateFunction): void { diff --git a/src/webview.ts b/src/webview.ts index c6d3f1d67..2d5ef2951 100644 --- a/src/webview.ts +++ b/src/webview.ts @@ -1,20 +1,12 @@ import * as vscode from 'vscode'; const defaultOpts = { - enableScripts: true + enableScripts: true, }; -function insertPanel(state: { webviews: Record }, key: string, panel: vscode.WebviewPanel): void { - state.webviews[key] = panel; -} - -function deleteWebviewPanel(state: { webviews: Record }, key: string): void { - delete state.webviews[key]; -} - -function selectWebviewPanel(state: { webviews: Record }, key: string): vscode.WebviewPanel | undefined { - return state.webviews[key]; -} +// keep track of open webviews that have a key, +// so that they can be updated +const webviewRegistry: Record = {}; function setHtml(panel: vscode.WebviewPanel, title: string, html: string): vscode.WebviewPanel { if (panel.title !== title) { @@ -47,12 +39,12 @@ function urlInIframe(uri: string): string { } export function show({ - title = "Webview", + title = 'Webview', html, url, key, column = vscode.ViewColumn.Beside, - opts = defaultOpts + opts = defaultOpts, }: { title?: string; html?: string; @@ -62,20 +54,19 @@ export function show({ opts?: typeof defaultOpts; }): vscode.WebviewPanel { const finalHtml = url ? urlInIframe(url) : html || ''; - const appState = db.getAppState(); if (key) { - const existingPanel = selectWebviewPanel(appState, key); + const existingPanel = webviewRegistry[key]; if (existingPanel) { return setHtml(existingPanel, title, finalHtml); } } - const panel = vscode.window.createWebviewPanel("calva-webview", title, column, opts); + const panel = vscode.window.createWebviewPanel('calva-webview', title, column, opts); setHtml(panel, title, finalHtml); if (key) { - insertPanel(appState, key, panel); - panel.onDidDispose(() => deleteWebviewPanel(appState, key)); + webviewRegistry[key] = panel; + panel.onDidDispose(() => delete webviewRegistry[key]); } return panel; From 5587e7829f40acbee61cf3a6c88b8e9f06ee698b Mon Sep 17 00:00:00 2001 From: Timothy Pratley Date: Thu, 26 Dec 2024 07:23:31 -0800 Subject: [PATCH 05/10] address linter warning about promises --- src/flare-handler.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/flare-handler.ts b/src/flare-handler.ts index c5024d36d..33e8930b7 100644 --- a/src/flare-handler.ts +++ b/src/flare-handler.ts @@ -1,13 +1,8 @@ import * as vscode from 'vscode'; import * as webview from './webview'; -function isFlare(value: any): boolean { - return ( - typeof value === 'object' && - value !== null && - Object.keys(value).length === 1 && - value.hasOwnProperty('calva/flare') - ); +function isFlare(s: string): boolean { + return s.startsWith('{:calva/flare '); } function getFlareRequest(flare: Record): any { @@ -38,19 +33,21 @@ type ActRequest = InfoRequest | WarnRequest | ErrorRequest | WebviewRequest | De const actHandlers: Record void> = { default: (request: DefaultRequest, evaluate: EvaluateFunction) => { - vscode.window.showErrorMessage(`Unknown flare request type: ${JSON.stringify(request.type)}`); + void vscode.window.showErrorMessage( + `Unknown flare request type: ${JSON.stringify(request.type)}` + ); }, info: ({ message, items = [], then }: InfoRequest, evaluate: EvaluateFunction) => { const p = vscode.window.showInformationMessage(message, ...items); if (then) { - p.then((selected) => evaluate('(resolve ' + then + ' ' + selected + ')', null)); + void p.then((selected) => evaluate('(resolve ' + then + ' ' + selected + ')', null)); } }, warn: ({ message, items = [] }: WarnRequest) => { - vscode.window.showWarningMessage(message, ...items); + void vscode.window.showWarningMessage(message, ...items); }, error: ({ message, items = [] }: ErrorRequest) => { - vscode.window.showErrorMessage(message, ...items); + void vscode.window.showErrorMessage(message, ...items); }, webview: (request: WebviewRequest) => { webview.show(request); From 4e363e1cc4c1dc3913aac6e27dc4e9857a977eaf Mon Sep 17 00:00:00 2001 From: Timothy Pratley Date: Thu, 26 Dec 2024 08:17:10 -0800 Subject: [PATCH 06/10] try to parse the edn value --- package-lock.json | 64 +++++++++++++++++++++++++++++++++++++++++--- package.json | 1 + src/flare-handler.ts | 20 +++++++++----- 3 files changed, 75 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index c5c140294..568bbf75a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "get-port": "^5.1.1", "immutable": "3.8.1", "immutable-cursor": "2.0.1", + "jsedn": "^0.4.1", "jszip": "3.8.0", "lodash": "^4.17.19", "lodash.isequal": "4.5.0", @@ -3574,7 +3575,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, "engines": { "node": ">= 6" } @@ -4201,6 +4201,14 @@ "node": ">=4" } }, + "node_modules/equals": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/equals/-/equals-1.0.5.tgz", + "integrity": "sha512-wI15a6ZoaaXPv+55+Vh2Kqn3+efKRv8QPtcGTjW5xmanMnQzESdAt566jevtMZyt3W/jwLDTzXpMph5ECDJ2zg==", + "dependencies": { + "jkroso-type": "1" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -6817,6 +6825,11 @@ "node": ">= 10.13.0" } }, + "node_modules/jkroso-type": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jkroso-type/-/jkroso-type-1.1.1.tgz", + "integrity": "sha512-zZgay+fPG6PgMUrpyFADmQmvLo39+AZa7Gc5pZhev2RhDxwANEq2etwD8d0e6rTg5NkwOIlQmaEmns3draC6Ng==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6887,6 +6900,19 @@ } } }, + "node_modules/jsedn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/jsedn/-/jsedn-0.4.1.tgz", + "integrity": "sha512-ZzdF1smvJyiydoBJFYbXLx52wz6VIaAyDT4fGTmtdP6B7zrGCBzUI/0+QFIKoUmwgbHPSpzljdjkbHQHKWf8CQ==", + "dependencies": { + "commander": "latest", + "equals": "latest", + "type-component": "latest" + }, + "bin": { + "jsedn": "bin/jsedn" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -10289,6 +10315,11 @@ "node": ">= 0.8.0" } }, + "node_modules/type-component": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/type-component/-/type-component-0.0.1.tgz", + "integrity": "sha512-mDZRBQS2yZkwRQKfjJvQ8UIYJeBNNWCq+HBNstl9N5s9jZ4dkVYXEGkVPsSCEh5Ld4JM1kmrZTzjnrqSAIQ7dw==" + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -13859,8 +13890,7 @@ "commander": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" }, "concat-map": { "version": "0.0.1", @@ -14361,6 +14391,14 @@ "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", "dev": true }, + "equals": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/equals/-/equals-1.0.5.tgz", + "integrity": "sha512-wI15a6ZoaaXPv+55+Vh2Kqn3+efKRv8QPtcGTjW5xmanMnQzESdAt566jevtMZyt3W/jwLDTzXpMph5ECDJ2zg==", + "requires": { + "jkroso-type": "1" + } + }, "err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -16331,6 +16369,11 @@ "supports-color": "^8.0.0" } }, + "jkroso-type": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jkroso-type/-/jkroso-type-1.1.1.tgz", + "integrity": "sha512-zZgay+fPG6PgMUrpyFADmQmvLo39+AZa7Gc5pZhev2RhDxwANEq2etwD8d0e6rTg5NkwOIlQmaEmns3draC6Ng==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -16387,6 +16430,16 @@ "xml-name-validator": "^3.0.0" } }, + "jsedn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/jsedn/-/jsedn-0.4.1.tgz", + "integrity": "sha512-ZzdF1smvJyiydoBJFYbXLx52wz6VIaAyDT4fGTmtdP6B7zrGCBzUI/0+QFIKoUmwgbHPSpzljdjkbHQHKWf8CQ==", + "requires": { + "commander": "latest", + "equals": "latest", + "type-component": "latest" + } + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -19078,6 +19131,11 @@ "prelude-ls": "^1.2.1" } }, + "type-component": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/type-component/-/type-component-0.0.1.tgz", + "integrity": "sha512-mDZRBQS2yZkwRQKfjJvQ8UIYJeBNNWCq+HBNstl9N5s9jZ4dkVYXEGkVPsSCEh5Ld4JM1kmrZTzjnrqSAIQ7dw==" + }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", diff --git a/package.json b/package.json index feaa41918..cd1e3f8e8 100644 --- a/package.json +++ b/package.json @@ -3355,6 +3355,7 @@ "get-port": "^5.1.1", "immutable": "3.8.1", "immutable-cursor": "2.0.1", + "jsedn": "^0.4.1", "jszip": "3.8.0", "lodash": "^4.17.19", "lodash.isequal": "4.5.0", diff --git a/src/flare-handler.ts b/src/flare-handler.ts index 33e8930b7..2608e21d4 100644 --- a/src/flare-handler.ts +++ b/src/flare-handler.ts @@ -1,8 +1,9 @@ import * as vscode from 'vscode'; import * as webview from './webview'; +import * as jsedn from 'jsedn'; -function isFlare(s: string): boolean { - return s.startsWith('{:calva/flare '); +function isFlare(x: any): boolean { + return typeof x === 'object' && x !== null && 'calva/flare' in x; } function getFlareRequest(flare: Record): any { @@ -59,10 +60,15 @@ function act(request: ActRequest, evaluate: EvaluateFunction): void { handler(request, evaluate); } -export function inspect(x: any, evaluate: EvaluateFunction): any { - if (isFlare(x)) { - console.log('FLARE'); - act(getFlareRequest(x), evaluate); +export function inspect(edn: string, evaluate: EvaluateFunction): any { + console.log('INSPECT', edn); + if (edn) { + const x = jsedn.parse(edn); + console.log('PARSED', x); + if (isFlare(x)) { + console.log('FLARE'); + act(getFlareRequest(x), evaluate); + } + return x; } - return x; } From eaa885ee8f7817475897acbf178a02d91ccf6af7 Mon Sep 17 00:00:00 2001 From: Timothy Pratley Date: Thu, 26 Dec 2024 11:39:18 -0800 Subject: [PATCH 07/10] use cljs-lib for parseEdn --- package-lock.json | 64 +++----------------------------------------- package.json | 1 - src/flare-handler.ts | 40 ++++++++++++++++----------- 3 files changed, 27 insertions(+), 78 deletions(-) diff --git a/package-lock.json b/package-lock.json index 568bbf75a..c5c140294 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,6 @@ "get-port": "^5.1.1", "immutable": "3.8.1", "immutable-cursor": "2.0.1", - "jsedn": "^0.4.1", "jszip": "3.8.0", "lodash": "^4.17.19", "lodash.isequal": "4.5.0", @@ -3575,6 +3574,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, "engines": { "node": ">= 6" } @@ -4201,14 +4201,6 @@ "node": ">=4" } }, - "node_modules/equals": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/equals/-/equals-1.0.5.tgz", - "integrity": "sha512-wI15a6ZoaaXPv+55+Vh2Kqn3+efKRv8QPtcGTjW5xmanMnQzESdAt566jevtMZyt3W/jwLDTzXpMph5ECDJ2zg==", - "dependencies": { - "jkroso-type": "1" - } - }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -6825,11 +6817,6 @@ "node": ">= 10.13.0" } }, - "node_modules/jkroso-type": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/jkroso-type/-/jkroso-type-1.1.1.tgz", - "integrity": "sha512-zZgay+fPG6PgMUrpyFADmQmvLo39+AZa7Gc5pZhev2RhDxwANEq2etwD8d0e6rTg5NkwOIlQmaEmns3draC6Ng==" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6900,19 +6887,6 @@ } } }, - "node_modules/jsedn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/jsedn/-/jsedn-0.4.1.tgz", - "integrity": "sha512-ZzdF1smvJyiydoBJFYbXLx52wz6VIaAyDT4fGTmtdP6B7zrGCBzUI/0+QFIKoUmwgbHPSpzljdjkbHQHKWf8CQ==", - "dependencies": { - "commander": "latest", - "equals": "latest", - "type-component": "latest" - }, - "bin": { - "jsedn": "bin/jsedn" - } - }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -10315,11 +10289,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-component": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/type-component/-/type-component-0.0.1.tgz", - "integrity": "sha512-mDZRBQS2yZkwRQKfjJvQ8UIYJeBNNWCq+HBNstl9N5s9jZ4dkVYXEGkVPsSCEh5Ld4JM1kmrZTzjnrqSAIQ7dw==" - }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -13890,7 +13859,8 @@ "commander": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true }, "concat-map": { "version": "0.0.1", @@ -14391,14 +14361,6 @@ "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", "dev": true }, - "equals": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/equals/-/equals-1.0.5.tgz", - "integrity": "sha512-wI15a6ZoaaXPv+55+Vh2Kqn3+efKRv8QPtcGTjW5xmanMnQzESdAt566jevtMZyt3W/jwLDTzXpMph5ECDJ2zg==", - "requires": { - "jkroso-type": "1" - } - }, "err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -16369,11 +16331,6 @@ "supports-color": "^8.0.0" } }, - "jkroso-type": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/jkroso-type/-/jkroso-type-1.1.1.tgz", - "integrity": "sha512-zZgay+fPG6PgMUrpyFADmQmvLo39+AZa7Gc5pZhev2RhDxwANEq2etwD8d0e6rTg5NkwOIlQmaEmns3draC6Ng==" - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -16430,16 +16387,6 @@ "xml-name-validator": "^3.0.0" } }, - "jsedn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/jsedn/-/jsedn-0.4.1.tgz", - "integrity": "sha512-ZzdF1smvJyiydoBJFYbXLx52wz6VIaAyDT4fGTmtdP6B7zrGCBzUI/0+QFIKoUmwgbHPSpzljdjkbHQHKWf8CQ==", - "requires": { - "commander": "latest", - "equals": "latest", - "type-component": "latest" - } - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -19131,11 +19078,6 @@ "prelude-ls": "^1.2.1" } }, - "type-component": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/type-component/-/type-component-0.0.1.tgz", - "integrity": "sha512-mDZRBQS2yZkwRQKfjJvQ8UIYJeBNNWCq+HBNstl9N5s9jZ4dkVYXEGkVPsSCEh5Ld4JM1kmrZTzjnrqSAIQ7dw==" - }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", diff --git a/package.json b/package.json index cd1e3f8e8..feaa41918 100644 --- a/package.json +++ b/package.json @@ -3355,7 +3355,6 @@ "get-port": "^5.1.1", "immutable": "3.8.1", "immutable-cursor": "2.0.1", - "jsedn": "^0.4.1", "jszip": "3.8.0", "lodash": "^4.17.19", "lodash.isequal": "4.5.0", diff --git a/src/flare-handler.ts b/src/flare-handler.ts index 2608e21d4..a526d62eb 100644 --- a/src/flare-handler.ts +++ b/src/flare-handler.ts @@ -1,14 +1,6 @@ import * as vscode from 'vscode'; import * as webview from './webview'; -import * as jsedn from 'jsedn'; - -function isFlare(x: any): boolean { - return typeof x === 'object' && x !== null && 'calva/flare' in x; -} - -function getFlareRequest(flare: Record): any { - return Object.values(flare)[0]; -} +import { parseEdn } from '../out/cljs-lib/cljs-lib'; type EvaluateFunction = ( code: string, @@ -60,15 +52,31 @@ function act(request: ActRequest, evaluate: EvaluateFunction): void { handler(request, evaluate); } +function isFlare(x: any): boolean { + return typeof x === 'object' && x !== null && 'calva/flare' in x; +} + +function getFlareRequest(flare: Record): any { + return Object.values(flare)[0]; +} + export function inspect(edn: string, evaluate: EvaluateFunction): any { console.log('INSPECT', edn); - if (edn) { - const x = jsedn.parse(edn); - console.log('PARSED', x); - if (isFlare(x)) { - console.log('FLARE'); - act(getFlareRequest(x), evaluate); + if ( + edn && + typeof edn === 'string' && + (edn.startsWith('{:calva/flare') || edn.startsWith('#:calva{:flare')) + ) { + try { + const x = parseEdn(edn); + console.log('PARSED', x); + if (isFlare(x)) { + console.log('FLARE'); + act(getFlareRequest(x), evaluate); + } + return x; + } catch (e) { + console.log('ERROR: jsedn.parse failed: ' + e); } - return x; } } From 6605dc07ce212588ff4b74147c4ce17a62ff8e69 Mon Sep 17 00:00:00 2001 From: Timothy Pratley Date: Thu, 26 Dec 2024 21:11:13 -0800 Subject: [PATCH 08/10] handle commands --- src/evaluate.ts | 2 +- src/flare-handler.ts | 91 +++++++++++++++++++++++++++++++++----------- src/webview.ts | 2 + 3 files changed, 72 insertions(+), 23 deletions(-) diff --git a/src/evaluate.ts b/src/evaluate.ts index b7250f0dc..98f8015cc 100644 --- a/src/evaluate.ts +++ b/src/evaluate.ts @@ -140,7 +140,7 @@ async function evaluateCodeUpdatingUI( result = value; - flareHandler.inspect(value, evaluateCodeUpdatingUI); + flareHandler.inspect(value, (code) => evaluateCodeUpdatingUI(code, options, selection)); if (showResult) { inspectorDataProvider.addItem(value, false, `[${session.replType}] ${ns}`); diff --git a/src/flare-handler.ts b/src/flare-handler.ts index a526d62eb..299539b04 100644 --- a/src/flare-handler.ts +++ b/src/flare-handler.ts @@ -1,16 +1,13 @@ import * as vscode from 'vscode'; +import * as path from 'path'; import * as webview from './webview'; import { parseEdn } from '../out/cljs-lib/cljs-lib'; -type EvaluateFunction = ( - code: string, - options: any, - selection?: vscode.Selection -) => Promise; +type EvaluateFunction = (code: string) => Promise; -type InfoRequest = { type: 'info'; message: string; items?: string[]; then?: any }; -type WarnRequest = { type: 'warn'; message: string; items?: string[] }; -type ErrorRequest = { type: 'error'; message: string; items?: string[] }; +type InfoRequest = { type: 'info'; message: string; items?: string[]; then?: string }; +type WarnRequest = { type: 'warn'; message: string; items?: string[]; then?: string }; +type ErrorRequest = { type: 'error'; message: string; items?: string[]; then?: string }; type WebviewRequest = { type: 'webview'; title?: string; @@ -19,10 +16,44 @@ type WebviewRequest = { key?: string; column?: vscode.ViewColumn; opts?: any; + then?: string; }; +type CommandRequest = { type: 'command'; command: string; args?: string[]; then?: string }; +type CommandsRequest = { type: 'commands'; then?: string }; type DefaultRequest = { type: 'default'; [key: string]: any }; -type ActRequest = InfoRequest | WarnRequest | ErrorRequest | WebviewRequest | DefaultRequest; +type ActRequest = + | InfoRequest + | WarnRequest + | ErrorRequest + | WebviewRequest + | CommandRequest + | CommandsRequest + | DefaultRequest; + +function callback(p, thensym: string, evaluate: EvaluateFunction): void { + if (thensym) { + p.then((x: any) => evaluate(`((resolve '${thensym}) ${JSON.stringify(x)})`)).error((e) => { + // TODO: I haven't seen this work yet, test it somehow + vscode.window.showErrorMessage('failed callback: ' + e); + console.log('OH NO', e); + }); + } +} + +function parseArg(arg: string) { + try { + // Try to parse JSON (handles numbers, booleans, arrays, objects) + return JSON.parse(arg); + } catch { + // If parsing fails, check if it's a valid file path and convert to URI + if (path.isAbsolute(arg) || arg.startsWith('./') || arg.startsWith('../')) { + return vscode.Uri.file(arg); + } + // Return the argument as a string if it's not a valid file path + return arg; + } +} const actHandlers: Record void> = { default: (request: DefaultRequest, evaluate: EvaluateFunction) => { @@ -32,18 +63,34 @@ const actHandlers: Record voi }, info: ({ message, items = [], then }: InfoRequest, evaluate: EvaluateFunction) => { const p = vscode.window.showInformationMessage(message, ...items); - if (then) { - void p.then((selected) => evaluate('(resolve ' + then + ' ' + selected + ')', null)); - } + callback(p, then, evaluate); + }, + warn: ({ message, items = [], then }: WarnRequest, evaluate: EvaluateFunction) => { + const p = vscode.window.showWarningMessage(message, ...items); + callback(p, then, evaluate); + }, + error: ({ message, items = [], then }: ErrorRequest, evaluate: EvaluateFunction) => { + const p = vscode.window.showErrorMessage(message, ...items); + callback(p, then, evaluate); }, - warn: ({ message, items = [] }: WarnRequest) => { - void vscode.window.showWarningMessage(message, ...items); + webview: ({ then, ...request }: WebviewRequest, evaluate: EvaluateFunction) => { + const p = webview.show(request); + // TODO: p here is a panel, not a promise, so then clause will fail + callback(p, then, evaluate); }, - error: ({ message, items = [] }: ErrorRequest) => { - void vscode.window.showErrorMessage(message, ...items); + // TODO: handling commands sounds like fun, but there aren't actually many that are useful afaik, + // there are some that could be considered dangerous + // there are so many and they aren't documented anywhere afaik + command: ({ command, args = [], then }: CommandRequest, evaluate: EvaluateFunction) => { + // TODO: args might not be an array like we expect + const parsedArgs = (args || []).map(parseArg); + console.log('ARGS', parsedArgs); + const p = vscode.commands.executeCommand(command, ...parsedArgs); + callback(p, then, evaluate); }, - webview: (request: WebviewRequest) => { - webview.show(request); + commands: ({ then }: CommandsRequest, evaluate: EvaluateFunction) => { + const p = vscode.commands.getCommands(); + callback(p, then, evaluate); }, }; @@ -71,12 +118,12 @@ export function inspect(edn: string, evaluate: EvaluateFunction): any { const x = parseEdn(edn); console.log('PARSED', x); if (isFlare(x)) { - console.log('FLARE'); - act(getFlareRequest(x), evaluate); + console.log('FLARE', x); + const request = getFlareRequest(x); + act(request, evaluate); } - return x; } catch (e) { - console.log('ERROR: jsedn.parse failed: ' + e); + console.log('ERROR: inspect failed', e); } } } diff --git a/src/webview.ts b/src/webview.ts index 2d5ef2951..c6d0ac2f8 100644 --- a/src/webview.ts +++ b/src/webview.ts @@ -71,3 +71,5 @@ export function show({ return panel; } + +// TODO: register a command for creating a webview, because why not? From 7372c226e6c19a21a582412cd2bfb09105511aad Mon Sep 17 00:00:00 2001 From: Timothy Pratley Date: Fri, 27 Dec 2024 14:48:21 -0800 Subject: [PATCH 09/10] add documentation for flares --- docs/site/flares.md | 129 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 docs/site/flares.md diff --git a/docs/site/flares.md b/docs/site/flares.md new file mode 100644 index 000000000..e505cc3d8 --- /dev/null +++ b/docs/site/flares.md @@ -0,0 +1,129 @@ +--- +title: Calva Flares Documentation +description: Learn how to use Calva Flares to enhance your development experience. +--- + +# Calva Flares + +Flares are a mechanism in Calva that allow the REPL server (where your Clojure code runs) to send requests to the REPL client (your Calva IDE) to trigger specific behaviors. +They bridge the gap between user-space code and IDE features, enabling dynamic and interactive workflows. + +Flares are special values that, when encountered by the IDE, prompt it to perform predefined actions such as rendering HTML, showing notifications, or visualizing data. + +> **TIP:** +> Don't put flares in your project code. +> Flares are IDE specific, so they should be created by tooling code. +> Flares will be created when invoking a tool or custom action from your IDE. + +## How to Create Flares + +Flares take the form of a map with a single key-value pair. +The key specifies the flare is for Calva `:calva/flare`, while the value contains the details of the request. + +```clojure +{:calva/flare {:type :info + :message "Congratulations, you sent a flare!"}} +``` + +- **Key**: `:calva/flare` – Identifies this as a flare for Calva. +- **Value**: A map defining the specific request, such as showing a message, rendering HTML, or invoking an IDE command. + +Here’s a flare to display a HTML greeting: + +```clojure +{:calva/flare {:type :webview + :html "

Hello, Calva!

", + :title "Greeting"}} +``` + +## Typical Uses of Flares + +Flares enhance your development experience by enabling IDE features directly from user-space code. Below are common use cases: + +### 1. Data Visualization + +Used with tools like Clay, you can render HTML, SVG, or other visual elements directly in the IDE: + +```clojure +(calva.clay/webview $current-form $file) +``` + +Produces a flare: + +```clojure +{:calva/flare {:type :webview + :url "https://localhost:1971"}}} +``` + +Enabling you to create a custom action "Send to Clay" to visualize Kindly annotated visualizations. + +### 2. Notifications + +Test results or task completion: + +```clojure +{:calva/flare {:type :info + :message "Tests Passed 🎉"}}} +``` + +### 3. VSCode Commands + +Developers can define custom workflows or integrate with external tools: + +```clojure +{:calva/flare {:type :command + :command "workbench.action.toggleLightDarkThemes" }} +``` + +### 4. Debugging and Status Updates + +Send contextual data back to the IDE for live updates or inline annotations. + +## Why Use Flares? + +Flares enhance the feedback loop between your code and the IDE, reducing context switching and enabling a more interactive development experience. + +### Key Benefits + +- **Immediate Feedback**: See results, warnings, or visualizations inline as part of your workflow. +- **Custom Workflows**: Tailor IDE behavior to suit your needs using tools like Clay or by creating custom flares. +- **IDE-Specific Features**: Leverage the unique capabilities of Calva while maintaining the flexibility to extend or modify functionality. + +## Allowed Commands + +Calva supports a predefined set of flare actions and commands that are allowed. +If you want to access other commands, enable them in settings. + +Be mindful that flares are values, and values may originate from sources outside of your code. +For example if you read a value out of a logfile into a map, it could be a flare! + +If you want to experiment with new flare handlers, consider using Joyride to inject them. + +## Flare Reference + +All flares may have a `:then` in them which is a fully qualified symbol of a function to invoke with the result of the processed flare. + +| type | keys | +|------|-----| +| `:info` | `:message`, `items` | +| `:warn` | `:message`, `items` | +| `:error` | `:message`, `items` | +| `:webview` | `:title`, `:html`, `:url`, `:key` | +| `:command` | `:command`, `:args` | + +VSCode commands aren't comprehensively documented, you'll have to discover their ids and arguments with some guesswork and research. + +## Recap of how to use Flares + +To start using flares in your Calva environment, follow these steps: + +1. Ensure you have the latest version of Calva installed. +2. Open your Clojure project in Calva. +3. Connect to your REPL. +4. Use the provided examples to experiment with flares from the REPL. +5. Create custom user actions that trigger flares. +6. Request toolmakers provide flare producing actions. + +Flares enhance our development experience in Calva. +Whether you're visualizing data or creating custom workflows, they open up more possibilities for interactive development. +Let us know how you’re using flares, and share your feedback to make this feature even better. From fdc9e89fac781c07abf7638ae7565c332974e8c0 Mon Sep 17 00:00:00 2001 From: Timothy Pratley Date: Fri, 27 Dec 2024 14:53:53 -0800 Subject: [PATCH 10/10] add link to flare docs from nav --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index fc8688eff..5bd3ef1c8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,6 +64,7 @@ nav: - fiddle-files.md - connect-sequences.md - custom-commands.md + - flares.md - refactoring.md - notebooks.md - clojuredocs.md