diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b74cce7..ede4ce249 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,11 @@ Changes to Calva. ## [Unreleased] +- [Added resultAsCommand and commandThen options for custom snippets](https://github.com/BetterThanTomorrow/calva/issues/2683) + ## [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/docs/site/commands-reference.md b/docs/site/commands-reference.md new file mode 100644 index 000000000..f9a16ad96 --- /dev/null +++ b/docs/site/commands-reference.md @@ -0,0 +1,244 @@ +--- +title: Calva Command Reference +description: All Calva commands. +--- + +# Calva Command Reference + +Calva's commands are part of the [Calva API](api.md). +They often accept arguments of some type. + +## JackIn + +| Command | Title | +| :------ | :---- | +| `calva.startJoyrideReplAndConnect` | Start Joyride REPL and Connect | +| `calva.copyJackInCommandToClipboard` | Copy Jack-In Command Line to Clipboard | +| `calva.jackIn` | Start a Project REPL and Connect (aka Jack-In) | +| `calva.jackOut` | Stop/Kill the Project REPL started by Calva (aka Jack-Out) | +| `calva.connect` | Connect to a Running REPL Server in the Project | +| `calva.connectNonProjectREPL` | Connect to a Running REPL Server, not in Project | +| `calva.disconnect` | Disconnect from the REPL Server | +| `calva.startStandaloneHelloRepl` | Create a Getting Started REPL project | +| `calva.revealJackInTerminal` | Reveal Jack-In Terminal | + +## Evaluate + +| Command | Title | +| :------ | :---- | +| `calva.togglePrettyPrint` | Toggle Pretty Printing for All Evaluations | +| `calva.clearInlineResults` | Clear Inline Evaluation Results | +| `calva.interruptAllEvaluations` | Interrupt Running Evaluations | +| `calva.evaluateSelection` | Evaluate Current Form (or selection, if any) | +| `calva.evaluateEnclosingForm` | Evaluate Current Enclosing Form | +| `calva.evaluateToCursor` | Evaluate From Start of List to Cursor, Closing Brackets | +| `calva.evaluateSelectionToSelectionEnd` | Evaluate Selection, Closing Brackets | +| `calva.evaluateTopLevelFormToCursor` | Evaluate From Start of Top Level Form to Cursor, Closing Brackets | +| `calva.tapSelection` | Tap Current Form (or selection, if any) | +| `calva.evaluateCurrentTopLevelForm` | Evaluate Top Level Form (defun) | +| `calva.tapCurrentTopLevelForm` | Tap Current Top Level Form | +| `calva.evaluateSelectionAsComment` | Evaluate Current Form to Comment | +| `calva.evaluateTopLevelFormAsComment` | Evaluate Top Level Form (defun) to Comment | +| `calva.copyLastResults` | Copy Last Evaluation Result to Clipboard | +| `calva.loadFile` | Load/Evaluate Current File and its Requires/Dependencies | +| `calva.refresh` | Refresh Changed Namespaces | +| `calva.refreshAll` | Refresh All Namespaces | +| `calva.evaluateFiddleForSourceFile` | Evaluate Fiddle File for Current File | + +## Test + +| Command | Title | +| :------ | :---- | +| `calva.evaluateStartOfFileToCursor` | Evaluate From Start of File to Cursor, Closing Brackets | +| `calva.runNamespaceTests` | Run Tests for Current Namespace | +| `calva.loadTestFileForCurrentNamespace` | Load/Evaluate Test File (as saved on disk) for Current Namespace | +| `calva.runAllTests` | Run All Tests | +| `calva.rerunTests` | Run Failing Tests Again | +| `calva.runTestUnderCursor` | Run Current Test | +| `calva.toggleBetweenImplAndTest` | Toggle between implementation and test | + +## Paredit + +| Command | Title | +| :------ | :---- | +| `paredit.togglemode` | Toggle Paredit Mode | +| `paredit.forwardSexp` | Move Cursor Forward Sexp/Form | +| `paredit.backwardSexp` | Move Cursor Backward Sexp/Form | +| `paredit.forwardSexpOrUp` | Move Cursor Forward or Up Sexp/Form | +| `paredit.backwardSexpOrUp` | Move Cursor Backward or Up Sexp/Form | +| `paredit.forwardDownSexp` | Move Cursor Forward Down Sexp/Form | +| `paredit.backwardDownSexp` | Move Cursor Backward Down Sexp/Form | +| `paredit.backwardUpSexp` | Move Cursor Backward Up Sexp/Form | +| `paredit.forwardUpSexp` | Move Cursor Forward Up Sexp/Form | +| `paredit.closeList` | Move Cursor Forward to List End/Close | +| `paredit.selectForwardSexp` | Select Forward Sexp | +| `paredit.selectRight` | Select Right | +| `paredit.selectBackwardSexp` | Select Backward Sexp | +| `paredit.selectForwardDownSexp` | Select Forward Down Sexp | +| `paredit.selectBackwardDownSexp` | Select Backward Down Sexp | +| `paredit.selectBackwardUpSexp` | Select Backward Up Sexp | +| `paredit.selectForwardUpSexp` | Select Forward Up Sexp | +| `paredit.selectBackwardSexpOrUp` | Select Backward Or Up Sexp | +| `paredit.selectForwardSexpOrUp` | Select Forward Or Up Sexp | +| `paredit.selectCloseList` | Select Forward to List End/Close | +| `paredit.selectOpenList` | Select Backward to List Start/Open | +| `paredit.rangeForDefun` | Select Current Top Level (aka defun) Form | +| `paredit.sexpRangeExpansion` | Expand Selection | +| `paredit.sexpRangeContraction` | Shrink Selection | +| `paredit.slurpSexpForward` | Slurp Sexp Forward | +| `paredit.slurpSexpBackward` | Slurp Sexp Backward | +| `paredit.barfSexpForward` | Barf Sexp Forward | +| `paredit.barfSexpBackward` | Barf Sexp Backward | +| `paredit.spliceSexp` | Splice Sexp | +| `paredit.splitSexp` | Split Sexp | +| `paredit.joinSexp` | Join Sexp | +| `paredit.raiseSexp` | Raise Sexp | +| `paredit.transpose` | Transpose (Swap) the two Sexps Around the Cursor | +| `paredit.dragSexprBackward` | Drag Sexp Backward | +| `paredit.dragSexprForward` | Drag Sexp Forward | +| `paredit.dragSexprBackwardUp` | Drag Sexp Backward Up | +| `paredit.dragSexprForwardDown` | Drag Sexp Forward Down | +| `paredit.dragSexprForwardUp` | Drag Sexp Forward Up | +| `paredit.dragSexprBackwardDown` | Drag Sexp Backward Down | +| `paredit.convolute` | Convolute Sexp ¯\_(ツ)_/¯ | +| `paredit.killRight` | Kill/Delete Right | +| `paredit.killLeft` | Kill/Delete Left | +| `paredit.killSexpForward` | Kill/Delete Sexp Forward | +| `paredit.killSexpBackward` | Kill/Delete Sexp Backward | +| `paredit.killListForward` | Kill/Delete Forward to End of List | +| `paredit.killListBackward` | Kill/Delete Backward to Start of List | +| `paredit.spliceSexpKillForward` | Splice & Kill/Delete Forward | +| `paredit.spliceSexpKillBackward` | Splice & Kill/Delete Backward | +| `paredit.deleteForward` | Delete Forward | +| `paredit.deleteBackward` | Delete Backward | +| `paredit.forceDeleteForward` | Force Delete Forward | +| `paredit.forceDeleteBackward` | Force Delete Backward | +| `paredit.wrapAroundParens` | Wrap Around () | +| `paredit.wrapAroundSquare` | Wrap Around [] | +| `paredit.wrapAroundCurly` | Wrap Around {} | +| `paredit.wrapAroundQuote` | Wrap Around "" | +| `paredit.rewrapParens` | Rewrap () | +| `paredit.rewrapSquare` | Rewrap [] | +| `paredit.rewrapCurly` | Rewrap {} | +| `paredit.rewrapSet` | Rewrap #{} | +| `paredit.rewrapQuote` | Rewrap "" | +| `paredit.addRichComment` | Add Rich Comment | + +## LSP + +| Command | Title | +| :------ | :---- | +| `calva.clojureLsp.showClojureLspMenu` | Show clojure-lsp menu | +| `calva.clojureLsp.download` | Download the configured Clojure LSP Server version | +| `calva.clojureLsp.start` | Start the Clojure LSP Server | +| `calva.clojureLsp.stop` | Stop the Clojure LSP Server | +| `calva.clojureLsp.restart` | Restart the Clojure LSP Server | +| `calva.clojureLsp.manage` | Manage Clojure LSP Servers | +| `calva.diagnostics.openClojureLspLogFile` | Open Clojure LSP Log File | +| `calva.diagnostics.showLspTraceLevelSettings` | Show LSP Trace Level Settings | +| `clojureLsp.refactor.cleanNs` | Clean NS Form | +| `clojureLsp.refactor.addMissingLibspec` | Add Missing Require | +| `clojureLsp.dragBackward` | Drag Sexp Backward | +| `clojureLsp.dragForward` | Drag Sexp Forward | +| `clojureLsp.refactor.cyclePrivacy` | Cycle/Toggle Privacy | +| `clojureLsp.refactor.expandLet` | Expand Let | +| `clojureLsp.refactor.inlineSymbol` | Inline Symbol | +| `clojureLsp.refactor.threadFirst` | Thread First | +| `clojureLsp.refactor.threadFirstAll` | Thread First All | +| `clojureLsp.refactor.threadLast` | Thread Last | +| `clojureLsp.refactor.threadLastAll` | Thread Last All | +| `clojureLsp.refactor.unwindAll` | Unwind All | +| `clojureLsp.refactor.unwindThread` | Unwind Thread | +| `clojureLsp.refactor.introduceLet` | Introduce let | +| `clojureLsp.refactor.moveToLet` | Move to Previous let Box | +| `clojureLsp.refactor.extractFunction` | Extract to New Function | +| `calva.diagnostics.clojureLspServerInfo` | Clojure-lsp Server Info | + +## Formatting + +| Command | Title | +| :------ | :---- | +| `calva-fmt.formatCurrentForm` | Format Current Form | +| `calva-fmt.alignCurrentForm` | Format and Align Current Form (recursively, experimental) | +| `calva-fmt.trimCurrentFormWhiteSpace` | Format Current Form and trim space between forms | +| `calva-fmt.inferParens` | Infer Parens (from the indentation) | +| `calva-fmt.tabIndent` | Indent Line | +| `calva-fmt.tabDedent` | Dedent Line | + +## Conversion + +| Command | Title | +| :------ | :---- | +| `calva.convertJs2Cljs` | Convert JavaScript code to ClojureScript | +| `calva.convertDart2Clj` | Convert Dart code to Clojure/ClojureDart | +| `calva.convertHtml2Hiccup` | Convert HTML code to Hiccup | +| `calva.pasteHtmlAsHiccup` | Paste HTML code as Hiccup | +| `calva.copyHtmlAsHiccup` | Copy HTML code as Hiccup | + +## Inspector + +| Command | Title | +| :------ | :---- | +| `calva.clearInspector` | Clear All Inspector Items | +| `calva.clearInspectorItem` | Clear Inspector Item | +| `calva.copyInspectorItem` | Copy Inspector Item | +| `calva.inspectItem` | Inspect Item | +| `calva.pasteAsInspectorItem` | Paste as Inspector Item | +| `calva.addToInspector` | Add Selection or Current Form to Inspector | +| `calva.revealInspector` | Reveal Inspector | + +## Show + +| Command | Title | +| :------ | :---- | +| `calva.info` | Show Information Message | +| `calva.warn` | Show Warning Message | +| `calva.error` | Show Error Message | +| `calva.webview` | Show Webview | + +## REPL + +| Command | Title | +| :------ | :---- | +| `calva.prettyPrintReplaceCurrentForm` | Replace Current Form (or Selection) with Pretty Printed Form | +| `calva.openUserConfigEdn` | Open REPL snippets User config.edn | +| `calva.rereadUserConfigEdn` | Refresh REPL snippets from User config.edn | +| `calva.diagnostics.toggleNreplLoggingEnabled` | Toggle nREPL Logging Enabled | +| `calva.toggleEvaluationSendCodeToOutputWindow` | Toggle also sending evaluated code to the REPL Window | +| `calva.showReplMenu` | Open the REPL Menu (Start/Connect a REPL, etc.) | +| `calva.toggleCLJCSession` | Toggle the REPL Connection (clj or cljs) used for CLJC Files | +| `calva.evaluateSelectionReplace` | Evaluate Current Form and Replace it with the Result | +| `calva.printLastStacktrace` | Print Last Stacktrace to REPL Window | +| `calva.requireREPLUtilities` | Require (refer) REPL utilities, like (doc) etcetera, into Current Namespace | +| `calva.runCustomREPLCommand` | Run Custom REPL Command | +| `calva.showOutputChannel` | Show/Open the Calva says Output Channel | +| `calva.showOutputTerminal` | Show/Open the Calva Output Terminal | +| `calva.showResultOutputDestination` | Show/Open the result output destination | +| `calva.showReplWindow` | Show/Open REPL Window | +| `calva.showFileForOutputWindowNS` | Show File for the Current REPL Window Namespace | +| `calva.setOutputWindowNamespace` | Switch Namespace in REPL Window to Current Namespace | +| `calva.sendCurrentFormToOutputWindow` | Send Current Form to REPL Window | +| `calva.sendCurrentTopLevelFormToOutputWindow` | Send Current Top Level Form to REPL Window | +| `calva.showPreviousReplHistoryEntry` | Show Previous REPL History Entry | +| `calva.showNextReplHistoryEntry` | Show Next REPL History Entry | +| `calva.clearReplHistory` | Clear REPL History | + +## Miscellaneous + +| Command | Title | +| :------ | :---- | +| `calva.activateCalva` | Activate the Calva Extension | +| `calva.diagnostics.printTextNotationFromDocument` | Print TextNotation from the current document to Calva says | +| `calva.diagnostics.createDocumentFromTextNotation` | Create a new Clojure Document from TextNotation | +| `calva.linting.resolveMacroAs` | Resolve Macro As | +| `calva.openCalvaDocs` | Open Documentation (calva.io) | +| `calva.debug.instrument` | Instrument Top Level Form for Debugging | +| `calva.createMinimalProject` | Create a mini Clojure project | +| `calva.continueComment` | Continue Comment (add a commented line below). | +| `calva.switchCljsBuild` | Select CLJS Build Connection | +| `calva.toggleKeybindingsEnabled` | Toggle Keybindings Enabled | +| `calva.selectCurrentForm` | Select Current Form | +| `calva.openFiddleForSourceFile` | Open Fiddle File for Current File | +| `calva.openSourceFileForFiddle` | Open Source File for Current Fiddle File | +| `calva.printClojureDocsToOutputWindow` | Print clojuredocs.org examples to OutputWindow | +| `calva.printClojureDocsToRichComment` | Print clojuredocs.org examples to Rich Comment | diff --git a/docs/site/commands.md b/docs/site/commands.md index cdb2e3606..68cfda12b 100644 --- a/docs/site/commands.md +++ b/docs/site/commands.md @@ -5,9 +5,6 @@ description: A list of all (well, not by far yet) Calva commands. A part of Calv # Calva Commands -!!! Note "This list is totally incomplete" - If you want to help the Calva project, one way to do so is to help making this list of commands complete. - Calva's commands are part of the [Calva API](api.md). They often accept arguments of some type, which you can use from keybindings and from [Joyride](https://github.com/BetterThanTomorrow/joyride) (or another VS Code extension). Well behaved commands return a Promise, if it is async. You can utilize this with Joyride too, or with keybindings involving [`runCommands`](https://blog.agical.se/en/posts/vs-code-runcommands-for-multi-commands-keyboard-shortcuts/). ## Example shortcut bindings @@ -44,16 +41,14 @@ Here's another way to achieve something similar. } ``` -## REPL commands - -Commands that establishes or needs a REPL connection. +## Commands with arguments | Command | Title | Arguments | Notes | | :------ | :---- | :-------- | :---- | | `calva.refresh` | Refreshes changed namespaces | A JSON object with stuff from [cider-nrepl ops/refresh](https://github.com/clojure-emacs/cider-nrepl/blob/master/doc/modules/ROOT/pages/nrepl-api/ops.adoc#refresh) | Mostly meant for sending `:dirs`, `:after`, and `:before`. The print options may or may not work. | `calva.refreshAll` | Refreshes changed namespaces | A JSON object with stuff from [cider-nrepl ops/refresh-aa](https://github.com/clojure-emacs/cider-nrepl/blob/master/doc/modules/ROOT/pages/nrepl-api/ops.adoc#refresh-all) | Mostly meant for sending `:dirs`, `:after`, and `:before`. The print options may or may not work. +Unfortunatley the arguments are not well documented, so you may need to poke around in the code to discover them. +If you search for the command, you should find it in package.json, and be able to navigate to the function definition to see the arguments. -## Wait, where are all the commands? - -Told you the list is incomplete... Please consider helping with making this a complete list! 🙏 \ No newline at end of file +See [Commands Reference](commands-reference.md) for the complete list of commands available. diff --git a/docs/site/custom-commands.md b/docs/site/custom-commands.md index 9692ef238..91fc0251a 100644 --- a/docs/site/custom-commands.md +++ b/docs/site/custom-commands.md @@ -23,6 +23,7 @@ The `calva.customREPLCommandSnippets` is an array of objects with the following * `ns`: A namespace to evaluate the command in. If omitted the command will be executed in the namespace of the current editor. * `repl`: Which repl session to use for the evaluation. Either `"clj"` or `"cljs"`. Omit if you want to use the session of the current editor. * `evaluationSendCodeToOutputWindow`: (default `true`) Whether the evaluated code should be echoed to the Output/REPL window. +* `command`: (optional) A VS Code command to invoke after the snippet completes, providing the snippet's result as arguments. Supply and empty array `[]` if the result should include the command itself. There are also substitutions available, which will take elements from the current state of Calva and splice them in to the text of your command before executing it. They are @@ -86,7 +87,6 @@ And these **Workspace** settings: "snippet": "(start)" } ], - ``` Issuing **Run Custom REPL Command** will then render a VS Code menu with all the commands, where the Workspace configured commands will be listed first. @@ -170,3 +170,23 @@ A new experimental feature lets library authors ship snippets inside their jar f {:name "edn hover show val" :snippet (str "### EDN show val\n```clojure\n" (pr-str (eval (symbol (str "$ns" "/" "$hover-top-level-defined-symbol")))) "\n```")}]} ``` + +## Running a command as the result of evaluation + +Supplying a `command` will cause the result of evaluation to be treated as a command. +`command` is an array signature of the command. +If empty then the result will be treated as the command. +If supplied then the result will be treated as additional arguments. + +```json + { + "name": "Eval Current Form as Command", + "key": "a", + "repl": "clj", + "snippet": "$current-form", + "command": [] + }, +``` + +It's useful to know that there is a "runCommands" command, +so you can also chain multiple command/snippets together. diff --git a/docs/site/finding-commands.md b/docs/site/finding-commands.md index 153276ca7..7d3837e39 100644 --- a/docs/site/finding-commands.md +++ b/docs/site/finding-commands.md @@ -26,6 +26,8 @@ Did you know? There is a complete list of Calva settings and commands in the *Co ![The Calva Contributions Tab](https://user-images.githubusercontent.com/30010/66733740-c754b800-ee60-11e9-877b-962f6b920cd7.png) +See also the [Commands Reference](commands-reference.md) + ## Toggling Keyboard Shortcuts On/Off The command `calva.toggleKeybindingsEnabled` can be used to quickly enable and disable (almost) all keyboard shortcuts. This allows you to quickly toggle between Calva keybindings and other keybindings which would otherwise not be available when Calva is enabled. This is particularly useful with the Paredit keyboard shortcuts, whose default shortcuts conflict with the default VS Code shortcuts for textual (non-structural) editing. diff --git a/mkdocs.yml b/mkdocs.yml index fc8688eff..03eeaeb4c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -108,6 +108,7 @@ nav: - API: - api.md - commands.md + - commands-reference.md - Guides: - get-started-with-clojure.md - jack-in-guide.md diff --git a/package.json b/package.json index feaa41918..525858e9d 100644 --- a/package.json +++ b/package.json @@ -2168,6 +2168,31 @@ "category": "Calva", "command": "calva.revealInspector", "title": "Reveal Inspector" + }, + { + "category": "Calva", + "command": "calva.revealJackInTerminal", + "title": "Reveal Jack-In Terminal" + }, + { + "category": "Calva", + "command": "calva.info", + "title": "Show Information Message" + }, + { + "category": "Calva", + "command": "calva.warn", + "title": "Show Warning Message" + }, + { + "category": "Calva", + "command": "calva.error", + "title": "Show Error Message" + }, + { + "category": "Calva", + "command": "calva.webview", + "title": "Show Webview" } ], "keybindings": [ @@ -3342,7 +3367,8 @@ "preprettier-format-watch": "npm run prettier-format", "eslint": "npx eslint . --ext .js,.jsx,.ts,.tsx", "eslint-watch": "npx esw . --ext .js,.jsx,.ts,.tsx --watch", - "install-ys": "curl -sS https://yamlscript.org/install | PREFIX=/tmp/yamlscript VERSION=0.1.56 bash" + "install-ys": "curl -sS https://yamlscript.org/install | PREFIX=/tmp/yamlscript VERSION=0.1.56 bash", + "generate-commands-md": "node ./scripts/generate-commands-md.js" }, "dependencies": { "@vscode/debugadapter": "^1.64.0", diff --git a/scripts/generate-commands-md.js b/scripts/generate-commands-md.js new file mode 100644 index 000000000..841be96fa --- /dev/null +++ b/scripts/generate-commands-md.js @@ -0,0 +1,95 @@ +const fs = require('fs'); +const path = require('path'); +const packageJson = require('../package.json'); + +const commands = packageJson.contributes.commands; + +const sections = { + Miscellaneous: [], + REPL: [], + Paredit: [], + LSP: [], + Formatting: [], + Test: [], + Evaluate: [], + JackIn: [], + Inspector: [], + Conversion: [], + Show: [], +}; + +commands.forEach((c) => { + if (c.command.startsWith('paredit.')) { + sections.Paredit.push(c); + } else if (c.command.includes('Lsp')) { + sections.LSP.push(c); + } else if (c.command.startsWith('calva-fmt')) { + sections.Formatting.push(c); + } else if ( + c.command.toLowerCase().includes('jack') || + c.command.includes('.connect') || + c.command.includes('.start') || + c.command.includes('.disconnect') + ) { + sections.JackIn.push(c); + } else if ( + c.command.toLowerCase().includes('repl') || + c.title.toLowerCase().includes('repl') || + c.command.includes('.show') + ) { + sections.REPL.push(c); + } else if (c.command.toLowerCase().includes('test') || c.title.toLowerCase().includes('test')) { + sections.Test.push(c); + } else if ( + c.command.toLowerCase().includes('evalu') || + c.title.toLowerCase().includes('evalu') || + c.command.includes('refresh') || + c.command.includes('.interrupt') || + c.command.includes('.tap') + ) { + sections.Evaluate.push(c); + } else if (c.command.toLowerCase().includes('inspect')) { + sections.Inspector.push(c); + } else if (c.command.includes('HtmlAsHiccup') || c.command.includes('convert')) { + sections.Conversion.push(c); + } else if (c.title.startsWith('Show')) { + sections.Show.push(c); + } else { + sections.Miscellaneous.push(c); + } +}); + +const generateSection = (title, commands) => { + return `## ${title} + +| Command | Title | +| :------ | :---- | +${commands.map((cmd) => `| \`${cmd.command}\` | ${cmd.title} |`).join('\n')} +`; +}; + +const content = `--- +title: Calva Command Reference +description: All Calva commands. +--- + +# Calva Command Reference + +Calva's commands are part of the [Calva API](api.md). +They often accept arguments of some type. + +${generateSection('JackIn', sections.JackIn)} +${generateSection('Evaluate', sections.Evaluate)} +${generateSection('Test', sections.Test)} +${generateSection('Paredit', sections.Paredit)} +${generateSection('LSP', sections.LSP)} +${generateSection('Formatting', sections.Formatting)} +${generateSection('Conversion', sections.Conversion)} +${generateSection('Inspector', sections.Inspector)} +${generateSection('Show', sections.Show)} +${generateSection('REPL', sections.REPL)} +${generateSection('Miscellaneous', sections.Miscellaneous)}`; + +const filename = '../docs/site/commands-reference.md'; +fs.writeFileSync(path.join(__dirname, filename), content); +console.log(filename + ' has been generated.'); diff --git a/src/config.ts b/src/config.ts index 2011ddca2..5b39d87b8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -161,6 +161,17 @@ watcher.onDidChange((uri: vscode.Uri) => { void updateCalvaConfigFromEdn(uri); }); +const exportsWatcher = vscode.workspace.createFileSystemWatcher( + '**/resources/calva.exports/config.edn', + false, + false, + false +); + +exportsWatcher.onDidChange((uri: vscode.Uri) => { + void updateCalvaConfigFromEdn(uri); +}); + // TODO find a way to validate the configs function getConfig() { const configOptions = vscode.workspace.getConfiguration('calva'); diff --git a/src/connector.ts b/src/connector.ts index af4abcb8d..41fb8dcf5 100644 --- a/src/connector.ts +++ b/src/connector.ts @@ -53,8 +53,17 @@ async function readRuntimeConfigs() { if (element.endsWith('.jar')) { const edn = await getJarContents(element.concat('!/calva.exports/config.edn')); return [element, edn]; + } else if (element.endsWith('/resources')) { + const configUri = vscode.Uri.file(element.concat('/calva.exports/config.edn')); + try { + await vscode.workspace.fs.stat(configUri); + const ednBytes = await vscode.workspace.fs.readFile(configUri); + const edn = new TextDecoder('utf-8').decode(ednBytes); + return [element, edn]; + } catch { + // no config found + } } - return [element, null]; }); const files = await Promise.all(configs); diff --git a/src/custom-snippets.ts b/src/custom-snippets.ts index 2e7498859..cf8472133 100644 --- a/src/custom-snippets.ts +++ b/src/custom-snippets.ts @@ -8,27 +8,34 @@ import { getConfig } from './config'; import * as replSession from './nrepl/repl-session'; import evaluate from './evaluate'; import * as state from './state'; -import { getStateValue } from '../out/cljs-lib/cljs-lib'; +import { getStateValue, parseEdn } from '../out/cljs-lib/cljs-lib'; import * as output from './results-output/output'; +// the structure of custom REPL command snippets as configured by the user export type CustomREPLCommandSnippet = { name: string; key?: string; snippet: string; repl?: string; ns?: string; + resultIsCommand?: boolean; + commandThen?: string; }; -type SnippetDefinition = { +// a snippet that is ready to be evaluated, with additional fields for internal use +export type SnippetDefinition = { snippet: string; ns: string; repl: string; evaluationSendCodeToOutputWindow?: boolean; + resultIsCommand?: boolean; + commandThen?: string; }; export function evaluateCustomCodeSnippetCommand(codeOrKeyOrSnippet?: string | SnippetDefinition) { evaluateCodeOrKeyOrSnippet(codeOrKeyOrSnippet).catch((err) => { console.log('Failed to run snippet', err); + void vscode.window.showErrorMessage(`Failed to run snippet: ${err.message}`); }); } @@ -51,6 +58,11 @@ async function evaluateCodeOrKeyOrSnippet(codeOrKeyOrSnippet?: string | SnippetD ? codeOrKeyOrSnippet : await getSnippetDefinition(codeOrKeyOrSnippet as string, editorNS, editorRepl); + if (!snippetDefinition) { + // user pressed ESC, not picking one + return undefined; + } + snippetDefinition.repl = snippetDefinition.repl ?? editorRepl; snippetDefinition.ns = snippetDefinition.ns ?? (editorRepl === snippetDefinition.repl ? editorNS : undefined); @@ -68,7 +80,26 @@ async function evaluateCodeOrKeyOrSnippet(codeOrKeyOrSnippet?: string | SnippetD : undefined; const context = makeContext(editor, snippetDefinition.ns, editorNS, snippetDefinition.repl); - await evaluateCodeInContext(editor, snippetDefinition.snippet, context, options); + const result = await evaluateCodeInContext(editor, snippetDefinition.snippet, context, options); + + if (typeof result === 'string' && snippetDefinition.resultIsCommand) { + const value = parseEdn(result); + if (!Array.isArray(value) || !value[0]) { + void vscode.window.showErrorMessage('Result is not a command array'); + return result; + } else { + const command = value[0]; + const args = value.slice(1).map(decodeArgument); + const commandResult = await vscode.commands.executeCommand(command, ...args); + if (snippetDefinition.commandThen) { + const code = `(${snippetDefinition.commandThen} ${commandResult})`; + return await evaluateCodeInContext(editor, code, context, options); + } + return commandResult; + } + } + + return result; } async function evaluateCodeInContext( @@ -112,6 +143,8 @@ async function getSnippetDefinition(codeOrKey: string, editorNS: string, editorR detail: `${entry.snippet}`, description: `${entry.repl}`, snippet: entry.snippet, + resultIsCommand: entry.resultIsCommand, + commandThen: entry.commandThen, }; snippetsMenuItems.push(item); if (!snippetsDict[entry.key]) { @@ -229,3 +262,134 @@ function interpolateCode(editor: vscode.TextEditor, code: string, context): stri .replace(/\$hover-tail/g, context.hovertail?.[1] ?? ''); } } + +function resolveRelativePath(path: string): string { + if (path.startsWith('/')) { + return path; + } + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders.length > 0) { + const workspacePath = workspaceFolders[0].uri.fsPath; + return vscode.Uri.file(`${workspacePath}/${path}`).fsPath; + } + return path; +} + +function resolveFilePath(path: string): vscode.Uri { + return vscode.Uri.file(resolveRelativePath(path)); +} + +const decoders = { + File: resolveFilePath, + Uri: vscode.Uri.parse, +}; + +function decodeArgument(arg: any): any { + if (Array.isArray(arg) && arg.length > 1) { + if (decoders[arg[0]]) { + const Decoder = decoders[arg[0]]; + const decodedArgs = arg.slice(1).map(decodeArgument); + return Decoder(...decodedArgs); + } + + const parts = arg[0].split('.'); + if (parts.length > 0) { + let vscodeType: any = vscode; + for (const part of parts) { + vscodeType = vscodeType[part]; + if (!vscodeType) { + break; + } + } + if (typeof vscodeType === 'function') { + const decodedArgs = arg.slice(1).map(decodeArgument); + if (vscodeType.prototype) { + return new vscodeType(...decodedArgs); + } else { + return vscodeType(...decodedArgs); + } + } + } + } + return arg; +} + +const defaultWebviewOptions = { + enableScripts: true, +}; + +interface CalvaWebPanel extends vscode.WebviewPanel { + url?: string; +} + +// keep track of open webviews that have a key +// so that they can be updated in the future +const calvaWebPanels: Record = {}; + +export function show({ + title = 'Webview', + key, + html, + url, + reload = false, + reveal = true, + column = vscode.ViewColumn.Beside, + opts = defaultWebviewOptions, +}: { + title?: string; + key?: string; + html?: string; + url?: string; + reload?: boolean; + reveal?: boolean; + column?: vscode.ViewColumn; + opts?: typeof defaultWebviewOptions; +}): void { + let panel: CalvaWebPanel; + if (key) { + panel = calvaWebPanels[key]; + } + if (!panel) { + panel = vscode.window.createWebviewPanel('calva-webview', title, column, opts); + if (key) { + calvaWebPanels[key] = panel; + panel.onDidDispose(() => delete calvaWebPanels[key]); + } + } + + if (html && panel.webview.html != html) { + panel.webview.html = html; + } + + if (url && (url != panel.url || reload)) { + panel.url = url; + panel.webview.html = urlInIframe(url); + } + + if (panel.title !== title) { + panel.title = title; + } + + if (reveal) { + panel.reveal(); + } +} + +function urlInIframe(uri: string): string { + return ` + + + + + + + +`; +} diff --git a/src/extension.ts b/src/extension.ts index 368eb0f93..60e13554f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -369,6 +369,10 @@ async function activate(context: vscode.ExtensionContext) { return inspectorTreeView.reveal(selectedItem, { select, focus, expand }); }, revealJackInTerminal: jackIn.revealJackInTerminal, + info: vscode.window.showInformationMessage, + warn: vscode.window.showWarningMessage, + error: vscode.window.showErrorMessage, + webview: snippets.show, }; function registerCalvaCommand([command, callback]) {