├── .github ├── CODE_OF_CONDUCT.md └── workflows │ └── release.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── icon │ └── qmd.svg ├── logo │ ├── quarto.png │ └── quarto.svg ├── walkthrough │ ├── empty.md │ ├── quarto-cli-dark.png │ └── quarto-cli-light.png └── www │ ├── assist │ ├── assist.css │ └── assist.js │ ├── codicon │ ├── codicon.css │ └── codicon.ttf │ ├── diagram │ ├── d3-graphviz.js │ ├── d3.v5.min.js │ ├── diagram.css │ ├── diagram.js │ ├── graphviz.min.js │ ├── graphvizlib.wasm │ ├── lodash.min.js │ └── mermaid.min.js │ └── preview │ ├── icon.png │ ├── index.js │ ├── main.css │ ├── preview-dark.svg │ └── preview-light.svg ├── language-configuration.json ├── languages ├── README.md ├── dot │ ├── dot.configuration.json │ ├── snippets │ │ ├── dot.json │ │ ├── principalEdgeAttributes.json │ │ ├── principalGraphAttributes.json │ │ └── principalNodeAttributes.json │ └── syntaxes │ │ └── dot.tmLanguage └── mermaid │ └── mermaid.tmLanguage.json ├── package.json ├── server ├── package.json ├── src │ ├── core │ │ ├── biblio.ts │ │ ├── config.ts │ │ ├── doc.ts │ │ ├── markdown │ │ │ └── markdown.ts │ │ ├── mathjax.ts │ │ └── refs.ts │ ├── providers │ │ ├── completion │ │ │ ├── completion-attrs.ts │ │ │ ├── completion-latex.ts │ │ │ ├── completion-yaml.ts │ │ │ ├── completion.ts │ │ │ ├── mathjax-completions.json │ │ │ ├── mathjax.json │ │ │ └── refs │ │ │ │ ├── completion-biblio.ts │ │ │ │ ├── completion-crossref.ts │ │ │ │ └── completion-refs.ts │ │ ├── diagnostics.ts │ │ ├── hover │ │ │ ├── hover-math.ts │ │ │ ├── hover-ref.ts │ │ │ ├── hover-yaml.ts │ │ │ └── hover.ts │ │ └── signature.ts │ ├── quarto │ │ ├── quarto-attr.ts │ │ ├── quarto-yaml.ts │ │ └── quarto.ts │ ├── server.ts │ └── shared │ │ ├── appdirs.ts │ │ ├── exec.ts │ │ ├── markdownit-math.ts │ │ ├── markdownit-yaml.ts │ │ ├── metadata.ts │ │ ├── path.ts │ │ ├── quarto.ts │ │ ├── storage.ts │ │ └── strings.ts ├── tsconfig.json ├── types │ └── mathjax-full │ │ └── index.d.ts └── yarn.lock ├── snippets └── quarto.code-snippets ├── src ├── browser.ts ├── core │ ├── command.ts │ ├── dispose.ts │ ├── doc.ts │ ├── git.ts │ ├── lazy.ts │ ├── links.ts │ ├── mime.ts │ ├── path.ts │ ├── platform.ts │ ├── png.ts │ ├── quarto.ts │ ├── schemes.ts │ ├── text.ts │ ├── wait.ts │ └── yaml.ts ├── extension.ts ├── lsp │ └── client.ts ├── main.ts ├── markdown │ ├── auto-id.ts │ ├── document.ts │ ├── engine.ts │ ├── language.ts │ └── toc.ts ├── providers │ ├── assist │ │ ├── codelens.ts │ │ ├── commands.ts │ │ ├── panel.ts │ │ ├── render-assist.ts │ │ ├── render-cache.ts │ │ └── webview.ts │ ├── background.ts │ ├── cell │ │ ├── codelens.ts │ │ ├── commands.ts │ │ ├── executors.ts │ │ └── options.ts │ ├── completion-path.ts │ ├── create │ │ ├── create-extension.ts │ │ ├── create-project.ts │ │ ├── create.ts │ │ ├── directory.ts │ │ └── firstrun.ts │ ├── diagram │ │ ├── codelens.ts │ │ ├── commands.ts │ │ ├── diagram-webview.ts │ │ └── diagram.ts │ ├── folding.ts │ ├── format.ts │ ├── hover-image.ts │ ├── insert.ts │ ├── link.ts │ ├── lua-types.ts │ ├── newdoc.ts │ ├── option.ts │ ├── paste.ts │ ├── preview │ │ ├── commands.ts │ │ ├── preview-env.ts │ │ ├── preview-errors.ts │ │ ├── preview-output.ts │ │ ├── preview-reveal.ts │ │ ├── preview-util.ts │ │ ├── preview-webview.ts │ │ └── preview.ts │ ├── selection-range.ts │ ├── statusbar.ts │ ├── symbol-document.ts │ ├── symbol-workspace.ts │ ├── walkthrough.ts │ └── webview.ts ├── shared │ ├── appdirs.ts │ ├── exec.ts │ ├── markdownit-math.ts │ ├── markdownit-yaml.ts │ ├── metadata.ts │ ├── path.ts │ ├── quarto.ts │ ├── storage.ts │ └── strings.ts └── vdoc │ ├── languages.ts │ ├── vdoc-content.ts │ ├── vdoc-tempfile.ts │ └── vdoc.ts ├── syntaxes ├── build-lang.js ├── quarto.tmLanguage └── quarto.tmLanguage.yaml ├── tsconfig.json ├── tsconfig.webpack.json ├── visx └── quarto-1.57.0.vsix ├── webpack.config.js └── yarn.lock /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: main 4 | 5 | name: Release 6 | 7 | jobs: 8 | create_release: 9 | runs-on: ubuntu-latest 10 | if: startsWith(github.event.head_commit.message, 'release') 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set version from commit message 14 | id: tag_id 15 | env: 16 | VAR: ${{ github.event.head_commit.message }} 17 | run: | 18 | TAG="${VAR#* }" 19 | echo ::set-output name=tag::"${TAG}" 20 | echo "${TAG}" 21 | - name: Set release name 22 | id: release_id 23 | run: | 24 | echo ::set-output name=releasename::"${{ steps.tag_id.outputs.tag }}" 25 | - name: Bump version and push tag 26 | id: tag_version 27 | uses: mathieudutour/github-tag-action@v6.0 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | default_bump: false 31 | custom_tag: ${{ steps.tag_id.outputs.tag }} 32 | tag_prefix: "" 33 | - name: Create Release 34 | uses: ncipollo/release-action@v1 35 | with: 36 | tag: ${{ steps.tag_version.outputs.new_tag }} 37 | name: ${{ steps.release_id.outputs.releasename }} 38 | generateReleaseNotes: true 39 | allowUpdates: false 40 | token: ${{ secrets.GITHUB_TOKEN }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | Thumbs.db 4 | node_modules/ 5 | .build/ 6 | dist/ 7 | out/ 8 | out-build/ 9 | out-editor/ 10 | out-editor-min/ 11 | out-monaco-editor-core/ 12 | out-vscode/ 13 | out-vscode-min/ 14 | build/node_modules 15 | coverage/ 16 | test_data/ 17 | yarn-error.log 18 | .vscode-test 19 | .vscode-test-web 20 | 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 10 | "stopOnEntry": false 11 | }, 12 | { 13 | "type": "node", 14 | "request": "attach", 15 | "name": "Attach to Server", 16 | "port": 6009, 17 | "restart": true, 18 | "outFiles": ["${workspaceRoot}/server/out/**/*.js"] 19 | }, 20 | { 21 | "type": "pwa-extensionHost", 22 | "name": "Run Web Extension in VS Code", 23 | "debugWebWorkerHost": true, 24 | "request": "launch", 25 | "args": [ 26 | "--extensionDevelopmentPath=${workspaceFolder}", 27 | "--extensionDevelopmentKind=web" 28 | ], 29 | "outFiles": ["${workspaceFolder}/dist/web/**/*.js"], 30 | "preLaunchTask": "npm: watch-web" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[html]": { 3 | "editor.formatOnSave": false 4 | }, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.formatOnSave": true, 7 | "editor.tabSize": 2, 8 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "watch", 7 | "problemMatcher": "$tsc-watch", 8 | "isBackground": true, 9 | "presentation": { 10 | "reveal": "always", 11 | "revealProblems": "onProblem" 12 | }, 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | } 17 | }, 18 | { 19 | "type": "npm", 20 | "script": "watch-web", 21 | "group": "build", 22 | "isBackground": true, 23 | "problemMatcher": ["$ts-webpack-watch"] 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .github/** 3 | .vscode/** 4 | .vscode-test/** 5 | test/** 6 | src/** 7 | browser/** 8 | visx/** 9 | node_modules/** 10 | **/*.map 11 | **/*.ts 12 | syntaxes/*.yaml 13 | syntaxes/*.js 14 | tsconfig.json 15 | cgmanifest.json 16 | server/src/** 17 | server/node_modules/** 18 | server/*.json 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) RStudio 4 | Copyright (c) Microsoft Corporation 5 | Copyright (c) James Yu 6 | Copyright (c) Waylon Flinn 7 | Copyright (c) Chris Bain 8 | Copyright (c) Matt Bierner 9 | Copyright (c) Takashi Tamura 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # quarto-vscode 2 | 3 | This repository has been archived. 4 | 5 | The code for the Quarto Extension for VS Code code now lives here: 6 | 7 | -------------------------------------------------------------------------------- /assets/icon/qmd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/logo/quarto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quarto-dev/quarto-vscode/53f3621d4e3753ffd6a43d15fcdb0947d767dfe1/assets/logo/quarto.png -------------------------------------------------------------------------------- /assets/logo/quarto.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /assets/walkthrough/empty.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/walkthrough/quarto-cli-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quarto-dev/quarto-vscode/53f3621d4e3753ffd6a43d15fcdb0947d767dfe1/assets/walkthrough/quarto-cli-dark.png -------------------------------------------------------------------------------- /assets/walkthrough/quarto-cli-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quarto-dev/quarto-vscode/53f3621d4e3753ffd6a43d15fcdb0947d767dfe1/assets/walkthrough/quarto-cli-light.png -------------------------------------------------------------------------------- /assets/www/assist/assist.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | (function () { 4 | // @ts-ignore 5 | const vscode = acquireVsCodeApi(); 6 | 7 | const main = document.getElementById("main"); 8 | 9 | // Handle messages sent from the extension to the webview 10 | let contentShown = false; 11 | window.addEventListener("message", (event) => { 12 | const message = event.data; // The json data that the extension sent 13 | switch (message.type) { 14 | case "update": { 15 | updateContent(message.body); 16 | contentShown = true; 17 | break; 18 | } 19 | case "noContent": { 20 | if (!contentShown) { 21 | setNoContent(message.body); 22 | } else if (message.updateMode === "live") { 23 | setNoContent(""); 24 | } 25 | break; 26 | } 27 | } 28 | }); 29 | 30 | /** 31 | * @param {string} contents 32 | */ 33 | function updateContent(contents) { 34 | main.innerHTML = contents; 35 | window.scrollTo(0, 0); 36 | } 37 | 38 | /** 39 | * @param {string} message 40 | */ 41 | function setNoContent(message) { 42 | main.innerHTML = `

${message}

`; 43 | } 44 | })(); 45 | -------------------------------------------------------------------------------- /assets/www/codicon/codicon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quarto-dev/quarto-vscode/53f3621d4e3753ffd6a43d15fcdb0947d767dfe1/assets/www/codicon/codicon.ttf -------------------------------------------------------------------------------- /assets/www/diagram/diagram.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: var(--vscode-editor-background); 3 | } 4 | 5 | body.vscode-light { 6 | color: rgb(113, 113, 113); 7 | } 8 | 9 | body.vscode-dark { 10 | color: rgb(146, 146, 146); 11 | } 12 | 13 | body.with-preview { 14 | background-color: white; 15 | } 16 | 17 | body.with-preview.vscode-light { 18 | color: rgb(113, 113, 113); 19 | } 20 | 21 | body.with-preview.vscode-dark { 22 | color: rgb(146, 146, 146); 23 | } 24 | 25 | .diagram-preview { 26 | width: 100%; 27 | height: 100%; 28 | padding: 8px; 29 | } 30 | 31 | .diagram-preview svg { 32 | width: 100%; 33 | height: 100%; 34 | } 35 | 36 | body .diagram-preview { 37 | display: none; 38 | } 39 | 40 | body.with-preview #no-preview { 41 | display: none; 42 | } 43 | 44 | body.with-preview.mermaid #mermaid-preview { 45 | display: block; 46 | } 47 | 48 | body.with-preview.graphviz #graphviz-preview { 49 | display: block; 50 | } 51 | 52 | #preview-error { 53 | margin-top: 1em; 54 | padding: 1em; 55 | color: black; 56 | background-color: #ffccd7; 57 | border-color: #ffccd7; 58 | border-radius: 5px; 59 | } 60 | 61 | #preview-error-message { 62 | margin: 0; 63 | } 64 | 65 | .hidden { 66 | display: none; 67 | } 68 | -------------------------------------------------------------------------------- /assets/www/diagram/diagram.js: -------------------------------------------------------------------------------- 1 | //@js-check 2 | 3 | (function () { 4 | const vscode = acquireVsCodeApi(); 5 | 6 | const reportError = _.debounce((message) => { 7 | const previewErrorMsg = document.getElementById("preview-error-message"); 8 | previewErrorMsg.innerText = message; 9 | document.getElementById("preview-error").classList.remove("hidden"); 10 | }, 2000); 11 | 12 | function clearError() { 13 | reportError.cancel(); 14 | document.getElementById("preview-error").classList.add("hidden"); 15 | } 16 | 17 | // clear preview 18 | function clearPreview() { 19 | document.body.classList.remove("with-preview"); 20 | document.body.classList.remove("mermaid"); 21 | document.body.classList.remove("graphviz"); 22 | const noPreview = document.createElement("p"); 23 | noPreview.innerText = "No diagram currently selected"; 24 | const previewDiv = document.querySelector("#no-preview"); 25 | previewDiv.appendChild(noPreview); 26 | } 27 | 28 | function updateMermaidPreview(src) { 29 | document.body.classList.add("with-preview"); 30 | document.body.classList.add("mermaid"); 31 | document.body.classList.remove("graphviz"); 32 | 33 | // validate first 34 | try { 35 | window.mermaid.parse(src); 36 | } catch (err) { 37 | reportError(err.str); 38 | return; 39 | } 40 | 41 | // render 42 | const kMermaidId = "mermaidSvg"; 43 | mermaidApi.render(kMermaidId, src, () => { 44 | const mermaidEl = document.querySelector(`#${kMermaidId}`); 45 | const previewDiv = document.querySelector("#mermaid-preview"); 46 | while (previewDiv.firstChild) { 47 | previewDiv.removeChild(previewDiv.firstChild); 48 | } 49 | previewDiv.appendChild(mermaidEl); 50 | clearError(); 51 | }); 52 | } 53 | 54 | function updateGraphvizPreview(graphviz, dot) { 55 | document.body.classList.add("with-preview"); 56 | document.body.classList.add("graphviz"); 57 | document.body.classList.remove("mermaid"); 58 | graphviz.renderDot(dot); 59 | } 60 | 61 | // always start with no preview 62 | clearPreview(); 63 | 64 | // initialize mermaid 65 | const mermaidApi = window.mermaid.mermaidAPI; 66 | mermaidApi.initialize({ startOnLoad: false }); 67 | 68 | // initialize graphvix 69 | const hpccWasm = window["@hpcc-js/wasm"]; 70 | hpccWasm.graphvizSync().then(() => { 71 | const graphviz = d3 72 | .select("#graphviz-preview") 73 | .graphviz({ zoom: false, fit: true }) 74 | .transition(function () { 75 | return d3.transition("main"); 76 | }) 77 | .on("initEnd", () => { 78 | vscode.postMessage({ type: "initialized" }); 79 | }); 80 | 81 | // error handling 82 | graphviz.onerror(reportError); 83 | graphviz.on("layoutEnd", clearError); 84 | 85 | // remember the last message and skip processing if its identical 86 | // to the current message (e.g. would happen on selection change) 87 | let lastMessage = undefined; 88 | 89 | // handle messages sent from the extension to the webview 90 | window.addEventListener("message", (event) => { 91 | // get the message 92 | const message = event.data; 93 | 94 | // skip if its the same as the last message 95 | if ( 96 | lastMessage && 97 | lastMessage.type === message.type && 98 | lastMessage.engine === message.engine && 99 | lastMessage.src === message.src 100 | ) { 101 | return; 102 | } 103 | 104 | // set last message 105 | lastMessage = message; 106 | 107 | // handle the message 108 | if (message.type === "render") { 109 | vscode.postMessage({ type: "render-begin" }); 110 | try { 111 | switch (message.engine) { 112 | case "mermaid": { 113 | updateMermaidPreview(message.src); 114 | break; 115 | } 116 | case "graphviz": { 117 | updateGraphvizPreview(graphviz, message.src); 118 | break; 119 | } 120 | } 121 | } finally { 122 | vscode.postMessage({ type: "render-end" }); 123 | } 124 | } else if (message.type === "clear") { 125 | clearPreview(); 126 | } 127 | }); 128 | }); 129 | })(); 130 | -------------------------------------------------------------------------------- /assets/www/diagram/graphvizlib.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quarto-dev/quarto-vscode/53f3621d4e3753ffd6a43d15fcdb0947d767dfe1/assets/www/diagram/graphvizlib.wasm -------------------------------------------------------------------------------- /assets/www/preview/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quarto-dev/quarto-vscode/53f3621d4e3753ffd6a43d15fcdb0947d767dfe1/assets/www/preview/icon.png -------------------------------------------------------------------------------- /assets/www/preview/main.css: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | :root { 7 | --container-paddding: 20px; 8 | --input-padding-vertical: 2px; 9 | --input-padding-horizontal: 4px; 10 | --input-margin-vertical: 4px; 11 | --input-margin-horizontal: 0; 12 | } 13 | 14 | html, 15 | body { 16 | height: 100%; 17 | min-height: 100%; 18 | padding: 0; 19 | margin: 0; 20 | } 21 | 22 | body { 23 | display: grid; 24 | grid-template-rows: auto 1fr; 25 | } 26 | 27 | input:not([type="checkbox"]), 28 | textarea { 29 | display: block; 30 | width: 100%; 31 | border: none; 32 | margin-right: 0.3em; 33 | font-family: var(--vscode-font-family); 34 | padding: var(--input-padding-vertical) var(--input-padding-horizontal); 35 | color: var(--vscode-input-foreground); 36 | outline-color: var(--vscode-input-border); 37 | background-color: var(--vscode-input-background); 38 | } 39 | 40 | input::placeholder, 41 | textarea::placeholder { 42 | color: var(--vscode-input-placeholderForeground); 43 | } 44 | 45 | button { 46 | border: none; 47 | padding: 3px; 48 | text-align: center; 49 | outline: 1px solid transparent; 50 | color: var(--vscode-icon-foreground); 51 | background: none; 52 | border-radius: 5px; 53 | } 54 | 55 | button:hover:not(:disabled) { 56 | cursor: pointer; 57 | color: var(--vscode-toolbar-hoverForeground); 58 | background: var(--vscode-toolbar-hoverBackground); 59 | } 60 | 61 | button:disabled { 62 | opacity: 0.5; 63 | } 64 | 65 | input:focus, 66 | button:focus { 67 | outline-color: var(--vscode-focusBorder); 68 | } 69 | 70 | .header { 71 | display: flex; 72 | margin: 0.4em 1em; 73 | } 74 | 75 | .url-input { 76 | flex: 1; 77 | } 78 | 79 | .controls { 80 | display: flex; 81 | } 82 | 83 | .controls button { 84 | display: flex; 85 | margin-right: 0.3em; 86 | } 87 | 88 | .content { 89 | width: 100%; 90 | height: 100%; 91 | display: flex; 92 | justify-content: center; 93 | } 94 | 95 | iframe { 96 | width: 100%; 97 | height: 100%; 98 | border: none; 99 | background: var(--vscode-editor-background); 100 | } 101 | 102 | .iframe-focused-alert { 103 | display: none; 104 | position: absolute; 105 | bottom: 1em; 106 | background: var(--vscode-editorWidget-background); 107 | color: var(--vscode-editorWidget-foreground); 108 | padding: 0.2em 0.2em; 109 | border-radius: 4px; 110 | 111 | font-size: 8px; 112 | font-family: monospace; 113 | user-select: none; 114 | pointer-events: none; 115 | } 116 | 117 | .iframe-focused.enable-focus-lock-indicator .iframe-focused-alert { 118 | display: none; 119 | } 120 | -------------------------------------------------------------------------------- /assets/www/preview/preview-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/www/preview/preview-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | // symbols used for start and end a block comment. Remove this entry if your language does not support block comments 4 | "blockComment": [""] 5 | }, 6 | // symbols used as brackets 7 | "brackets": [ 8 | ["{", "}"], 9 | ["[", "]"], 10 | ["(", ")"] 11 | ], 12 | "colorizedBracketPairs": [], 13 | "autoClosingPairs": [ 14 | { 15 | "open": "{", 16 | "close": "}" 17 | }, 18 | { 19 | "open": "[", 20 | "close": "]" 21 | }, 22 | { 23 | "open": "(", 24 | "close": ")" 25 | }, 26 | { 27 | "open": "\"", 28 | "close": "\"", 29 | "notIn": ["string"] 30 | }, 31 | { 32 | "open": "'", 33 | "close": "'", 34 | "notIn": ["string"] 35 | }, 36 | { 37 | "open": "{{< ", 38 | "close": " >" 39 | } 40 | ], 41 | "surroundingPairs": [ 42 | ["(", ")"], 43 | ["[", "]"], 44 | ["`", "`"], 45 | ["_", "_"], 46 | ["*", "*"], 47 | ["{", "}"], 48 | ["'", "'"], 49 | ["\"", "\""], 50 | ["$", "$"] 51 | ], 52 | "folding": { 53 | "offSide": true, 54 | "markers": { 55 | "start": "^\\s*", 56 | "end": "^\\s*" 57 | } 58 | }, 59 | "wordPattern": { 60 | "pattern": "(\\p{Alphabetic}|\\p{Number}|\\p{Nonspacing_Mark})(((\\p{Alphabetic}|\\p{Number}|\\p{Nonspacing_Mark})|[_])?(\\p{Alphabetic}|\\p{Number}|\\p{Nonspacing_Mark}))*", 61 | "flags": "ug" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /languages/README.md: -------------------------------------------------------------------------------- 1 | We've incorporated code from several other extensions to provide features for LaTeX, Mermaid, and Graphviz: 2 | 3 | - LaTeX code completion, preview, and language support from https://github.com/James-Yu/LaTeX-Workshop 4 | 5 | - Graphviz language support from https://github.com/Stephanvs/vscode-graphviz 6 | 7 | - Mermaid language support from https://github.com/bpruitt-goddard/vscode-mermaid-syntax-highlight 8 | (fork maintained at https://github.com/quarto-dev/vscode-mermaid-syntax-highlight) 9 | -------------------------------------------------------------------------------- /languages/dot/dot.configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "//", 4 | "blockComment": [ 5 | "/*", 6 | "*/" 7 | ] 8 | }, 9 | "brackets": [ 10 | [ 11 | "{", 12 | "}" 13 | ], 14 | [ 15 | "[", 16 | "]" 17 | ], 18 | [ 19 | "(", 20 | ")" 21 | ] 22 | ] 23 | } -------------------------------------------------------------------------------- /languages/dot/snippets/dot.json: -------------------------------------------------------------------------------- 1 | { 2 | "Graph Template": { 3 | "prefix": "graph", 4 | "body": [ 5 | "digraph ${name:G} {", 6 | "\tsplines=\"FALSE\";", 7 | "", 8 | "\t/* Entities */", 9 | "\t${1:shortName} [label=\"${2:$1}\", shape=\"${3|square,rectangle,circle,ellipse,triangle,plaintext,point,diamond,pentagon,hexagon,septagon,octagon,egg,trapezium,parallelogram,house,doublecircle,doubleoctagon,tripleoctagon,invtriangle,invtrapezium,invhouse,Mdiamond,Msquare,Mcircle,none,note,tab,folder,box3d|}\"${4:, URL=\"${5:http://en.wikipedia.org/wiki/John de Fries}\"}]", 10 | "\t${0}", 11 | "\t/* Relationships */", 12 | "\t${6:F1} -> $1${7:[label=\"${8:.63}\"]}", 13 | "", 14 | "\t/* Ranks */", 15 | "\t{ rank=${9:|same,min,max,# max is bottom|}; $1; };", 16 | "}" 17 | ], 18 | "description": "Graph Template" 19 | }, 20 | "Convert > to ->": { 21 | "prefix": ">", 22 | "body": [ 23 | "-> " 24 | ], 25 | "description": "-> (convert \">\" to \"->\")" 26 | }, 27 | "New Variable": { 28 | "prefix": "var", 29 | "body": [ 30 | "${1:shortname} [label=\"${2:$1}\", shape=\"${3|square,rectangle,circle,ellipse,triangle,plaintext,point,diamond,pentagon,hexagon,septagon,octagon,egg,trapezium,parallelogram,house,doublecircle,doubleoctagon,tripleoctagon,invtriangle,invtrapezium,invhouse,Mdiamond,Msquare,Mcircle,none,note,tab,folder,box3d|}\"${4:, URL=\"${5:http://en.wikipedia.org/wiki/John de Fries}\"}]", 31 | "${0}" 32 | ], 33 | "description": "New variable" 34 | }, 35 | "New variable [plaintext]": { 36 | "prefix": "var", 37 | "body": [ 38 | "\"${1:Machine: a}\" [ shape = plaintext ];" 39 | ], 40 | "description": "New variable [plaintext]" 41 | }, 42 | "Property [styles…]": { 43 | "prefix": "prop", 44 | "body": [ 45 | "[style=dotted; color=red; style=bold,label=\"100 times\"; weight=8]" 46 | ], 47 | "description": "Property [styles…]" 48 | }, 49 | "Path from -> to [label]": { 50 | "prefix": "path", 51 | "body": [ 52 | "${1:from} -> ${2:to} [label=\"${3:.7}\";]" 53 | ], 54 | "description": "Path from -> to [label]" 55 | }, 56 | "Path from -> {to list}": { 57 | "prefix": "path", 58 | "body": [ 59 | "${1:From} -> {${2:item1} ${3:item2} $0}" 60 | ], 61 | "description": "Path from -> {to list}" 62 | }, 63 | "{ rank=same|min|max; x; y }": { 64 | "prefix": "rank", 65 | "body": [ 66 | "{ rank=${1|same,min,max,# max is bottom|}; ${2:space delimitted list }};" 67 | ], 68 | "description": "{rank=same|min|max; x; y}" 69 | }, 70 | "Subgraph template": { 71 | "prefix": "subgraph", 72 | "body": [ 73 | "subgraph ${1:cluster0}", 74 | "\t${2:node [style=filled color=white]}", 75 | "}" 76 | ], 77 | "description": "subgraph template" 78 | }, 79 | "Attribute label=": { 80 | "prefix": "label=", 81 | "body": "label=\"$1\"" 82 | }, 83 | "Attribute label=table": { 84 | "prefix": "label=table", 85 | "body": [ 86 | "label=<", 87 | "\t", 88 | "\t\t", 89 | "\t\t\t", 92 | "\t\t", 93 | "\t
", 90 | "\t\t\t\t$2", 91 | "\t\t\t
", 94 | ">" 95 | ] 96 | }, 97 | "Attribute style=...": { 98 | "prefix": "style", 99 | "body": "style=\"${1|solid,dashed,dotted,bold,invis|}\"", 100 | "description": "This attribute is a comma-separated list of primitives with optional argument list" 101 | }, 102 | "Attribute dir=...": { 103 | "prefix": "dir", 104 | "body": "dir=${1|back,forward,both,none|}" 105 | }, 106 | "Attribute shape=...": { 107 | "prefix": "shape", 108 | "body": "shape=${1|square,rectangle,circle,ellipse,triangle,plaintext,point,diamond,pentagon,hexagon,septagon,octagon,egg,trapezium,parallelogram,house,doublecircle,doubleoctagon,tripleoctagon,invtriangle,invtrapezium,invhouse,Mdiamond,Msquare,Mcircle,none,note,tab,folder,box3d|}" 109 | }, 110 | "Attribute shape=record": { 111 | "prefix": "shape=record", 112 | "body": "shape=record label=\"<${1:f0}> $2${3:|}${4}\"" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quarto-lsp-server", 3 | "description": "Quarto LSP", 4 | "version": "1.0.0", 5 | "author": "quarto", 6 | "license": "MIT", 7 | "engines": { 8 | "node": "*" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/quarto-dev/quarto-vscode/" 13 | }, 14 | "dependencies": { 15 | "file-url": "^4.0.0", 16 | "js-yaml": "^4.1.0", 17 | "markdown-it": "^12.3.2", 18 | "mathjax-full": "^3.2.0", 19 | "uuid": "^8.3.2", 20 | "vscode-languageserver": "^7.0.0", 21 | "vscode-languageserver-textdocument": "^1.0.4", 22 | "vscode-uri": "^3.0.3" 23 | }, 24 | "scripts": {}, 25 | "devDependencies": { 26 | "@types/js-yaml": "^4.0.5", 27 | "@types/markdown-it": "^12.2.3", 28 | "esbuild": "^0.16.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/src/core/config.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import type { SupportedExtension } from "mathjax-full"; 7 | 8 | export class ExtensionConfig { 9 | public quartoPath(): string { 10 | return this.quartoPath_; 11 | } 12 | 13 | public mathJaxExtensions(): SupportedExtension[] { 14 | return this.mathJaxExtensions_; 15 | } 16 | 17 | public mathJaxScale(): number { 18 | return this.mathJaxScale_; 19 | } 20 | 21 | public mathJaxTheme(): "light" | "dark" { 22 | return this.mathJaxTheme_; 23 | } 24 | 25 | public update(configuration: Record) { 26 | this.quartoPath_ = configuration?.path || this.quartoPath_; 27 | this.mathJaxExtensions_ = 28 | configuration?.mathjax?.extensions || this.mathJaxExtensions_; 29 | this.mathJaxScale_ = configuration?.mathjax?.scale || this.mathJaxScale_; 30 | this.mathJaxTheme_ = configuration?.mathjax?.theme || this.mathJaxTheme_; 31 | } 32 | 33 | private quartoPath_: string = ""; 34 | private mathJaxExtensions_: SupportedExtension[] = []; 35 | private mathJaxScale_: number = 1; 36 | private mathJaxTheme_: "light" | "dark" = "dark"; 37 | } 38 | 39 | export const config = new ExtensionConfig(); 40 | -------------------------------------------------------------------------------- /server/src/core/doc.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import { URI } from "vscode-uri"; 7 | 8 | import { TextDocument } from "vscode-languageserver-textdocument"; 9 | 10 | export const kQuartoLanguageId = "quarto"; 11 | export const kMarkdownLanguageId = "markdown"; 12 | export const kYamlLanguageId = "yaml"; 13 | 14 | export enum DocType { 15 | None, 16 | Qmd, 17 | Yaml, 18 | } 19 | 20 | export function docType(doc: TextDocument) { 21 | if (isQuartoDoc(doc)) { 22 | return DocType.Qmd; 23 | } else if (isQuartoYaml(doc)) { 24 | return DocType.Yaml; 25 | } else { 26 | return DocType.None; 27 | } 28 | } 29 | 30 | export function isQuartoDoc(doc: TextDocument) { 31 | return ( 32 | doc.languageId === kQuartoLanguageId || 33 | doc.languageId === kMarkdownLanguageId 34 | ); 35 | } 36 | 37 | export function isQuartoYaml(doc: TextDocument) { 38 | return ( 39 | doc.languageId === kYamlLanguageId && 40 | (doc.uri.match(/_quarto(-.*?)?\.ya?ml$/) || 41 | doc.uri.match(/_metadata\.ya?ml$/) || 42 | doc.uri.match(/_extension\.ya?ml$/)) 43 | ); 44 | } 45 | 46 | export function filePathForDoc(doc: TextDocument) { 47 | return URI.parse(doc.uri).fsPath; 48 | } 49 | 50 | const kRegExYAML = 51 | /(^)(---[ \t]*[\r\n]+(?![ \t]*[\r\n]+)[\W\w]*?[\r\n]+(?:---|\.\.\.))([ \t]*)$/gm; 52 | 53 | export function isQuartoRevealDoc(doc: TextDocument) { 54 | if (isQuartoDoc(doc)) { 55 | const text = doc.getText(); 56 | if (text) { 57 | const match = doc.getText().match(kRegExYAML); 58 | if (match) { 59 | const yaml = match[0]; 60 | return ( 61 | !!yaml.match(/^format\:\s+revealjs\s*$/gm) || 62 | !!yaml.match(/^[ \t]*revealjs\:\s*(default)?\s*$/gm) 63 | ); 64 | } 65 | } 66 | } else { 67 | return false; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /server/src/core/refs.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument } from "vscode-languageserver-textdocument"; 2 | import { Position } from "vscode-languageserver-types"; 3 | import { isContentPosition } from "./markdown/markdown"; 4 | 5 | export function bypassRefIntelligence( 6 | doc: TextDocument, 7 | pos: Position, 8 | line: string 9 | ): boolean { 10 | // bypass if the current line doesn't contain a @ 11 | // (performance optimization so we don't execute the regexs 12 | // below if we don't need to) 13 | if (line.indexOf("@") === -1) { 14 | return true; 15 | } 16 | 17 | // ensure we have the file scheme 18 | if (!doc.uri.startsWith("file:")) { 19 | return true; 20 | } 21 | 22 | // check if we are in markdown 23 | if (!isContentPosition(doc, pos)) { 24 | return true; 25 | } 26 | 27 | return false; 28 | } 29 | -------------------------------------------------------------------------------- /server/src/providers/completion/completion-attrs.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import { EditorContext, quarto } from "../../quarto/quarto"; 7 | import { AttrToken } from "../../quarto/quarto-attr"; 8 | 9 | export async function attrCompletions(context: EditorContext) { 10 | // bail if no quarto connection 11 | if (!quarto) { 12 | return null; 13 | } 14 | 15 | // validate trigger 16 | if (context.trigger && !["="].includes(context.trigger)) { 17 | return null; 18 | } 19 | 20 | // check for simple div 21 | let token = simpleDivToken(context); 22 | 23 | // bypass if the current line doesn't contain a { 24 | // (performance optimization so we don't execute the regexs 25 | // below if we don't need to) 26 | if (!token && context.line.indexOf("{") === -1) { 27 | return null; 28 | } 29 | 30 | // see what kind of token we might have 31 | token = 32 | token || blockCompletionToken(context) || figureCompletionToken(context); 33 | if (token) { 34 | return quarto.getAttrCompletions(token, context); 35 | } else { 36 | return null; 37 | } 38 | } 39 | 40 | const kBlockAttrRegex = /^([\t >]*(`{3,}|\#+|\:{3,}).*?\{)(.*?)\}[ \t]*$/; 41 | function blockCompletionToken(context: EditorContext): AttrToken | undefined { 42 | return matchCompletionToken(context, kBlockAttrRegex, (type) => { 43 | return type.indexOf(":") !== -1 44 | ? "div" 45 | : type.indexOf("#") !== -1 46 | ? "heading" 47 | : "codeblock"; 48 | }); 49 | } 50 | 51 | const kSimpleDivRegex = /(^[\t >]*(?:\:{3,})\s+)([\w-]+)\s*$/; 52 | function simpleDivToken(context: EditorContext): AttrToken | undefined { 53 | const match = context.line.match(kSimpleDivRegex); 54 | // if we are at the end then return a token 55 | if (context.line.slice(context.position.column).trim() === "") { 56 | if (match) { 57 | return { 58 | formats: [], 59 | context: "div-simple", 60 | attr: match[2], 61 | token: match[2], 62 | }; 63 | } 64 | } 65 | } 66 | 67 | const kFigureAttrRegex = 68 | /^([\t >]*(\!\[[^\]]*\]\([^\]]+\))\{)([^\}]*)\}[ \t]*$/; 69 | function figureCompletionToken(context: EditorContext): AttrToken | undefined { 70 | return matchCompletionToken(context, kFigureAttrRegex, (type) => "figure"); 71 | } 72 | 73 | function matchCompletionToken( 74 | context: EditorContext, 75 | pattern: RegExp, 76 | type: (type: string) => string 77 | ): AttrToken | undefined { 78 | const match = context.line.match(pattern); 79 | if (match) { 80 | // is the cursor in the attr region? (group 3) 81 | const beginAttr = match[1].length; 82 | const endAttr = match[1].length + match[3].length; 83 | const col = context.position.column; 84 | 85 | if (col >= beginAttr && col <= endAttr) { 86 | // is the next character a space or '}' ? 87 | if (context.line[col] === " " || context.line[col] === "}") { 88 | // token is the current location back to the next space or { 89 | const attrToCursor = context.line.slice(beginAttr, col); 90 | const spacePos = attrToCursor.lastIndexOf(" "); 91 | const token = 92 | spacePos !== -1 93 | ? match[3].slice(spacePos + 1, col - beginAttr) 94 | : match[3].slice(0, col - beginAttr); 95 | 96 | // return scope & token 97 | return { 98 | context: type(match[2]), 99 | attr: match[3], 100 | token, 101 | } as AttrToken; 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /server/src/providers/completion/completion-latex.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) 2016 James Yu 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | * ------------------------------------------------------------------------------------------ */ 6 | 7 | // based on https://github.com/James-Yu/LaTeX-Workshop/blob/master/src/providers/completion.ts 8 | 9 | import { Position, TextDocument } from "vscode-languageserver-textdocument"; 10 | 11 | import { 12 | CompletionContext, 13 | CompletionItem, 14 | CompletionItemKind, 15 | InsertTextFormat, 16 | Range, 17 | } from "vscode-languageserver/node"; 18 | 19 | import { isLatexPosition } from "../../core/markdown/markdown"; 20 | 21 | interface LatexCommand { 22 | command: string; 23 | snippet?: string; 24 | detail?: string; 25 | documentation?: string; 26 | } 27 | import mathjaxImport from "./mathjax.json"; 28 | const kMathjaxCommands = mathjaxImport as Record; 29 | 30 | import mathjaxCompletions from "./mathjax-completions.json"; 31 | import { mathjaxLoadedExtensions } from "../../core/mathjax"; 32 | const kMathjaxCompletions = mathjaxCompletions as Record; 33 | for (const key of Object.keys(kMathjaxCompletions)) { 34 | if (key.match(/\{.*?\}/)) { 35 | const ent = kMathjaxCompletions[key]; 36 | const newKey = key.replace(/\{.*?\}/, ""); 37 | delete kMathjaxCompletions[key]; 38 | kMathjaxCompletions[newKey] = ent; 39 | } 40 | } 41 | 42 | // for latex we complete the subset of commands supported by mathjax 43 | // (as those will work universally in pdf and html) 44 | export async function latexCompletions( 45 | doc: TextDocument, 46 | pos: Position, 47 | completionContext?: CompletionContext 48 | ): Promise { 49 | // validate trigger 50 | const trigger = completionContext?.triggerCharacter; 51 | if (trigger && !["\\"].includes(trigger)) { 52 | return null; 53 | } 54 | 55 | // check for latex position 56 | if (!isLatexPosition(doc, pos)) { 57 | return null; 58 | } 59 | 60 | // scan back from the cursor to see if there is a \ 61 | const line = doc 62 | .getText(Range.create(pos.line, 0, pos.line + 1, 0)) 63 | .trimEnd(); 64 | const text = line.slice(0, pos.character); 65 | const backslashPos = text.lastIndexOf("\\"); 66 | const spacePos = text.lastIndexOf(" "); 67 | if (backslashPos !== -1 && backslashPos > spacePos) { 68 | const loadedExtensions = mathjaxLoadedExtensions(); 69 | const token = text.slice(backslashPos + 1); 70 | const completions: CompletionItem[] = Object.keys(kMathjaxCommands) 71 | .filter((cmdName) => { 72 | if (cmdName.startsWith(token)) { 73 | // filter on loaded extensions 74 | const pkgs = kMathjaxCommands[cmdName]; 75 | return ( 76 | pkgs.length === 0 || 77 | pkgs.some((pkg) => loadedExtensions.includes(pkg)) 78 | ); 79 | } else { 80 | return false; 81 | } 82 | }) 83 | .map((cmd) => { 84 | const mathjaxCompletion = kMathjaxCompletions[cmd]; 85 | if (mathjaxCompletion) { 86 | return { 87 | kind: CompletionItemKind.Function, 88 | label: mathjaxCompletion.command, 89 | documentation: mathjaxCompletion.documentation, 90 | detail: mathjaxCompletion.detail, 91 | insertTextFormat: InsertTextFormat.Snippet, 92 | insertText: mathjaxCompletion.snippet, 93 | }; 94 | } else { 95 | return { 96 | kind: CompletionItemKind.Function, 97 | label: cmd, 98 | }; 99 | } 100 | }); 101 | 102 | // single completion w/ matching token is ignored 103 | if (completions.length == 1 && completions[0].label === token) { 104 | return null; 105 | } 106 | 107 | // return completions if we have them 108 | if (completions.length > 0) { 109 | return completions; 110 | } 111 | } 112 | 113 | return null; 114 | } 115 | -------------------------------------------------------------------------------- /server/src/providers/completion/completion-yaml.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import { Range, TextEdit } from "vscode-languageserver-types"; 7 | 8 | import { 9 | Command, 10 | CompletionItem, 11 | CompletionItemKind, 12 | } from "vscode-languageserver/node"; 13 | 14 | import { EditorContext, quarto } from "../../quarto/quarto"; 15 | 16 | export async function yamlCompletions(context: EditorContext) { 17 | // bail if no quarto connection 18 | if (!quarto) { 19 | return null; 20 | } 21 | 22 | // get completions 23 | const result = await quarto.getYamlCompletions(context); 24 | if (result) { 25 | // if there is one completion and it matches the token 26 | // then don't return it 27 | if ( 28 | result.completions.length === 1 && 29 | result.token === result.completions[0].value 30 | ) { 31 | return null; 32 | } 33 | 34 | // mqp our completions to vscode completions 35 | return result.completions.map((completion) => { 36 | const completionWord = completion.value.replace(/: $/, ""); 37 | const item: CompletionItem = { 38 | label: completionWord, 39 | kind: CompletionItemKind.Field, 40 | }; 41 | // strip tags from description 42 | if (completion.description) { 43 | item.documentation = decodeEntities( 44 | completion.description 45 | .replace(/(<([^>]+)>)/gi, "") 46 | .replace(/\n/g, " ") 47 | ); 48 | } 49 | if (result.token.length > 0 && completionWord.startsWith(result.token)) { 50 | const edit = TextEdit.replace( 51 | Range.create( 52 | context.position.row, 53 | context.position.column - result.token.length, 54 | context.position.row, 55 | context.position.column 56 | ), 57 | completion.value 58 | ); 59 | item.textEdit = edit; 60 | } else { 61 | item.insertText = completion.value; 62 | } 63 | 64 | if (completion.suggest_on_accept) { 65 | item.command = Command.create( 66 | "Suggest", 67 | "editor.action.triggerSuggest" 68 | ); 69 | } 70 | return item; 71 | }); 72 | } else { 73 | return null; 74 | } 75 | } 76 | 77 | function decodeEntities(encodedString: string) { 78 | var translate_re = /&(nbsp|amp|quot|lt|gt);/g; 79 | var translate: Record = { 80 | nbsp: " ", 81 | amp: "&", 82 | quot: '"', 83 | lt: "<", 84 | gt: ">", 85 | }; 86 | return encodedString 87 | .replace(translate_re, function (_match, entity: string) { 88 | return translate[entity]; 89 | }) 90 | .replace(/&#(\d+);/gi, function (_match, numStr) { 91 | var num = parseInt(numStr, 10); 92 | return String.fromCharCode(num); 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /server/src/providers/completion/completion.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import { Position, TextDocument } from "vscode-languageserver-textdocument"; 7 | 8 | import { 9 | CompletionContext, 10 | CompletionItem, 11 | CompletionTriggerKind, 12 | ServerCapabilities, 13 | } from "vscode-languageserver/node"; 14 | import { editorContext } from "../../quarto/quarto"; 15 | import { attrCompletions } from "./completion-attrs"; 16 | import { latexCompletions } from "./completion-latex"; 17 | import { yamlCompletions } from "./completion-yaml"; 18 | import { refsCompletions } from "./refs/completion-refs"; 19 | 20 | export const kCompletionCapabilities: ServerCapabilities = { 21 | completionProvider: { 22 | resolveProvider: false, 23 | // register a superset of all trigger characters for embedded languages 24 | // (languages are responsible for declaring which one they support if any) 25 | triggerCharacters: [".", "$", "@", ":", "\\", "="], 26 | }, 27 | }; 28 | 29 | export async function onCompletion( 30 | doc: TextDocument, 31 | pos: Position, 32 | completionContext?: CompletionContext 33 | ): Promise { 34 | const explicit = 35 | completionContext?.triggerKind === CompletionTriggerKind.TriggerCharacter; 36 | const trigger = completionContext?.triggerCharacter; 37 | const context = editorContext(doc, pos, explicit, trigger); 38 | return ( 39 | (await refsCompletions(doc, pos, context, completionContext)) || 40 | (await attrCompletions(context)) || 41 | (await latexCompletions(doc, pos, completionContext)) || 42 | (await yamlCompletions(context)) || 43 | null 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /server/src/providers/completion/refs/completion-biblio.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import { TextDocument } from "vscode-languageserver-textdocument"; 7 | import { 8 | CompletionItem, 9 | CompletionItemKind, 10 | MarkupKind, 11 | } from "vscode-languageserver/node"; 12 | import { biblioRefs } from "../../../core/biblio"; 13 | 14 | export function biblioCompletions( 15 | token: string, 16 | doc: TextDocument 17 | ): CompletionItem[] | null { 18 | const refs = biblioRefs(doc); 19 | if (refs) { 20 | return refs 21 | .filter((ref) => ref.id.startsWith(token)) 22 | .map((ref) => ({ 23 | kind: CompletionItemKind.Constant, 24 | label: ref.id, 25 | documentation: ref.cite 26 | ? { 27 | kind: MarkupKind.Markdown, 28 | value: ref.cite, 29 | } 30 | : undefined, 31 | })); 32 | } else { 33 | return null; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/src/providers/completion/refs/completion-refs.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import { TextDocument } from "vscode-languageserver-textdocument"; 7 | import { Range, Position } from "vscode-languageserver-types"; 8 | 9 | import { CompletionContext, CompletionItem } from "vscode-languageserver/node"; 10 | import { filePathForDoc } from "../../../core/doc"; 11 | import { bypassRefIntelligence } from "../../../core/refs"; 12 | 13 | import { EditorContext, quarto } from "../../../quarto/quarto"; 14 | import { projectDirForDocument } from "../../../shared/metadata"; 15 | import { biblioCompletions } from "./completion-biblio"; 16 | import { crossrefCompletions } from "./completion-crossref"; 17 | 18 | export async function refsCompletions( 19 | doc: TextDocument, 20 | pos: Position, 21 | context: EditorContext, 22 | _completionContext?: CompletionContext 23 | ): Promise { 24 | // bail if no quarto connection 25 | if (!quarto) { 26 | return null; 27 | } 28 | 29 | // validate trigger 30 | if (context.trigger && !["@"].includes(context.trigger)) { 31 | return null; 32 | } 33 | 34 | if (bypassRefIntelligence(doc, pos, context.line)) { 35 | return null; 36 | } 37 | 38 | // scan back from the cursor to see if there is a @ 39 | const line = doc 40 | .getText(Range.create(pos.line, 0, pos.line + 1, 0)) 41 | .trimEnd(); 42 | const text = line.slice(0, pos.character); 43 | const atPos = text.lastIndexOf("@"); 44 | const spacePos = text.lastIndexOf(" "); 45 | 46 | if (atPos !== -1 && atPos > spacePos) { 47 | // everything between the @ and the cursor must match the cite pattern 48 | const tokenText = text.slice(atPos + 1, pos.character); 49 | if (/[^@;\[\]\s\!\,]*/.test(tokenText)) { 50 | // make sure there is no text directly ahead (except bracket, space, semicolon) 51 | const nextChar = text.slice(pos.character, pos.character + 1); 52 | if (!nextChar || [";", " ", "]"].includes(nextChar)) { 53 | // construct path 54 | const path = filePathForDoc(doc); 55 | const projectDir = projectDirForDocument(path); 56 | const biblioItems = biblioCompletions(tokenText, doc); 57 | const crossrefItems = await crossrefCompletions( 58 | tokenText, 59 | doc.getText(), 60 | path, 61 | projectDir 62 | ); 63 | if (biblioItems || crossrefItems) { 64 | return (biblioItems || []).concat(crossrefItems || []); 65 | } else { 66 | return null; 67 | } 68 | } 69 | } 70 | } 71 | 72 | return null; 73 | } 74 | -------------------------------------------------------------------------------- /server/src/providers/diagnostics.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import { TextDocument } from "vscode-languageserver-textdocument"; 7 | import { 8 | Diagnostic, 9 | DiagnosticSeverity, 10 | Position, 11 | Range, 12 | } from "vscode-languageserver/node"; 13 | import { 14 | editorContext, 15 | kEndColumn, 16 | kEndRow, 17 | kStartColumn, 18 | kStartRow, 19 | LintItem, 20 | quarto, 21 | } from "../quarto/quarto"; 22 | 23 | export async function provideDiagnostics( 24 | doc: TextDocument 25 | ): Promise { 26 | // bail if no quarto connection 27 | if (!quarto) { 28 | return []; 29 | } 30 | 31 | if (quarto) { 32 | const context = editorContext(doc, Position.create(0, 0), true); 33 | const diagnostics = await quarto.getYamlDiagnostics(context); 34 | return diagnostics.map((item) => { 35 | return { 36 | severity: lintSeverity(item), 37 | range: Range.create( 38 | item[kStartRow], 39 | item[kStartColumn], 40 | item[kEndRow], 41 | item[kEndColumn] 42 | ), 43 | message: item.text, 44 | source: "quarto", 45 | }; 46 | }); 47 | } else { 48 | return []; 49 | } 50 | } 51 | 52 | function lintSeverity(item: LintItem) { 53 | if (item.type === "error") { 54 | return DiagnosticSeverity.Error; 55 | } else if (item.type === "warning") { 56 | return DiagnosticSeverity.Warning; 57 | } else { 58 | return DiagnosticSeverity.Information; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server/src/providers/hover/hover-math.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) 2016 James Yu 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | * ------------------------------------------------------------------------------------------ */ 6 | 7 | import { Hover, Position } from "vscode-languageserver/node"; 8 | import { TextDocument } from "vscode-languageserver-textdocument"; 9 | 10 | import { mathRange } from "../../core/markdown/markdown"; 11 | import { mathjaxTypesetToMarkdown } from "../../core/mathjax"; 12 | 13 | export function mathHover(doc: TextDocument, pos: Position): Hover | null { 14 | const range = mathRange(doc, pos); 15 | if (range) { 16 | defineNewCommands(doc.getText()); 17 | const contents = mathjaxTypesetToMarkdown(range.math); 18 | if (contents) { 19 | return { 20 | contents, 21 | range: range.range, 22 | }; 23 | } 24 | } 25 | return null; 26 | } 27 | 28 | // newcommand macros we have already typeset 29 | const newCommandsDefined = new Set(); 30 | function defineNewCommands(content: string) { 31 | // define any commands that haven't beeen already 32 | for (const command of newCommands(content)) { 33 | if (!newCommandsDefined.has(command)) { 34 | mathjaxTypesetToMarkdown(command); 35 | newCommandsDefined.add(command); 36 | } 37 | } 38 | } 39 | 40 | // based on https://github.com/James-Yu/LaTeX-Workshop/blob/b5ea2a626be7d4e5a2ebe0ec93a4012f42bf931a/src/providers/preview/mathpreviewlib/newcommandfinder.ts#L92 41 | function* newCommands(content: string) { 42 | const regex = 43 | /(\\(?:(?:(?:(?:re)?new|provide)command|DeclareMathOperator)(\*)?{\\[a-zA-Z]+}(?:\[[^[\]{}]*\])*{.*})|\\(?:def\\[a-zA-Z]+(?:#[0-9])*{.*})|\\DeclarePairedDelimiter{\\[a-zA-Z]+}{[^{}]*}{[^{}]*})/gm; 44 | const noCommentContent = stripComments(content); 45 | let result: RegExpExecArray | null; 46 | do { 47 | result = regex.exec(noCommentContent); 48 | if (result) { 49 | let command = result[1]; 50 | if (result[2]) { 51 | command = command.replace(/\*/, ""); 52 | } 53 | yield command; 54 | } 55 | } while (result); 56 | } 57 | 58 | function stripComments(text: string): string { 59 | const reg = /(^|[^\\]|(?:(? x.id === citeId); 45 | if (ref?.cite) { 46 | lastRef = ref; 47 | return hoverFromCslRef(ref.cite, range); 48 | } 49 | } 50 | } 51 | } 52 | } 53 | 54 | return null; 55 | } 56 | 57 | function hoverFromCslRef(cite: string, range: Range): Hover { 58 | return { 59 | contents: { 60 | kind: "markdown", 61 | value: cite, 62 | }, 63 | range, 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /server/src/providers/hover/hover-yaml.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import { Position, TextDocument } from "vscode-languageserver-textdocument"; 7 | import { Hover } from "vscode-languageserver/node"; 8 | 9 | import { editorContext, quarto } from "../../quarto/quarto"; 10 | 11 | export async function yamlHover( 12 | doc: TextDocument, 13 | pos: Position 14 | ): Promise { 15 | // bail if no quarto connection 16 | if (!quarto?.getHover) { 17 | return null; 18 | } 19 | try { 20 | const context = editorContext(doc, pos, true); 21 | const result = await quarto.getHover(context); 22 | if (result === null) { 23 | return null; 24 | } 25 | return { 26 | contents: { 27 | kind: "markdown", 28 | value: result.content, 29 | }, 30 | range: result.range, 31 | }; 32 | } catch { 33 | return null; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/src/providers/hover/hover.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import { Position, TextDocument } from "vscode-languageserver-textdocument"; 7 | import { Hover, ServerCapabilities } from "vscode-languageserver/node"; 8 | 9 | import { yamlHover } from "./hover-yaml"; 10 | import { mathHover } from "./hover-math"; 11 | import { refHover } from "./hover-ref"; 12 | 13 | export const kHoverCapabilities: ServerCapabilities = { 14 | hoverProvider: true, 15 | }; 16 | 17 | export async function onHover( 18 | doc: TextDocument, 19 | pos: Position 20 | ): Promise { 21 | return ( 22 | refHover(doc, pos) || mathHover(doc, pos) || (await yamlHover(doc, pos)) 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /server/src/providers/signature.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import { Position, TextDocument } from "vscode-languageserver-textdocument"; 7 | import { ServerCapabilities, SignatureHelp } from "vscode-languageserver/node"; 8 | 9 | export const kSignatureCapabilities: ServerCapabilities = { 10 | signatureHelpProvider: { 11 | // assume for now that these cover all languages (we can introduce 12 | // a refinement system like we do for completion triggers if necessary) 13 | triggerCharacters: ["(", ","], 14 | retriggerCharacters: [")"], 15 | }, 16 | }; 17 | 18 | export async function onSignatureHelp( 19 | doc: TextDocument, 20 | pos: Position 21 | ): Promise { 22 | return null; 23 | } 24 | -------------------------------------------------------------------------------- /server/src/quarto/quarto-yaml.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import * as path from "path"; 7 | 8 | import fileUrl from "file-url"; 9 | import { EditorContext, HoverResult } from "./quarto"; 10 | 11 | export const kStartRow = "start.row"; 12 | export const kStartColumn = "start.column"; 13 | export const kEndRow = "end.row"; 14 | export const kEndColumn = "end.column"; 15 | 16 | export interface LintItem { 17 | [kStartRow]: number; 18 | [kStartColumn]: number; 19 | [kEndRow]: number; 20 | [kEndColumn]: number; 21 | text: string; 22 | type: string; 23 | } 24 | 25 | export interface CompletionResult { 26 | token: string; 27 | completions: Completion[]; 28 | cacheable: boolean; 29 | } 30 | 31 | export interface Completion { 32 | type: string; 33 | value: string; 34 | display?: string; 35 | description?: string; 36 | suggest_on_accept?: boolean; 37 | replace_to_end?: boolean; 38 | } 39 | 40 | export interface QuartoYamlModule { 41 | getCompletions(context: EditorContext): Promise; 42 | getLint(context: EditorContext): Promise>; 43 | getHover?: (context: EditorContext) => Promise; 44 | } 45 | 46 | export function initializeQuartoYamlModule( 47 | resourcesPath: string 48 | ): Promise { 49 | const modulePath = path.join(resourcesPath, "editor", "tools", "vs-code.mjs"); 50 | return new Promise((resolve, reject) => { 51 | import(fileUrl(modulePath)) 52 | .then((mod) => { 53 | const quartoModule = mod as QuartoYamlModule; 54 | resolve(quartoModule); 55 | }) 56 | .catch((error) => { 57 | reject(error); 58 | }); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /server/src/quarto/quarto.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import { URL } from "url"; 7 | 8 | import { TextDocument } from "vscode-languageserver-textdocument"; 9 | import { Position, Range, CompletionItem } from "vscode-languageserver-types"; 10 | import { 11 | filePathForDoc, 12 | isQuartoDoc, 13 | isQuartoRevealDoc, 14 | isQuartoYaml, 15 | } from "../core/doc"; 16 | import { initializeAttrCompletionProvider, AttrToken } from "./quarto-attr"; 17 | import { initializeQuartoYamlModule, QuartoYamlModule } from "./quarto-yaml"; 18 | import { initQuartoContext } from "../shared/quarto"; 19 | import { ExecFileSyncOptions } from "child_process"; 20 | 21 | export interface EditorContext { 22 | path: string; 23 | filetype: string; 24 | embedded: boolean; 25 | line: string; 26 | code: string; 27 | position: { 28 | row: number; 29 | column: number; 30 | }; 31 | explicit: boolean; 32 | trigger?: string; 33 | formats: string[]; 34 | project_formats: string[]; 35 | engine: string; 36 | client: string; 37 | } 38 | 39 | export function editorContext( 40 | doc: TextDocument, 41 | pos: Position, 42 | explicit: boolean, 43 | trigger?: string 44 | ) { 45 | const path = filePathForDoc(doc); 46 | const filetype = isQuartoDoc(doc) 47 | ? "markdown" 48 | : isQuartoYaml(doc) 49 | ? "yaml" 50 | : "markdown"; // should never get here 51 | const embedded = false; 52 | const code = doc.getText(); 53 | const line = doc 54 | .getText(Range.create(pos.line, 0, pos.line, code.length)) 55 | .replace(/[\r\n]+$/, ""); 56 | const position = { row: pos.line, column: pos.character }; 57 | 58 | // detect reveal document 59 | const formats: string[] = []; 60 | if (isQuartoRevealDoc(doc)) { 61 | formats.push("revealjs"); 62 | } 63 | 64 | return { 65 | path, 66 | filetype, 67 | embedded, 68 | line, 69 | code, 70 | position, 71 | explicit, 72 | trigger, 73 | formats, 74 | project_formats: [], 75 | engine: "jupyter", 76 | client: "lsp", 77 | }; 78 | } 79 | 80 | export const kStartRow = "start.row"; 81 | export const kStartColumn = "start.column"; 82 | export const kEndRow = "end.row"; 83 | export const kEndColumn = "end.column"; 84 | 85 | export interface LintItem { 86 | [kStartRow]: number; 87 | [kStartColumn]: number; 88 | [kEndRow]: number; 89 | [kEndColumn]: number; 90 | text: string; 91 | type: string; 92 | } 93 | 94 | export interface CompletionResult { 95 | token: string; 96 | completions: Completion[]; 97 | cacheable: boolean; 98 | } 99 | 100 | export interface HoverResult { 101 | content: string; 102 | range: { start: Position; end: Position }; 103 | } 104 | 105 | export interface Completion { 106 | type: string; 107 | value: string; 108 | display?: string; 109 | description?: string; 110 | suggest_on_accept?: boolean; 111 | replace_to_end?: boolean; 112 | } 113 | 114 | export interface Quarto { 115 | getYamlCompletions(context: EditorContext): Promise; 116 | getAttrCompletions( 117 | token: AttrToken, 118 | context: EditorContext 119 | ): Promise; 120 | getYamlDiagnostics(context: EditorContext): Promise; 121 | getHover?: (context: EditorContext) => Promise; 122 | runQuarto: (options: ExecFileSyncOptions, ...args: string[]) => string; 123 | runPandoc: (options: ExecFileSyncOptions, ...args: string[]) => string; 124 | resourcePath: string; 125 | } 126 | 127 | export let quarto: Quarto | undefined; 128 | 129 | export function initializeQuarto( 130 | quartoPath?: string, 131 | workspaceFolder?: string 132 | ) { 133 | const quartoContext = initQuartoContext(quartoPath, workspaceFolder); 134 | initializeQuartoYamlModule(quartoContext.resourcePath) 135 | .then((mod) => { 136 | const quartoModule = mod as QuartoYamlModule; 137 | quarto = { 138 | getYamlCompletions: quartoModule.getCompletions, 139 | getAttrCompletions: initializeAttrCompletionProvider( 140 | quartoContext.resourcePath 141 | ), 142 | getYamlDiagnostics: quartoModule.getLint, 143 | getHover: quartoModule.getHover, 144 | runQuarto: quartoContext.runQuarto, 145 | runPandoc: quartoContext.runPandoc, 146 | resourcePath: quartoContext.resourcePath, 147 | }; 148 | }) 149 | .catch((error) => { 150 | console.log(error); 151 | }); 152 | } 153 | -------------------------------------------------------------------------------- /server/src/shared/appdirs.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as path from "path"; 7 | import * as fs from "fs"; 8 | import * as os from "os"; 9 | 10 | export function quartoDataDir(subdir?: string, roaming = false) { 11 | return quartoDir(userDataDir, subdir, roaming); 12 | } 13 | 14 | export function quartoConfigDir(subdir?: string, roaming = false) { 15 | return quartoDir(userConfigDir, subdir, roaming); 16 | } 17 | 18 | export function quartoCacheDir(subdir?: string) { 19 | return quartoDir(userCacheDir, subdir); 20 | } 21 | 22 | export function quartoRuntimeDir(subdir?: string) { 23 | return quartoDir(userRuntimeDir, subdir); 24 | } 25 | 26 | function quartoDir( 27 | sourceFn: (appName: string, roaming?: boolean) => string, 28 | subdir?: string, 29 | roaming?: boolean 30 | ) { 31 | const dir = sourceFn("quarto", roaming); 32 | const fullDir = subdir ? path.join(dir, subdir) : dir; 33 | if (!fs.existsSync(fullDir)) { 34 | fs.mkdirSync(fullDir); 35 | } 36 | return fullDir; 37 | } 38 | 39 | export function userDataDir(appName: string, roaming = false) { 40 | switch (os.platform()) { 41 | case "darwin": 42 | return darwinUserDataDir(appName); 43 | case "win32": 44 | return windowsUserDataDir(appName, roaming); 45 | case "linux": 46 | default: 47 | return xdgUserDataDir(appName); 48 | } 49 | } 50 | 51 | export function userConfigDir(appName: string, roaming = false) { 52 | switch (os.platform()) { 53 | case "darwin": 54 | return darwinUserDataDir(appName); 55 | case "win32": 56 | return windowsUserDataDir(appName, roaming); 57 | case "linux": 58 | default: 59 | return xdgUserConfigDir(appName); 60 | } 61 | } 62 | 63 | export function userCacheDir(appName: string) { 64 | switch (os.platform()) { 65 | case "darwin": 66 | return darwinUserCacheDir(appName); 67 | case "win32": 68 | return windowsUserDataDir(appName); 69 | case "linux": 70 | default: 71 | return xdgUserCacheDir(appName); 72 | } 73 | } 74 | 75 | export function userRuntimeDir(appName: string) { 76 | switch (os.platform()) { 77 | case "darwin": 78 | return darwinUserCacheDir(appName); 79 | case "win32": 80 | return windowsUserDataDir(appName); 81 | case "linux": 82 | default: 83 | return xdgUserRuntimeDir(appName); 84 | } 85 | } 86 | 87 | function darwinUserDataDir(appName: string) { 88 | return path.join( 89 | process.env["HOME"] || "", 90 | "Library", 91 | "Application Support", 92 | appName 93 | ); 94 | } 95 | 96 | function darwinUserCacheDir(appName: string) { 97 | return path.join(process.env["HOME"] || "", "Library", "Caches", appName); 98 | } 99 | 100 | function xdgUserDataDir(appName: string) { 101 | const dataHome = 102 | process.env["XDG_DATA_HOME"] || 103 | path.join(process.env["HOME"] || "", ".local", "share"); 104 | return path.join(dataHome, appName); 105 | } 106 | 107 | function xdgUserConfigDir(appName: string) { 108 | const configHome = 109 | process.env["XDG_CONFIG_HOME"] || 110 | path.join(process.env["HOME"] || "", ".config"); 111 | return path.join(configHome, appName); 112 | } 113 | 114 | function xdgUserCacheDir(appName: string) { 115 | const cacheHome = 116 | process.env["XDG_CACHE_HOME"] || 117 | path.join(process.env["HOME"] || "", ".cache"); 118 | return path.join(cacheHome, appName); 119 | } 120 | 121 | function xdgUserRuntimeDir(appName: string) { 122 | const runtimeDir = process.env["XDG_RUNTIME_DIR"]; 123 | if (runtimeDir) { 124 | return runtimeDir; 125 | } else { 126 | return xdgUserDataDir(appName); 127 | } 128 | } 129 | 130 | function windowsUserDataDir(appName: string, roaming = false) { 131 | const dir = 132 | (roaming ? process.env["APPDATA"] : process.env["LOCALAPPDATA"]) || ""; 133 | return path.join(dir, appName); 134 | } 135 | -------------------------------------------------------------------------------- /server/src/shared/exec.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as child_process from "child_process"; 7 | 8 | // helper to run a program and capture its output 9 | export function execProgram( 10 | program: string, 11 | args: string[], 12 | options?: child_process.ExecFileSyncOptions 13 | ) { 14 | return ( 15 | child_process.execFileSync(program, args, { 16 | encoding: "utf-8", 17 | ...options, 18 | }) as unknown as string 19 | ).trim(); 20 | } 21 | -------------------------------------------------------------------------------- /server/src/shared/path.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | export function pathWithForwardSlashes(path: string) { 7 | return path.replace(/\\/g, "/"); 8 | } 9 | -------------------------------------------------------------------------------- /server/src/shared/storage.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as path from "path"; 7 | import * as fs from "fs"; 8 | 9 | import * as uuid from "uuid"; 10 | 11 | import { quartoCacheDir } from "./appdirs"; 12 | 13 | export function fileCrossrefIndexStorage(file: string) { 14 | return fileScratchStorage(file, "xref.json"); 15 | } 16 | 17 | export function fileScratchStorage(file: string, scope: string, dir?: boolean) { 18 | // determine uuid for file scratch storage 19 | file = path.normalize(file); 20 | const index = readFileScratchStorageIndex(); 21 | let fileStorage = index[file]; 22 | if (!fileStorage) { 23 | fileStorage = uuid.v4(); 24 | index[file] = fileStorage; 25 | writeFileScratchStorageIndex(index); 26 | } 27 | 28 | // ensure the dir exists 29 | const scratchStorageDir = fileScratchStoragePath(fileStorage); 30 | if (!fs.existsSync(scratchStorageDir)) { 31 | fs.mkdirSync(scratchStorageDir); 32 | } 33 | 34 | // return the path for the scope (creating dir as required) 35 | const scopedScratchStorage = path.join(scratchStorageDir, scope); 36 | if (dir) { 37 | if (!fs.existsSync(scopedScratchStorage)) { 38 | fs.mkdirSync(scopedScratchStorage); 39 | } 40 | } 41 | return scopedScratchStorage; 42 | } 43 | 44 | function readFileScratchStorageIndex(): Record { 45 | const index = fileScratchStorageIndexPath(); 46 | if (fs.existsSync(index)) { 47 | return JSON.parse(fs.readFileSync(index, { encoding: "utf-8" })); 48 | } 49 | return {}; 50 | } 51 | 52 | function writeFileScratchStorageIndex(index: Record) { 53 | fs.writeFileSync( 54 | fileScratchStorageIndexPath(), 55 | JSON.stringify(index, undefined, 2), 56 | { encoding: "utf-8" } 57 | ); 58 | } 59 | 60 | const fileScratchStorageIndexPath = () => fileScratchStoragePath("INDEX"); 61 | 62 | function fileScratchStoragePath(file?: string) { 63 | const storagePath = path.join(quartoCacheDir(), "file-storage"); 64 | if (!fs.existsSync(storagePath)) { 65 | fs.mkdirSync(storagePath); 66 | } 67 | return file ? path.join(storagePath, file) : storagePath; 68 | } 69 | -------------------------------------------------------------------------------- /server/src/shared/strings.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export function escapeRegExpCharacters(value: string): string { 7 | return value.replace(/[\\\{\}\*\+\?\|\^\$\.\[\]\(\)]/g, "\\$&"); 8 | } 9 | 10 | export function shQuote(value: string): string { 11 | if (/\s/g.test(value)) { 12 | return `"${value}"`; 13 | } else { 14 | return value; 15 | } 16 | } 17 | 18 | export function winShEscape(value: string): string { 19 | return value.replace(" ", "^ "); 20 | } 21 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "lib": ["ES2019"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "esModuleInterop": true, 9 | "resolveJsonModule": true, 10 | "strict": true, 11 | "outDir": "out", 12 | "rootDir": "src", 13 | "paths": { 14 | "mathjax-full": ["./types/mathjax-full"] 15 | } 16 | }, 17 | "include": ["src"], 18 | "exclude": ["node_modules", ".vscode-test"] 19 | } 20 | -------------------------------------------------------------------------------- /server/types/mathjax-full/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { default as TexError } from "mathjax-full/js/input/tex/TexError.js"; 2 | import type { TeX } from "mathjax-full/js/input/tex.js"; 3 | import type { LiteElement } from "mathjax-full/js/adaptors/lite/Element"; 4 | import type { LiteDocument } from "mathjax-full/js/adaptors/lite/Document.js"; 5 | import type { LiteText } from "mathjax-full/js/adaptors/lite/Text.js"; 6 | 7 | export type SupportedExtension = 8 | | "action" 9 | | "ams" 10 | | "amscd" 11 | | "autoload" 12 | | "base" 13 | | "bbox" 14 | | "boldsymbol" 15 | | "braket" 16 | | "bussproofs" 17 | | "cancel" 18 | | "cases" 19 | | "centernot" 20 | | "color" 21 | | "colortbl" 22 | | "colorv2" 23 | | "configmacros" 24 | | "empheq" 25 | | "enclose" 26 | | "extpfeil" 27 | | "gensymb" 28 | | "html" 29 | | "mathtools" 30 | | "mhchem" 31 | | "newcommand" 32 | | "noerrors" 33 | | "noundefined" 34 | | "physics" 35 | | "require" 36 | | "setoptions" 37 | | "tagformat" 38 | | "textcomp" 39 | | "textmacros" 40 | | "unicode" 41 | | "upgreek" 42 | | "verb"; 43 | 44 | export type TexOption = { 45 | packages?: readonly SupportedExtension[]; 46 | inlineMath?: readonly [string, string][]; 47 | displayMath?: readonly [string, string][]; 48 | processEscapes?: boolean; 49 | processEnvironments?: boolean; 50 | processRefs?: boolean; 51 | digits?: RegExp; 52 | tags?: "all" | "ams" | "none"; 53 | tagSide?: "right" | "left"; 54 | tagIndent?: string; 55 | useLabelIds?: boolean; 56 | maxMacros?: number; 57 | maxBuffer?: number; 58 | baseURL?: string; 59 | formatError?: ( 60 | jax: TeX, 61 | message: TexError 62 | ) => unknown; 63 | }; 64 | 65 | export type SvgOption = { 66 | scale?: number; 67 | minScale?: number; 68 | mtextInheritFont?: boolean; 69 | merrorInheritFont?: boolean; 70 | mathmlSpacing?: boolean; 71 | skipAttributes?: { [attrname: string]: boolean }; 72 | exFactor?: number; 73 | displayAlign?: "left" | "center" | "right"; 74 | displayIndent?: number; 75 | fontCache?: "local" | "global" | "none"; 76 | internalSpeechTitles?: boolean; 77 | }; 78 | 79 | export type ConvertOption = { 80 | display?: boolean; 81 | em?: number; 82 | ex?: number; 83 | containerWidth?: number; 84 | lineWidth?: number; 85 | scale?: number; 86 | }; 87 | -------------------------------------------------------------------------------- /snippets/quarto.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Insert bold text": { 3 | "prefix": "bold", 4 | "body": "**${1:${TM_SELECTED_TEXT}}**$0", 5 | "description": "Insert bold text" 6 | }, 7 | "Insert italic text": { 8 | "prefix": "italic", 9 | "body": "*${1:${TM_SELECTED_TEXT}}*$0", 10 | "description": "Insert italic text" 11 | }, 12 | "Insert quoted text": { 13 | "prefix": "quote", 14 | "body": "> ${1:${TM_SELECTED_TEXT}}", 15 | "description": "Insert quoted text" 16 | }, 17 | "Insert inline code": { 18 | "prefix": "code", 19 | "body": "`${1:${TM_SELECTED_TEXT}}`$0", 20 | "description": "Insert inline code" 21 | }, 22 | "Insert shortcode": { 23 | "prefix": "shortcode", 24 | "body": "{{< $0 >}}", 25 | "description": "Insert shortcode" 26 | }, 27 | "Insert fenced code block": { 28 | "prefix": "fenced codeblock", 29 | "body": [ 30 | "```${1|python,c,c++,c#,ruby,go,java,php,htm,css,javascript,json,markdown,console|}", 31 | "${TM_SELECTED_TEXT}$0", 32 | "```" 33 | ], 34 | "description": "Insert fenced code block" 35 | }, 36 | "Insert executable code block": { 37 | "prefix": "executable codeblock", 38 | "body": [ 39 | "```{${1|python,r,julia,ojs,sql,bash|}}", 40 | "${TM_SELECTED_TEXT}$0", 41 | "```" 42 | ], 43 | "description": "Insert executable code block" 44 | }, 45 | "Insert raw code block": { 46 | "prefix": "raw codeblock", 47 | "body": [ 48 | "```{${1|html,latex,openxml,opendocument,asciidoc,docbook,markdown,dokuwiki,fb2,gfm,haddock,icml,ipynb,jats,jira,json,man,mediawiki,ms,muse,opml,org,plain,rst,rtf,tei,texinfo,textile,xwiki,zimwiki,native|}}", 49 | "${TM_SELECTED_TEXT}$0", 50 | "```" 51 | ], 52 | "description": "Insert raw code block" 53 | }, 54 | "Insert heading level 1": { 55 | "prefix": "heading1", 56 | "body": "# ${1:${TM_SELECTED_TEXT}}", 57 | "description": "Insert heading level 1" 58 | }, 59 | "Insert heading level 2": { 60 | "prefix": "heading2", 61 | "body": "## ${1:${TM_SELECTED_TEXT}}", 62 | "description": "Insert heading level 2" 63 | }, 64 | "Insert heading level 3": { 65 | "prefix": "heading3", 66 | "body": "### ${1:${TM_SELECTED_TEXT}}", 67 | "description": "Insert heading level 3" 68 | }, 69 | "Insert heading level 4": { 70 | "prefix": "heading4", 71 | "body": "#### ${1:${TM_SELECTED_TEXT}}", 72 | "description": "Insert heading level 4" 73 | }, 74 | "Insert heading level 5": { 75 | "prefix": "heading5", 76 | "body": "##### ${1:${TM_SELECTED_TEXT}}", 77 | "description": "Insert heading level 5" 78 | }, 79 | "Insert heading level 6": { 80 | "prefix": "heading6", 81 | "body": "###### ${1:${TM_SELECTED_TEXT}}", 82 | "description": "Insert heading level 6" 83 | }, 84 | "Insert unordered list": { 85 | "prefix": "unordered list", 86 | "body": ["- ${1:first}", "- ${2:second}", "- ${3:third}", "$0"], 87 | "description": "Insert unordered list" 88 | }, 89 | "Insert ordered list": { 90 | "prefix": "ordered list", 91 | "body": ["1. ${1:first}", "2. ${2:second}", "3. ${3:third}", "$0"], 92 | "description": "Insert ordered list" 93 | }, 94 | "Insert horizontal rule": { 95 | "prefix": "horizontal rule", 96 | "body": "----------\n", 97 | "description": "Insert horizontal rule" 98 | }, 99 | "Insert link": { 100 | "prefix": "link", 101 | "body": "[${TM_SELECTED_TEXT:${1:text}}](${2:link})$0", 102 | "description": "Insert link" 103 | }, 104 | "Insert image": { 105 | "prefix": "image", 106 | "body": "![${TM_SELECTED_TEXT:${1:alt}}](${2:link})$0", 107 | "description": "Insert image" 108 | }, 109 | "Insert strikethrough": { 110 | "prefix": "strikethrough", 111 | "body": "~~${1:${TM_SELECTED_TEXT}}~~", 112 | "description": "Insert strikethrough" 113 | }, 114 | "Insert div block": { 115 | "prefix": "div", 116 | "body": ["::: {.${1:class}}", "${TM_SELECTED_TEXT}$0", ":::"], 117 | "description": "Insert div block" 118 | }, 119 | "Insert span": { 120 | "prefix": "span", 121 | "body": "[${TM_SELECTED_TEXT:${1:text}}]{.${2:class}}$0", 122 | "description": "Insert span" 123 | }, 124 | "Insert callout block": { 125 | "prefix": "callout", 126 | "body": [ 127 | "::: {.${1|callout,callout-note,callout-tip,callout-important,callout-caution,callout-warning|}}", 128 | "${TM_SELECTED_TEXT}$0", 129 | ":::" 130 | ], 131 | "description": "Insert callout block" 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/browser.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from "vscode"; 7 | import { activateCommon } from "./extension"; 8 | import { MarkdownEngine } from "./markdown/engine"; 9 | 10 | export function activate(context: vscode.ExtensionContext) { 11 | // create markdown engine 12 | const engine = new MarkdownEngine(); 13 | 14 | // activate providers common to browser/node 15 | activateCommon(context, engine); 16 | } 17 | -------------------------------------------------------------------------------- /src/core/command.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | 7 | import * as vscode from "vscode"; 8 | 9 | export interface Command { 10 | readonly id: string; 11 | 12 | execute(...args: any[]): void; 13 | } 14 | 15 | export class CommandManager { 16 | private readonly commands = new Map(); 17 | 18 | public dispose() { 19 | for (const registration of this.commands.values()) { 20 | registration.dispose(); 21 | } 22 | this.commands.clear(); 23 | } 24 | 25 | public register(command: T): T { 26 | this.registerCommand(command.id, command.execute, command); 27 | return command; 28 | } 29 | 30 | private registerCommand( 31 | id: string, 32 | impl: (...args: any[]) => void, 33 | thisArg?: any 34 | ) { 35 | if (this.commands.has(id)) { 36 | return; 37 | } 38 | 39 | this.commands.set(id, vscode.commands.registerCommand(id, impl, thisArg)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/core/dispose.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | 7 | import * as vscode from "vscode"; 8 | 9 | export function disposeAll(disposables: vscode.Disposable[]) { 10 | while (disposables.length) { 11 | const item = disposables.pop(); 12 | item?.dispose(); 13 | } 14 | } 15 | 16 | export abstract class Disposable { 17 | private _isDisposed = false; 18 | 19 | protected _disposables: vscode.Disposable[] = []; 20 | 21 | public dispose(): any { 22 | if (this._isDisposed) { 23 | return; 24 | } 25 | this._isDisposed = true; 26 | disposeAll(this._disposables); 27 | } 28 | 29 | protected _register(value: T): T { 30 | if (this._isDisposed) { 31 | value.dispose(); 32 | } else { 33 | this._disposables.push(value); 34 | } 35 | return value; 36 | } 37 | 38 | protected get isDisposed() { 39 | return this._isDisposed; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/core/doc.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | 7 | import * as vscode from "vscode"; 8 | import { Uri } from "vscode"; 9 | import { extname } from "./path"; 10 | export const kQuartoLanguageId = "quarto"; 11 | export const kMarkdownLanguageId = "markdown"; 12 | const kMermaidLanguageId = "mermaid"; 13 | const kDotLanguageId = "dot"; 14 | const kYamlLanguageId = "yaml"; 15 | 16 | export const kQuartoDocSelector: vscode.DocumentSelector = { 17 | language: kQuartoLanguageId, 18 | scheme: "*", 19 | }; 20 | 21 | export function isQuartoDoc(doc?: vscode.TextDocument) { 22 | return ( 23 | isLanguageDoc(kQuartoLanguageId, doc) || 24 | isLanguageDoc(kMarkdownLanguageId, doc) 25 | ); 26 | } 27 | 28 | export function isMermaidDoc(doc?: vscode.TextDocument) { 29 | return isLanguageDoc(kMermaidLanguageId, doc); 30 | } 31 | 32 | export function isGraphvizDoc(doc?: vscode.TextDocument) { 33 | return isLanguageDoc(kDotLanguageId, doc); 34 | } 35 | 36 | function isLanguageDoc(languageId: string, doc?: vscode.TextDocument) { 37 | return !!doc && doc.languageId === languageId; 38 | } 39 | 40 | export function isNotebook(doc?: vscode.TextDocument) { 41 | return !!doc && isNotebookUri(doc.uri); 42 | } 43 | 44 | export function isNotebookUri(uri: Uri) { 45 | return extname(uri.fsPath).toLowerCase() === ".ipynb"; 46 | } 47 | 48 | export function isQuartoYaml(doc?: vscode.TextDocument) { 49 | return ( 50 | !!doc && 51 | doc.languageId === kYamlLanguageId && 52 | doc.uri.toString().match(/_quarto\.ya?ml$/) 53 | ); 54 | } 55 | 56 | export function isMarkdownDoc(document?: vscode.TextDocument) { 57 | return ( 58 | !!document && (isQuartoDoc(document) || document.languageId === "markdown") 59 | ); 60 | } 61 | 62 | export function validatateQuartoExtension(document: vscode.TextDocument) { 63 | const ext = extname(document.uri.toString()).toLowerCase(); 64 | return [".qmd", ".rmd", ".md"].includes(ext); 65 | } 66 | 67 | export async function resolveQuartoDocUri( 68 | resource: vscode.Uri 69 | ): Promise { 70 | try { 71 | const doc = await tryResolveUriToQuartoDoc(resource); 72 | if (doc) { 73 | return doc; 74 | } 75 | } catch { 76 | // Noop 77 | } 78 | 79 | // If no extension, try with `.qmd` extension 80 | if (extname(resource.path) === "") { 81 | return tryResolveUriToQuartoDoc( 82 | resource.with({ path: resource.path + ".qmd" }) 83 | ); 84 | } 85 | 86 | return undefined; 87 | } 88 | 89 | export function getWholeRange(doc: vscode.TextDocument) { 90 | const begin = new vscode.Position(0, 0); 91 | const end = doc.lineAt(doc.lineCount - 1).range.end; 92 | return new vscode.Range(begin, end); 93 | } 94 | 95 | export function preserveEditorFocus(editor?: vscode.TextEditor) { 96 | // focus the editor (sometimes the terminal steals focus) 97 | editor = editor || vscode.window.activeTextEditor; 98 | if (editor) { 99 | if (!isNotebook(editor?.document)) { 100 | setTimeout(() => { 101 | if (editor) { 102 | vscode.window.showTextDocument( 103 | editor.document, 104 | editor.viewColumn, 105 | false 106 | ); 107 | } 108 | }, 200); 109 | } 110 | } 111 | } 112 | 113 | export function findEditor( 114 | filter: (doc: vscode.TextDocument) => boolean, 115 | includeVisible = true 116 | ) { 117 | const activeDoc = vscode.window.activeTextEditor?.document; 118 | if (activeDoc && filter(activeDoc)) { 119 | return vscode.window.activeTextEditor; 120 | } else if (includeVisible) { 121 | const visibleEditor = vscode.window.visibleTextEditors.find((editor) => 122 | filter(editor.document) 123 | ); 124 | if (visibleEditor) { 125 | return visibleEditor; 126 | } else { 127 | return undefined; 128 | } 129 | } else { 130 | return undefined; 131 | } 132 | } 133 | 134 | async function tryResolveUriToQuartoDoc( 135 | resource: vscode.Uri 136 | ): Promise { 137 | let document: vscode.TextDocument; 138 | try { 139 | document = await vscode.workspace.openTextDocument(resource); 140 | } catch { 141 | return undefined; 142 | } 143 | if (isQuartoDoc(document)) { 144 | return document; 145 | } 146 | return undefined; 147 | } 148 | -------------------------------------------------------------------------------- /src/core/git.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as fs from "fs"; 7 | import * as path from "path"; 8 | import * as os from "os"; 9 | 10 | import { lines } from "./text"; 11 | import { execProgram } from "../shared/exec"; 12 | 13 | export async function ensureGitignore( 14 | dir: string, 15 | entries: string[] 16 | ): Promise { 17 | // if .gitignore exists, then ensure it has the requisite entries 18 | const gitignorePath = path.join(dir, ".gitignore"); 19 | if (fs.existsSync(gitignorePath)) { 20 | const gitignore = lines( 21 | fs.readFileSync(gitignorePath, { 22 | encoding: "utf-8", 23 | }) 24 | ).map((line) => line.trim()); 25 | const requiredEntries: string[] = []; 26 | for (const requiredEntry of entries) { 27 | if (!gitignore.includes(requiredEntry)) { 28 | requiredEntries.push(requiredEntry); 29 | } 30 | } 31 | if (requiredEntries.length > 0) { 32 | writeGitignore(dir, gitignore.concat(requiredEntries)); 33 | return true; 34 | } else { 35 | return false; 36 | } 37 | } else { 38 | // if it doesn't exist then auto-create if we are in a git project or we had the force flag 39 | try { 40 | const result = await execProgram("git", ["rev-parse"], { 41 | cwd: dir, 42 | }); 43 | if (result !== undefined) { 44 | createGitignore(dir, entries); 45 | return true; 46 | } else { 47 | return false; 48 | } 49 | } catch { 50 | return false; 51 | } 52 | } 53 | } 54 | 55 | export function createGitignore(dir: string, entries: string[]) { 56 | writeGitignore(dir, entries); 57 | } 58 | 59 | function writeGitignore(dir: string, lines: string[]) { 60 | const lineEnding = os.platform() === "win32" ? "\r\n" : "\n"; 61 | fs.writeFileSync( 62 | path.join(dir, ".gitignore"), 63 | lines.join(lineEnding) + lineEnding, 64 | { encoding: "utf-8" } 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/core/lazy.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | 7 | export interface Lazy { 8 | readonly value: T; 9 | readonly hasValue: boolean; 10 | map(f: (x: T) => R): Lazy; 11 | } 12 | 13 | class LazyValue implements Lazy { 14 | private _hasValue: boolean = false; 15 | private _value?: T; 16 | 17 | constructor(private readonly _getValue: () => T) {} 18 | 19 | get value(): T { 20 | if (!this._hasValue) { 21 | this._hasValue = true; 22 | this._value = this._getValue(); 23 | } 24 | return this._value!; 25 | } 26 | 27 | get hasValue(): boolean { 28 | return this._hasValue; 29 | } 30 | 31 | public map(f: (x: T) => R): Lazy { 32 | return new LazyValue(() => f(this.value)); 33 | } 34 | } 35 | 36 | export function lazy(getValue: () => T): Lazy { 37 | return new LazyValue(getValue); 38 | } 39 | -------------------------------------------------------------------------------- /src/core/links.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | 7 | import * as vscode from "vscode"; 8 | import { Schemes } from "./schemes"; 9 | 10 | const knownSchemes = [...Object.values(Schemes), `${vscode.env.uriScheme}:`]; 11 | 12 | export function getUriForLinkWithKnownExternalScheme( 13 | link: string 14 | ): vscode.Uri | undefined { 15 | if (knownSchemes.some((knownScheme) => isOfScheme(knownScheme, link))) { 16 | return vscode.Uri.parse(link); 17 | } 18 | 19 | return undefined; 20 | } 21 | 22 | export function isOfScheme(scheme: string, link: string): boolean { 23 | return link.toLowerCase().startsWith(scheme); 24 | } 25 | 26 | // https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types#Common_image_file_types 27 | const kImageExtensions = [ 28 | ".apng", 29 | ".bmp", 30 | ".gif", 31 | ".ico", 32 | ".cur", 33 | ".jpg", 34 | ".jpeg", 35 | ".jfif", 36 | ".pjpeg", 37 | ".pjp", 38 | ".png", 39 | ".svg", 40 | ".tif", 41 | ".tiff", 42 | ".webp", 43 | ]; 44 | 45 | export function isImageLink(link: string) { 46 | return kImageExtensions.some((ext) => link.toLowerCase().endsWith(ext)); 47 | } 48 | -------------------------------------------------------------------------------- /src/core/mime.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { extname } from "path"; 7 | 8 | export const kTextHtml = "text/html"; 9 | export const kTextMarkdown = "text/markdown"; 10 | export const kTextXml = "text/xml"; 11 | export const kTextLatex = "text/latex"; 12 | export const kTextPlain = "text/plain"; 13 | export const kImagePng = "image/png"; 14 | export const kImageJpeg = "image/jpeg"; 15 | export const kImageSvg = "image/svg+xml"; 16 | export const kApplicationPdf = "application/pdf"; 17 | export const kApplicationJavascript = "application/javascript"; 18 | export const kApplicationJupyterWidgetState = 19 | "application/vnd.jupyter.widget-state+json"; 20 | export const kApplicationJupyterWidgetView = 21 | "application/vnd.jupyter.widget-view+json"; 22 | 23 | export const kRestructuredText = "text/restructuredtext"; 24 | export const kApplicationRtf = "application/rtf"; 25 | 26 | export function extensionForMimeImageType(mimeType: string) { 27 | switch (mimeType) { 28 | case kImagePng: 29 | return "png"; 30 | case kImageJpeg: 31 | return "jpeg"; 32 | case kImageSvg: 33 | return "svg"; 34 | case kApplicationPdf: 35 | return "pdf"; 36 | default: 37 | return "bin"; 38 | } 39 | } 40 | 41 | export function contentType(path: string): string | undefined { 42 | return MEDIA_TYPES[extname(path.toLowerCase())]; 43 | } 44 | 45 | export function isPdfContent(path?: string) { 46 | return path && contentType(path) === kApplicationPdf; 47 | } 48 | 49 | export function isHtmlContent(path?: string) { 50 | return path && contentType(path) === kTextHtml; 51 | } 52 | 53 | export function isTextContent(path?: string) { 54 | return ( 55 | path && 56 | (contentType(path) === kTextMarkdown || 57 | contentType(path) === kTextPlain || 58 | contentType(path) === kTextXml) 59 | ); 60 | } 61 | 62 | const MEDIA_TYPES: Record = { 63 | ".md": kTextMarkdown, 64 | ".markdown": kTextMarkdown, 65 | ".html": kTextHtml, 66 | ".htm": kTextHtml, 67 | ".json": "application/json", 68 | ".map": "application/json", 69 | ".txt": kTextPlain, 70 | ".tex": kTextPlain, 71 | ".adoc": kTextPlain, 72 | ".asciidoc": kTextPlain, 73 | ".xml": "text/xml", 74 | ".ts": "text/typescript", 75 | ".tsx": "text/tsx", 76 | ".js": "application/javascript", 77 | ".jsx": "text/jsx", 78 | ".gz": "application/gzip", 79 | ".css": "text/css", 80 | ".wasm": "application/wasm", 81 | ".mjs": "application/javascript", 82 | ".svg": kImageSvg, 83 | ".png": kImagePng, 84 | ".jpg": kImageJpeg, 85 | ".jpeg": kImageJpeg, 86 | ".pdf": kApplicationPdf, 87 | ".gif": "image/gif", 88 | ".wav": "audio/wav", 89 | ".mp4": "video/mp4", 90 | ".woff": "application/font-woff", 91 | ".ttf": "application/font-ttf", 92 | ".eot": "application/vnd.ms-fontobject", 93 | ".otf": "application/font-otf", 94 | ".textile": kTextPlain, 95 | ".texinfo": kTextPlain, 96 | ".tei": kTextPlain, 97 | ".rst": kTextPlain, 98 | ".org": kTextPlain, 99 | ".opml": kTextPlain, 100 | ".muse": kTextPlain, 101 | ".ms": kTextPlain, 102 | ".native": kTextPlain, 103 | ".man": kTextPlain, 104 | ".dokuwiki": kTextPlain, 105 | ".haddock": kTextPlain, 106 | ".icml": kTextPlain, 107 | ".jira": kTextPlain, 108 | ".mediawiki": kTextPlain, 109 | ".xwiki": kTextPlain, 110 | ".zim": kTextPlain, 111 | }; 112 | -------------------------------------------------------------------------------- /src/core/path.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | 7 | /// 8 | 9 | export { basename, dirname, extname, isAbsolute, join } from "path"; 10 | -------------------------------------------------------------------------------- /src/core/platform.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as child_process from "child_process"; 7 | 8 | export function isRStudioWorkbench() { 9 | // RS_SERVER_URL e.g. https://daily-rsw.soleng.rstudioservices.com/ 10 | // RS_SESSION_URL e.g. /s/eae053c9ab5a71168ee19/ 11 | return process.env.RS_SERVER_URL && process.env.RS_SESSION_URL; 12 | } 13 | 14 | export function isVSCodeServer() { 15 | return !!vsCodeServerProxyUri(); 16 | } 17 | 18 | export function vsCodeServerProxyUri() { 19 | return process.env.VSCODE_PROXY_URI; 20 | } 21 | 22 | export function vsCodeWebUrl(serverUrl: string) { 23 | const port = new URL(serverUrl).port; 24 | if (isRStudioWorkbench()) { 25 | return rswURL(port); 26 | } else if (isVSCodeServer()) { 27 | return vsCodeServerProxyUri()!.replace("{{port}}", `${port}`); 28 | } else { 29 | return serverUrl; 30 | } 31 | } 32 | 33 | export function rswURL(port: string) { 34 | const server = process.env.RS_SERVER_URL!; 35 | const session = process.env.RS_SESSION_URL!; 36 | const portToken = rswPortToken(port); 37 | const url = `${server}${session.slice(1)}p/${portToken}/`; 38 | return url; 39 | } 40 | 41 | function rswPortToken(port: string) { 42 | try { 43 | const result = child_process.execFileSync( 44 | "/usr/lib/rstudio-server/bin/rserver-url", 45 | [port], 46 | { 47 | encoding: "utf-8", 48 | } 49 | ) as unknown as string; 50 | return result; 51 | } catch (e) { 52 | throw new Error(`Failed to map RSW port token`); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/core/quarto.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import semver from "semver"; 7 | 8 | import { window, env, Uri } from "vscode"; 9 | import { QuartoContext } from "../shared/quarto"; 10 | 11 | export async function withMinimumQuartoVersion( 12 | context: QuartoContext, 13 | version: string, 14 | action: string, 15 | f: () => Promise 16 | ) { 17 | if (context.available) { 18 | if (semver.gte(context.version, version)) { 19 | await f(); 20 | } else { 21 | window.showWarningMessage( 22 | `${action} requires Quarto version ${version} or greater`, 23 | { modal: true } 24 | ); 25 | } 26 | } else { 27 | await promptForQuartoInstallation(action); 28 | } 29 | } 30 | 31 | export async function promptForQuartoInstallation(context: string) { 32 | const installQuarto = { title: "Install Quarto" }; 33 | const result = await window.showWarningMessage( 34 | "Quarto Installation Not Found", 35 | { 36 | modal: true, 37 | detail: `Please install the Quarto CLI before ${context.toLowerCase()}.`, 38 | }, 39 | installQuarto 40 | ); 41 | if (result === installQuarto) { 42 | env.openExternal(Uri.parse("https://quarto.org/docs/get-started/")); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/core/schemes.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | 7 | import { Uri } from "vscode"; 8 | 9 | export const Schemes = { 10 | http: "http:", 11 | https: "https:", 12 | file: "file:", 13 | untitled: "untitled", 14 | mailto: "mailto:", 15 | data: "data:", 16 | vscode: "vscode:", 17 | "vscode-insiders": "vscode-insiders:", 18 | }; 19 | 20 | export function hasFileScheme(uri: Uri) { 21 | return uri.scheme === Schemes.file.slice(0, Schemes.file.length - 1); 22 | } 23 | -------------------------------------------------------------------------------- /src/core/text.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export function lines(text: string): string[] { 7 | return text.split(/\r?\n/); 8 | } 9 | 10 | export function normalizeNewlines(text: string) { 11 | return lines(text).join("\n"); 12 | } 13 | -------------------------------------------------------------------------------- /src/core/wait.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export function sleep(ms: number) { 7 | return new Promise((resolve) => setTimeout(resolve, ms)); 8 | } 9 | -------------------------------------------------------------------------------- /src/core/yaml.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as yaml from "js-yaml"; 7 | 8 | export function parseFrontMatterStr(str: string) { 9 | str = str.replace(/---\s*$/, ""); 10 | try { 11 | return yaml.load(str); 12 | } catch (error) { 13 | return undefined; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | 7 | import * as vscode from "vscode"; 8 | import QuartoLinkProvider, { OpenLinkCommand } from "./providers/link"; 9 | import QuartoDocumentSymbolProvider from "./providers/symbol-document"; 10 | import QuartoFoldingProvider from "./providers/folding"; 11 | import { PathCompletionProvider } from "./providers/completion-path"; 12 | import QuartoSelectionRangeProvider from "./providers/selection-range"; 13 | import QuartoWorkspaceSymbolProvider from "./providers/symbol-workspace"; 14 | import { MarkdownEngine } from "./markdown/engine"; 15 | import { activateBackgroundHighlighter } from "./providers/background"; 16 | import { kQuartoDocSelector } from "./core/doc"; 17 | import { Command, CommandManager } from "./core/command"; 18 | import { newDocumentCommands } from "./providers/newdoc"; 19 | import { insertCommands } from "./providers/insert"; 20 | import { activateDiagram } from "./providers/diagram/diagram"; 21 | import { activateOptionEnterProvider } from "./providers/option"; 22 | import { formattingCommands } from "./providers/format"; 23 | 24 | export function activateCommon( 25 | context: vscode.ExtensionContext, 26 | engine: MarkdownEngine, 27 | commands?: Command[] 28 | ) { 29 | // core language features 30 | const symbolProvider = new QuartoDocumentSymbolProvider(engine); 31 | context.subscriptions.push( 32 | vscode.Disposable.from( 33 | vscode.languages.registerDocumentSymbolProvider( 34 | kQuartoDocSelector, 35 | symbolProvider 36 | ), 37 | vscode.languages.registerDocumentLinkProvider( 38 | kQuartoDocSelector, 39 | new QuartoLinkProvider(engine) 40 | ), 41 | vscode.languages.registerFoldingRangeProvider( 42 | kQuartoDocSelector, 43 | new QuartoFoldingProvider(engine) 44 | ), 45 | vscode.languages.registerSelectionRangeProvider( 46 | kQuartoDocSelector, 47 | new QuartoSelectionRangeProvider(engine) 48 | ), 49 | vscode.languages.registerWorkspaceSymbolProvider( 50 | new QuartoWorkspaceSymbolProvider(symbolProvider) 51 | ), 52 | PathCompletionProvider.register(engine) 53 | ) 54 | ); 55 | 56 | // option enter handler 57 | activateOptionEnterProvider(context, engine); 58 | 59 | // background highlighter 60 | activateBackgroundHighlighter(context, engine); 61 | 62 | // diagramming 63 | const diagramCommands = activateDiagram(context, engine); 64 | 65 | // commands (common + passed) 66 | const commandManager = new CommandManager(); 67 | commandManager.register(new OpenLinkCommand(engine)); 68 | for (const cmd of formattingCommands()) { 69 | commandManager.register(cmd); 70 | } 71 | for (const cmd of newDocumentCommands()) { 72 | commandManager.register(cmd); 73 | } 74 | for (const cmd of insertCommands(engine)) { 75 | commandManager.register(cmd); 76 | } 77 | for (const cmd of diagramCommands) { 78 | commandManager.register(cmd); 79 | } 80 | if (commands) { 81 | for (const cmd of commands) { 82 | commandManager.register(cmd); 83 | } 84 | } 85 | context.subscriptions.push(commandManager); 86 | } 87 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | 7 | import * as vscode from "vscode"; 8 | import * as path from "path"; 9 | import { MarkdownEngine } from "./markdown/engine"; 10 | import { kQuartoDocSelector } from "./core/doc"; 11 | import { activateLsp } from "./lsp/client"; 12 | import { cellCommands } from "./providers/cell/commands"; 13 | import { quartoCellExecuteCodeLensProvider } from "./providers/cell/codelens"; 14 | import { activateQuartoAssistPanel } from "./providers/assist/panel"; 15 | import { activateCommon } from "./extension"; 16 | import { activatePreview } from "./providers/preview/preview"; 17 | import { initQuartoContext } from "./shared/quarto"; 18 | import { activateStatusBar } from "./providers/statusbar"; 19 | import { walkthroughCommands } from "./providers/walkthrough"; 20 | import { activateLuaTypes } from "./providers/lua-types"; 21 | import { activateCreate } from "./providers/create/create"; 22 | import { activatePaste } from "./providers/paste"; 23 | 24 | export async function activate(context: vscode.ExtensionContext) { 25 | // create markdown engine 26 | const engine = new MarkdownEngine(); 27 | 28 | // commands 29 | const commands = cellCommands(engine); 30 | 31 | // get quarto context (some features conditional on it) 32 | const config = vscode.workspace.getConfiguration("quarto"); 33 | const quartoPath = config.get("path") as string | undefined; 34 | const workspaceFolder = vscode.workspace.workspaceFolders?.length 35 | ? vscode.workspace.workspaceFolders[0].uri.fsPath 36 | : undefined; 37 | const quartoContext = initQuartoContext( 38 | quartoPath, 39 | workspaceFolder, 40 | vscode.window.showWarningMessage 41 | ); 42 | if (quartoContext.available) { 43 | // ensure quarto is on the path 44 | context.environmentVariableCollection.prepend( 45 | "PATH", 46 | path.delimiter + quartoContext.binPath + path.delimiter 47 | ); 48 | 49 | // status bar 50 | activateStatusBar(quartoContext); 51 | 52 | // lua types 53 | await activateLuaTypes(context, quartoContext); 54 | 55 | // lsp 56 | activateLsp(context, engine); 57 | 58 | // assist panel 59 | const assistCommands = activateQuartoAssistPanel(context, engine); 60 | commands.push(...assistCommands); 61 | 62 | // walkthough 63 | commands.push(...walkthroughCommands(quartoContext)); 64 | } 65 | 66 | // provide preview 67 | const previewCommands = activatePreview(context, quartoContext, engine); 68 | commands.push(...previewCommands); 69 | 70 | // provide create 71 | const createCommands = await activateCreate(context, quartoContext); 72 | commands.push(...createCommands); 73 | 74 | // provide code lens 75 | vscode.languages.registerCodeLensProvider( 76 | kQuartoDocSelector, 77 | quartoCellExecuteCodeLensProvider(engine) 78 | ); 79 | 80 | // provide paste handling 81 | const pasteCommands = activatePaste(); 82 | commands.push(...pasteCommands); 83 | 84 | // activate providers common to browser/node 85 | activateCommon(context, engine, commands); 86 | } 87 | -------------------------------------------------------------------------------- /src/markdown/document.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | 7 | import * as vscode from "vscode"; 8 | 9 | export interface MarkdownTextLine { 10 | text: string; 11 | } 12 | 13 | export interface MarkdownTextDocument { 14 | readonly uri: vscode.Uri; 15 | readonly version: number; 16 | readonly lineCount: number; 17 | 18 | lineAt(line: number): MarkdownTextLine; 19 | getText(): string; 20 | } 21 | -------------------------------------------------------------------------------- /src/markdown/engine.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | 7 | import MarkdownIt from "markdown-it"; 8 | import Token from "markdown-it/lib/token"; 9 | import containerPlugin from "markdown-it-container"; 10 | import * as vscode from "vscode"; 11 | import { MarkdownTextDocument } from "./document"; 12 | import { mathPlugin } from "../shared/markdownit-math"; 13 | import { frontMatterPlugin } from "../shared/markdownit-yaml"; 14 | 15 | const UNICODE_NEWLINE_REGEX = /\u2028|\u2029/g; 16 | 17 | export class MarkdownEngine { 18 | private md?: MarkdownIt; 19 | 20 | private _tokenCache = new TokenCache(); 21 | 22 | public constructor() {} 23 | 24 | public async parse(document: MarkdownTextDocument): Promise { 25 | const engine = await this.getEngine(); 26 | const tokens = this.tokenizeDocument(document, engine); 27 | return tokens; 28 | } 29 | 30 | // will work only after the engine has been initialized elsewhere 31 | // (returns empty set of tokens if it hasn't) 32 | public parseSync(document: MarkdownTextDocument): Token[] { 33 | if (this.md) { 34 | const tokens = this.tokenizeDocument(document, this.md); 35 | return tokens; 36 | } else { 37 | return []; 38 | } 39 | } 40 | 41 | public cleanCache(): void { 42 | this._tokenCache.clean(); 43 | } 44 | 45 | private async getEngine(): Promise { 46 | if (!this.md) { 47 | this.md = MarkdownIt("zero"); 48 | // tokenize blocks only 49 | this.md.enable([ 50 | "blockquote", 51 | "code", 52 | "fence", 53 | "heading", 54 | "lheading", 55 | "html_block", 56 | "list", 57 | "paragraph", 58 | "hr", 59 | // exclude some blocks we don't care about 60 | // "reference", 61 | ]); 62 | this.md.use(mathPlugin, { 63 | enableInlines: false, 64 | }); 65 | this.md.use(frontMatterPlugin); 66 | this.md.use(containerPlugin, "", { 67 | validate: (_params: string) => { 68 | return true; 69 | }, 70 | }); 71 | } 72 | return this.md; 73 | } 74 | 75 | private tokenizeDocument( 76 | document: MarkdownTextDocument, 77 | engine: MarkdownIt 78 | ): Token[] { 79 | const cached = this._tokenCache.tryGetCached(document); 80 | if (cached) { 81 | return cached; 82 | } 83 | 84 | const tokens = this.tokenizeString(document.getText(), engine); 85 | this._tokenCache.update(document, tokens); 86 | return tokens; 87 | } 88 | 89 | private tokenizeString(text: string, engine: MarkdownIt) { 90 | return engine.parse(text.replace(UNICODE_NEWLINE_REGEX, ""), {}); 91 | } 92 | } 93 | 94 | class TokenCache { 95 | private cachedDocument?: { 96 | readonly uri: vscode.Uri; 97 | readonly version: number; 98 | }; 99 | private tokens?: Token[]; 100 | 101 | public tryGetCached(document: MarkdownTextDocument): Token[] | undefined { 102 | if ( 103 | this.cachedDocument && 104 | this.cachedDocument.uri.toString() === document.uri.toString() && 105 | this.cachedDocument.version === document.version 106 | ) { 107 | return this.tokens; 108 | } 109 | return undefined; 110 | } 111 | 112 | public update(document: MarkdownTextDocument, tokens: Token[]) { 113 | this.cachedDocument = { 114 | uri: document.uri, 115 | version: document.version, 116 | }; 117 | this.tokens = tokens; 118 | } 119 | 120 | public clean(): void { 121 | this.cachedDocument = undefined; 122 | this.tokens = undefined; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/markdown/language.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Position } from "vscode"; 7 | 8 | import Token from "markdown-it/lib/token"; 9 | 10 | export function isLanguageBlock(token: Token) { 11 | return isFencedCode(token) || isDisplayMath(token); 12 | } 13 | 14 | // a language block that will be executed with its results 15 | // inclued in the document (either by an engine or because 16 | // it is a raw or display math block) 17 | export function isExecutableLanguageBlock(token: Token) { 18 | return ( 19 | (isFencedCode(token) && 20 | token.info.match(/^\{=?([a-zA-Z0-9_\-]+)(?: *[ ,].*?)?\}$/)) || 21 | isDisplayMath(token) 22 | ); 23 | } 24 | 25 | export function languageBlockAtPosition( 26 | tokens: Token[], 27 | position: Position, 28 | includeFence = false 29 | ) { 30 | for (const languageBlock of tokens.filter(isExecutableLanguageBlock)) { 31 | if (languageBlock.map) { 32 | let [begin, end] = languageBlock.map; 33 | if (includeFence) { 34 | begin--; 35 | end++; 36 | } 37 | if (position.line > begin && position.line < end - 1) { 38 | return languageBlock; 39 | } 40 | } 41 | } 42 | return undefined; 43 | } 44 | 45 | export function isFencedCode(token: Token) { 46 | return token.type === "fence"; 47 | } 48 | 49 | export function isDisplayMath(token: Token) { 50 | return token.type === "math_block"; 51 | } 52 | 53 | export function isDiagram(token: Token) { 54 | return ( 55 | isExecutableLanguageBlockOf("mermaid")(token) || 56 | isExecutableLanguageBlockOf("dot")(token) 57 | ); 58 | } 59 | 60 | export function languageNameFromBlock(token: Token) { 61 | if (isDisplayMath(token)) { 62 | return "tex"; 63 | } else { 64 | const match = token.info.match(/^\{?=?([a-zA-Z0-9_\-]+)/); 65 | if (match) { 66 | return match[1].split("-").pop() || ""; 67 | } else { 68 | return ""; 69 | } 70 | } 71 | } 72 | 73 | export function isExecutableLanguageBlockOf(language: string) { 74 | return (token: Token) => { 75 | return ( 76 | isExecutableLanguageBlock(token) && 77 | languageNameFromBlock(token) === language 78 | ); 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /src/providers/assist/codelens.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { 7 | CodeLens, 8 | CodeLensProvider, 9 | ProviderResult, 10 | TextDocument, 11 | Range, 12 | CancellationToken, 13 | } from "vscode"; 14 | import { MarkdownEngine } from "../../markdown/engine"; 15 | import { isDisplayMath } from "../../markdown/language"; 16 | 17 | export function quartoLensCodeLensProvider( 18 | engine: MarkdownEngine 19 | ): CodeLensProvider { 20 | return { 21 | provideCodeLenses( 22 | document: TextDocument, 23 | token: CancellationToken 24 | ): ProviderResult { 25 | const lenses: CodeLens[] = []; 26 | const tokens = engine.parseSync(document); 27 | const mathBlocks = tokens.filter(isDisplayMath); 28 | for (let i = 0; i < mathBlocks.length; i++) { 29 | // respect cancellation request 30 | if (token.isCancellationRequested) { 31 | return []; 32 | } 33 | 34 | const block = mathBlocks[i]; 35 | if (block.map) { 36 | // push code lens 37 | const range = new Range(block.map[0], 0, block.map[0], 0); 38 | lenses.push( 39 | ...[ 40 | new CodeLens(range, { 41 | title: "$(zoom-in) Preview", 42 | tooltip: "Preview the rendered LaTeX math", 43 | command: "quarto.previewMath", 44 | arguments: [block.map[0] + 1], 45 | }), 46 | ] 47 | ); 48 | } 49 | } 50 | return lenses; 51 | }, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/providers/assist/commands.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Position, Selection, window, commands } from "vscode"; 7 | import { Command } from "../../core/command"; 8 | import { isQuartoDoc, preserveEditorFocus } from "../../core/doc"; 9 | import { MarkdownEngine } from "../../markdown/engine"; 10 | import { languageBlockAtPosition } from "../../markdown/language"; 11 | import { QuartoAssistViewProvider } from "./webview"; 12 | 13 | export class PreviewMathCommand implements Command { 14 | private static readonly id = "quarto.previewMath"; 15 | public readonly id = PreviewMathCommand.id; 16 | constructor( 17 | private readonly provider_: QuartoAssistViewProvider, 18 | private readonly engine_: MarkdownEngine 19 | ) {} 20 | async execute(line: number): Promise { 21 | if (window.activeTextEditor) { 22 | const doc = window.activeTextEditor.document; 23 | if (isQuartoDoc(doc)) { 24 | // if selection isn't currently in the block then move it there 25 | const tokens = await this.engine_.parse(doc); 26 | const block = languageBlockAtPosition( 27 | tokens, 28 | new Position(line, 0), 29 | true 30 | ); 31 | const selection = window.activeTextEditor.selection; 32 | if ( 33 | block && 34 | block.map && 35 | (selection.active.line < block.map[0] || 36 | selection.active.line >= block.map[1]) 37 | ) { 38 | const selPos = new Position(line, 0); 39 | window.activeTextEditor.selection = new Selection(selPos, selPos); 40 | } 41 | 42 | activateAssistPanel(this.provider_); 43 | } 44 | } 45 | } 46 | } 47 | 48 | export class ShowAssistCommand implements Command { 49 | private static readonly id = "quarto.showAssist"; 50 | public readonly id = ShowAssistCommand.id; 51 | constructor(private readonly provider_: QuartoAssistViewProvider) {} 52 | async execute(): Promise { 53 | activateAssistPanel(this.provider_); 54 | } 55 | } 56 | 57 | function activateAssistPanel(provider: QuartoAssistViewProvider) { 58 | // attempt to activate (if we fail to the view has been closed so 59 | // recreate it by calling focus) 60 | preserveEditorFocus(); 61 | if (!provider.activate()) { 62 | commands.executeCommand("quarto-assist.focus"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/providers/assist/panel.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ExtensionContext, window, languages, commands } from "vscode"; 7 | import { Command } from "../../core/command"; 8 | import { kQuartoDocSelector } from "../../core/doc"; 9 | import { MarkdownEngine } from "../../markdown/engine"; 10 | import { quartoLensCodeLensProvider } from "./codelens"; 11 | import { PreviewMathCommand, ShowAssistCommand } from "./commands"; 12 | import { QuartoAssistViewProvider } from "./webview"; 13 | 14 | export function activateQuartoAssistPanel( 15 | context: ExtensionContext, 16 | engine: MarkdownEngine 17 | ): Command[] { 18 | const provider = new QuartoAssistViewProvider(context, engine); 19 | context.subscriptions.push(provider); 20 | 21 | context.subscriptions.push( 22 | window.registerWebviewViewProvider( 23 | QuartoAssistViewProvider.viewType, 24 | provider 25 | ) 26 | ); 27 | 28 | context.subscriptions.push( 29 | languages.registerCodeLensProvider( 30 | kQuartoDocSelector, 31 | quartoLensCodeLensProvider(engine) 32 | ) 33 | ); 34 | 35 | context.subscriptions.push( 36 | commands.registerCommand("quarto.assist.pin", () => { 37 | provider.pin(); 38 | }) 39 | ); 40 | 41 | context.subscriptions.push( 42 | commands.registerCommand("quarto.assist.unpin", () => { 43 | provider.unpin(); 44 | }) 45 | ); 46 | 47 | return [ 48 | new ShowAssistCommand(provider), 49 | new PreviewMathCommand(provider, engine), 50 | ]; 51 | } 52 | -------------------------------------------------------------------------------- /src/providers/assist/render-cache.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) 2020 Matt Bierner 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | 7 | import { Uri, Range, TextEditor } from "vscode"; 8 | 9 | export type RenderCacheKey = typeof renderCacheKeyNone | EditorRenderCacheKey; 10 | 11 | export const renderCacheKeyNone = { type: "none" } as const; 12 | 13 | export function createRenderCacheKey( 14 | editor: TextEditor | undefined 15 | ): RenderCacheKey { 16 | if (!editor) { 17 | return renderCacheKeyNone; 18 | } 19 | 20 | return new EditorRenderCacheKey( 21 | editor.document.uri, 22 | editor.document.version, 23 | editor.document.getWordRangeAtPosition(editor.selection.active) 24 | ); 25 | } 26 | 27 | export function renderCacheKeyEquals( 28 | a: RenderCacheKey, 29 | b: RenderCacheKey 30 | ): boolean { 31 | if (a === b) { 32 | return true; 33 | } 34 | 35 | if (a.type !== b.type) { 36 | return false; 37 | } 38 | 39 | if (a.type === "none" || b.type === "none") { 40 | return false; 41 | } 42 | 43 | return a.equals(b); 44 | } 45 | 46 | export class EditorRenderCacheKey { 47 | readonly type = "editor"; 48 | 49 | constructor( 50 | public readonly url: Uri, 51 | public readonly version: number, 52 | public readonly wordRange: Range | undefined 53 | ) {} 54 | 55 | public equals(other: EditorRenderCacheKey): boolean { 56 | if (this.url.toString() !== other.url.toString()) { 57 | return false; 58 | } 59 | 60 | if (this.version !== other.version) { 61 | return false; 62 | } 63 | 64 | if (!other.wordRange || !this.wordRange) { 65 | return false; 66 | } 67 | 68 | if (other.wordRange === this.wordRange) { 69 | return true; 70 | } 71 | 72 | return this.wordRange.isEqual(other.wordRange); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/providers/cell/codelens.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | // icon reference: https://code.visualstudio.com/api/references/icons-in-labels 7 | 8 | import { 9 | CancellationToken, 10 | CodeLens, 11 | CodeLensProvider, 12 | ProviderResult, 13 | TextDocument, 14 | Range, 15 | } from "vscode"; 16 | import { MarkdownEngine } from "../../markdown/engine"; 17 | import { languageNameFromBlock } from "../../markdown/language"; 18 | import { blockHasExecutor, hasExecutor } from "./executors"; 19 | 20 | export function quartoCellExecuteCodeLensProvider( 21 | engine: MarkdownEngine 22 | ): CodeLensProvider { 23 | return { 24 | provideCodeLenses( 25 | document: TextDocument, 26 | token: CancellationToken 27 | ): ProviderResult { 28 | const lenses: CodeLens[] = []; 29 | const tokens = engine.parseSync(document); 30 | const executableBlocks = tokens.filter(blockHasExecutor); 31 | for (let i = 0; i < executableBlocks.length; i++) { 32 | // respect cancellation request 33 | if (token.isCancellationRequested) { 34 | return []; 35 | } 36 | 37 | const block = executableBlocks[i]; 38 | if (block.map) { 39 | // detect the language and see if it has a cell executor 40 | const language = languageNameFromBlock(block); 41 | if (!hasExecutor(language)) { 42 | continue; 43 | } 44 | 45 | // push code lens 46 | const range = new Range(block.map[0], 0, block.map[0], 0); 47 | lenses.push( 48 | ...[ 49 | new CodeLens(range, { 50 | title: "$(run) Run Cell", 51 | tooltip: "Execute the code in this cell", 52 | command: "quarto.runCurrentCell", 53 | arguments: [block.map[0] + 1], 54 | }), 55 | ] 56 | ); 57 | if (i < executableBlocks.length - 1) { 58 | lenses.push( 59 | new CodeLens(range, { 60 | title: "Run Next Cell", 61 | tooltip: "Execute the next code cell", 62 | command: "quarto.runNextCell", 63 | arguments: [block.map[0] + 1], 64 | }) 65 | ); 66 | } 67 | if (i > 0) { 68 | lenses.push( 69 | new CodeLens(range, { 70 | title: "Run Above", 71 | tooltip: "Execute the cells above this one", 72 | command: "quarto.runCellsAbove", 73 | arguments: [block.map[0] + 1], 74 | }) 75 | ); 76 | } 77 | } 78 | } 79 | return lenses; 80 | }, 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /src/providers/cell/options.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import Token from "markdown-it/lib/token"; 7 | 8 | import * as yaml from "js-yaml"; 9 | 10 | import { lines } from "../../core/text"; 11 | import { languageNameFromBlock } from "../../markdown/language"; 12 | 13 | export const kExecuteEval = "eval"; 14 | 15 | export function cellOptions(token: Token): Record { 16 | const language = languageNameFromBlock(token); 17 | const source = lines(token.content); 18 | 19 | const commentChars = langCommentChars(language); 20 | const optionPattern = optionCommentPattern(commentChars[0]); 21 | const optionSuffix = commentChars[1] || ""; 22 | 23 | // find the yaml lines 24 | const optionsSource: string[] = []; 25 | const yamlLines: string[] = []; 26 | for (const line of source) { 27 | const optionMatch = line.match(optionPattern); 28 | if (optionMatch) { 29 | if (!optionSuffix || line.trimRight().endsWith(optionSuffix)) { 30 | let yamlOption = line.substring(optionMatch[0].length); 31 | if (optionSuffix) { 32 | yamlOption = yamlOption.trimRight(); 33 | yamlOption = yamlOption.substring( 34 | 0, 35 | yamlOption.length - optionSuffix.length 36 | ); 37 | } 38 | yamlLines.push(yamlOption); 39 | optionsSource.push(line); 40 | continue; 41 | } 42 | } 43 | break; 44 | } 45 | 46 | // parse the yaml 47 | if (yamlLines.length > 0) { 48 | try { 49 | const options = yaml.load(yamlLines.join("\n")); 50 | if ( 51 | typeof options === "object" && 52 | !Array.isArray(options) && 53 | options !== null 54 | ) { 55 | return options; 56 | } else { 57 | return {}; 58 | } 59 | } catch (_e) { 60 | // ignore for invalid yaml 61 | return {}; 62 | } 63 | } else { 64 | return {}; 65 | } 66 | } 67 | 68 | function langCommentChars(lang: string): string[] { 69 | const chars = kLangCommentChars[lang] || "#"; 70 | if (!Array.isArray(chars)) { 71 | return [chars]; 72 | } else { 73 | return chars; 74 | } 75 | } 76 | function optionCommentPattern(comment: string) { 77 | return new RegExp("^" + escapeRegExp(comment) + "\\s*\\| ?"); 78 | } 79 | 80 | const kLangCommentChars: Record = { 81 | r: "#", 82 | python: "#", 83 | julia: "#", 84 | scala: "//", 85 | matlab: "%", 86 | csharp: "//", 87 | fsharp: "//", 88 | c: ["/*", "*/"], 89 | css: ["/*", "*/"], 90 | sas: ["*", ";"], 91 | powershell: "#", 92 | bash: "#", 93 | sql: "--", 94 | mysql: "--", 95 | psql: "--", 96 | lua: "--", 97 | cpp: "//", 98 | cc: "//", 99 | stan: "#", 100 | octave: "#", 101 | fortran: "!", 102 | fortran95: "!", 103 | awk: "#", 104 | gawk: "#", 105 | stata: "*", 106 | java: "//", 107 | groovy: "//", 108 | sed: "#", 109 | perl: "#", 110 | ruby: "#", 111 | tikz: "%", 112 | js: "//", 113 | d3: "//", 114 | node: "//", 115 | sass: "//", 116 | coffee: "#", 117 | go: "//", 118 | asy: "//", 119 | haskell: "--", 120 | dot: "//", 121 | ojs: "//", 122 | apl: "⍝", 123 | }; 124 | 125 | function escapeRegExp(str: string) { 126 | return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string 127 | } 128 | -------------------------------------------------------------------------------- /src/providers/create/create-project.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------- 5 | */ 6 | 7 | import { commands, ExtensionContext, QuickPickItem, Uri, window } from "vscode"; 8 | import { Command } from "../../core/command"; 9 | import { withMinimumQuartoVersion } from "../../core/quarto"; 10 | import { QuartoContext } from "../../shared/quarto"; 11 | import { resolveDirectoryForCreate } from "./directory"; 12 | import { createFirstRun } from "./firstrun"; 13 | 14 | export class CreateProjectCommand implements Command { 15 | constructor( 16 | public readonly id: string, 17 | private readonly context_: ExtensionContext, 18 | private readonly quartoContext_: QuartoContext 19 | ) {} 20 | 21 | async execute() { 22 | await withMinimumQuartoVersion( 23 | this.quartoContext_, 24 | "1.0.0", 25 | "Creating projects", 26 | async () => { 27 | // select project type 28 | const typePick = await selectProjectType(1, 2); 29 | if (!typePick) { 30 | return; 31 | } 32 | 33 | // resolve directory 34 | const projDir = await resolveDirectoryForCreate( 35 | this.context_, 36 | "Project", 37 | "Project Directory Name", 38 | false 39 | ); 40 | if (!projDir) { 41 | return; 42 | } 43 | 44 | // create the project 45 | await createAndOpenProject( 46 | this.context_, 47 | this.quartoContext_, 48 | typePick, 49 | projDir 50 | ); 51 | } 52 | ); 53 | } 54 | } 55 | 56 | async function createAndOpenProject( 57 | context: ExtensionContext, 58 | quartoContext: QuartoContext, 59 | pick: CreateProjectQuickPickItem, 60 | projDir: string 61 | ) { 62 | // create the project 63 | quartoContext.runQuarto({}, "create-project", projDir, "--type", pick.type); 64 | 65 | // write the first run file 66 | createFirstRun(context, projDir, pick.firstRun); 67 | 68 | // open the project 69 | await commands.executeCommand("vscode.openFolder", Uri.file(projDir)); 70 | } 71 | 72 | interface CreateProjectQuickPickItem extends QuickPickItem { 73 | type: string; 74 | name: string; 75 | firstRun: string[]; 76 | } 77 | 78 | function selectProjectType( 79 | step?: number, 80 | totalSteps?: number 81 | ): Promise { 82 | return new Promise((resolve) => { 83 | const defaultType: CreateProjectQuickPickItem = { 84 | type: "default", 85 | name: "Default", 86 | firstRun: ["$(dirname).qmd"], 87 | label: "$(gear) Default Project", 88 | detail: "Simple project with starter document", 89 | alwaysShow: true, 90 | }; 91 | const websiteType: CreateProjectQuickPickItem = { 92 | type: "website", 93 | name: "Website", 94 | firstRun: ["index.qmd"], 95 | label: "$(globe) Website Project", 96 | detail: "Website with index and about pages", 97 | alwaysShow: true, 98 | }; 99 | const blogType: CreateProjectQuickPickItem = { 100 | type: "website:blog", 101 | name: "Blog", 102 | firstRun: ["index.qmd"], 103 | label: "$(preview) Blog Project", 104 | detail: "Blog with index/about pages and posts.", 105 | alwaysShow: true, 106 | }; 107 | const bookType: CreateProjectQuickPickItem = { 108 | type: "book", 109 | name: "Book", 110 | firstRun: ["index.qmd"], 111 | label: "$(book) Book Project", 112 | detail: "Book with chapters and bibliography.", 113 | alwaysShow: true, 114 | }; 115 | const quickPick = window.createQuickPick(); 116 | quickPick.title = "Create Quarto Project"; 117 | quickPick.placeholder = "Select project type"; 118 | quickPick.step = step; 119 | quickPick.totalSteps = totalSteps; 120 | quickPick.items = [defaultType, websiteType, blogType, bookType]; 121 | let accepted = false; 122 | quickPick.onDidAccept(() => { 123 | accepted = true; 124 | quickPick.hide(); 125 | resolve(quickPick.selectedItems[0]); 126 | }); 127 | quickPick.onDidHide(() => { 128 | if (!accepted) { 129 | resolve(undefined); 130 | } 131 | }); 132 | quickPick.show(); 133 | }); 134 | } 135 | -------------------------------------------------------------------------------- /src/providers/create/create.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------- 5 | */ 6 | 7 | import { ExtensionContext, workspace, window, ViewColumn } from "vscode"; 8 | import { QuartoContext } from "../../shared/quarto"; 9 | import { collectFirstRun } from "./firstrun"; 10 | import { CreateProjectCommand } from "./create-project"; 11 | 12 | export async function activateCreate( 13 | context: ExtensionContext, 14 | quartoContext: QuartoContext 15 | ) { 16 | // open documents if there is a first-run file 17 | if (quartoContext.workspaceDir) { 18 | const firstRun = await collectFirstRun(context, quartoContext.workspaceDir); 19 | for (const file of firstRun) { 20 | const doc = await workspace.openTextDocument(file); 21 | await window.showTextDocument(doc, ViewColumn.Active, false); 22 | } 23 | } 24 | 25 | // commands 26 | return [ 27 | new CreateProjectCommand("quarto.createProject", context, quartoContext), 28 | new CreateProjectCommand( 29 | "quarto.fileCreateProject", 30 | context, 31 | quartoContext 32 | ), 33 | ]; 34 | } 35 | -------------------------------------------------------------------------------- /src/providers/create/directory.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------- 5 | */ 6 | 7 | import fs from "fs"; 8 | import path from "path"; 9 | 10 | import { ExtensionContext, Uri, window } from "vscode"; 11 | 12 | export async function resolveDirectoryForCreate( 13 | context: ExtensionContext, 14 | name: string, 15 | subdirTitle: string, 16 | forceSubdirPrompt: boolean 17 | ) { 18 | // select direcotry (see if we have a default parent) 19 | const kDefaultParentDir = `quarto.create${name}.dir`; 20 | const defaultParent = context.globalState.get( 21 | kDefaultParentDir, 22 | undefined 23 | ); 24 | const projFolder = await window.showOpenDialog({ 25 | title: `New ${name} Directory`, 26 | openLabel: `Choose ${name} Directory`, 27 | canSelectFiles: false, 28 | canSelectFolders: true, 29 | canSelectMany: false, 30 | defaultUri: 31 | defaultParent && fs.existsSync(defaultParent) 32 | ? Uri.file(defaultParent) 33 | : undefined, 34 | }); 35 | if (!projFolder) { 36 | return; 37 | } 38 | 39 | // see if we need a sub-directory 40 | let projDir: string | undefined = projFolder[0].fsPath; 41 | let defaultName: string | undefined; 42 | const emptyDir = isDirEmpty(projDir); 43 | 44 | // prompt if the directory is not empty or we are being forced 45 | if (!emptyDir || forceSubdirPrompt) { 46 | // if they gave us an empty dir then that's the default 47 | if (emptyDir) { 48 | defaultName = path.basename(projDir); 49 | projDir = path.dirname(projDir); 50 | } 51 | projDir = await directoryWithSubdir( 52 | subdirTitle, 53 | projDir, 54 | defaultName, 55 | 2, 56 | 2 57 | ); 58 | } 59 | if (!projDir) { 60 | return; 61 | } 62 | 63 | // update the default parent dir 64 | context.globalState.update(kDefaultParentDir, path.dirname(projDir)); 65 | 66 | // return the projDir 67 | return projDir; 68 | } 69 | 70 | function directoryWithSubdir( 71 | title: string, 72 | parentDir: string, 73 | defaultName?: string, 74 | step?: number, 75 | totalSteps?: number 76 | ): Promise { 77 | return new Promise((resolve) => { 78 | const inputBox = window.createInputBox(); 79 | inputBox.title = title; 80 | inputBox.prompt = defaultName 81 | ? path.join(parentDir, defaultName) 82 | : defaultName; 83 | inputBox.placeholder = title; 84 | if (defaultName) { 85 | inputBox.value = defaultName; 86 | } 87 | inputBox.step = step; 88 | inputBox.totalSteps = totalSteps; 89 | inputBox.onDidChangeValue((value) => { 90 | inputBox.prompt = path.join(parentDir, value); 91 | }); 92 | let accepted = false; 93 | inputBox.onDidAccept(() => { 94 | accepted = true; 95 | inputBox.hide(); 96 | resolve( 97 | inputBox.value.length ? path.join(parentDir, inputBox.value) : undefined 98 | ); 99 | }); 100 | inputBox.onDidHide(() => { 101 | if (!accepted) { 102 | resolve(undefined); 103 | } 104 | }); 105 | inputBox.show(); 106 | }); 107 | } 108 | 109 | function isDirEmpty(dirname: string) { 110 | const listing = fs 111 | .readdirSync(dirname) 112 | .filter((file) => path.basename(file) !== ".DS_Store"); 113 | return listing.length === 0; 114 | } 115 | -------------------------------------------------------------------------------- /src/providers/create/firstrun.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------- 5 | */ 6 | 7 | import fs from "fs"; 8 | import path from "path"; 9 | import { ExtensionContext } from "vscode"; 10 | 11 | const kQuartoCreateFirstRun = "quarto.create.firstRun"; 12 | 13 | export function createFirstRun( 14 | context: ExtensionContext, 15 | projectDir: string, 16 | openFiles: string[] 17 | ) { 18 | openFiles = openFiles.map((file) => 19 | path.join(projectDir, file.replace("$(dirname)", path.basename(projectDir))) 20 | ); 21 | context.globalState.update(kQuartoCreateFirstRun, openFiles.join("\n")); 22 | } 23 | 24 | export async function collectFirstRun( 25 | context: ExtensionContext, 26 | projectDir: string 27 | ): Promise { 28 | const firstRun = context.globalState 29 | .get(kQuartoCreateFirstRun, "") 30 | .split("\n") 31 | .filter((file) => file.startsWith(projectDir) && fs.existsSync(file)); 32 | await context.globalState.update(kQuartoCreateFirstRun, undefined); 33 | return firstRun; 34 | } 35 | -------------------------------------------------------------------------------- /src/providers/diagram/codelens.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { 7 | CodeLens, 8 | CodeLensProvider, 9 | ProviderResult, 10 | TextDocument, 11 | Range, 12 | CancellationToken, 13 | } from "vscode"; 14 | import { MarkdownEngine } from "../../markdown/engine"; 15 | import { isDiagram } from "../../markdown/language"; 16 | 17 | export function diagramCodeLensProvider( 18 | engine: MarkdownEngine 19 | ): CodeLensProvider { 20 | return { 21 | provideCodeLenses( 22 | document: TextDocument, 23 | token: CancellationToken 24 | ): ProviderResult { 25 | const lenses: CodeLens[] = []; 26 | const tokens = engine.parseSync(document); 27 | const diagramBlocks = tokens.filter(isDiagram); 28 | for (let i = 0; i < diagramBlocks.length; i++) { 29 | // respect cancellation request 30 | if (token.isCancellationRequested) { 31 | return []; 32 | } 33 | 34 | const block = diagramBlocks[i]; 35 | if (block.map) { 36 | // push code lens 37 | const range = new Range(block.map[0], 0, block.map[0], 0); 38 | lenses.push( 39 | ...[ 40 | new CodeLens(range, { 41 | title: "$(zoom-in) Preview", 42 | tooltip: "Preview the diagram", 43 | command: "quarto.previewDiagram", 44 | arguments: [block.map[0] + 1], 45 | }), 46 | ] 47 | ); 48 | } 49 | } 50 | return lenses; 51 | }, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/providers/diagram/commands.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { commands, Position, window, Selection } from "vscode"; 7 | import { Command } from "../../core/command"; 8 | import { isGraphvizDoc, isMermaidDoc, isQuartoDoc } from "../../core/doc"; 9 | import { MarkdownEngine } from "../../markdown/engine"; 10 | import { 11 | isDiagram, 12 | isDisplayMath, 13 | languageBlockAtPosition, 14 | } from "../../markdown/language"; 15 | import { QuartoDiagramWebviewManager } from "./diagram-webview"; 16 | 17 | export function diagramCommands( 18 | manager: QuartoDiagramWebviewManager, 19 | engine: MarkdownEngine 20 | ): Command[] { 21 | return [ 22 | new PreviewDiagramCommand(manager), 23 | new PreviewShortcutCommand(engine), 24 | ]; 25 | } 26 | 27 | class PreviewDiagramCommand implements Command { 28 | constructor(private readonly manager_: QuartoDiagramWebviewManager) {} 29 | execute(line?: number): void { 30 | // set selection to line 31 | if (line && window.activeTextEditor) { 32 | const selPos = new Position(line, 0); 33 | window.activeTextEditor.selection = new Selection(selPos, selPos); 34 | } 35 | 36 | // ensure diagram view is visible 37 | this.manager_.showDiagram(); 38 | } 39 | 40 | private static readonly id = "quarto.previewDiagram"; 41 | public readonly id = PreviewDiagramCommand.id; 42 | } 43 | 44 | class PreviewShortcutCommand implements Command { 45 | constructor(private readonly engine_: MarkdownEngine) {} 46 | async execute(): Promise { 47 | // first determine whether this is an alias for preview math or preview diagram 48 | if (window.activeTextEditor) { 49 | const doc = window.activeTextEditor.document; 50 | if (isQuartoDoc(doc)) { 51 | // are we in a language block? 52 | const tokens = await this.engine_.parse(doc); 53 | const line = window.activeTextEditor.selection.start.line; 54 | const block = languageBlockAtPosition(tokens, new Position(line, 0)); 55 | if (block) { 56 | if (isDisplayMath(block)) { 57 | commands.executeCommand("quarto.previewMath", line); 58 | return; 59 | } else if (isDiagram(block)) { 60 | commands.executeCommand("quarto.previewDiagram", line); 61 | return; 62 | } 63 | } 64 | } else if (isMermaidDoc(doc) || isGraphvizDoc(doc)) { 65 | commands.executeCommand("quarto.previewDiagram"); 66 | return; 67 | } 68 | } 69 | // info message 70 | window.showInformationMessage( 71 | "No preview available (selection not within an equation or diagram)" 72 | ); 73 | } 74 | 75 | private static readonly id = "quarto.previewShortcut"; 76 | public readonly id = PreviewShortcutCommand.id; 77 | } 78 | -------------------------------------------------------------------------------- /src/providers/diagram/diagram.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ExtensionContext, languages } from "vscode"; 7 | import { Command } from "../../core/command"; 8 | import { kQuartoDocSelector } from "../../core/doc"; 9 | import { MarkdownEngine } from "../../markdown/engine"; 10 | import { diagramCodeLensProvider } from "./codelens"; 11 | import { diagramCommands } from "./commands"; 12 | import { QuartoDiagramWebviewManager } from "./diagram-webview"; 13 | 14 | export function activateDiagram( 15 | context: ExtensionContext, 16 | engine: MarkdownEngine 17 | ): Command[] { 18 | // initiaize manager 19 | const diagramManager = new QuartoDiagramWebviewManager(context, engine); 20 | 21 | // code lens 22 | context.subscriptions.push( 23 | languages.registerCodeLensProvider( 24 | kQuartoDocSelector, 25 | diagramCodeLensProvider(engine) 26 | ) 27 | ); 28 | 29 | // diagram commands 30 | return diagramCommands(diagramManager, engine); 31 | } 32 | -------------------------------------------------------------------------------- /src/providers/hover-image.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import * as path from "path"; 7 | import * as fs from "fs"; 8 | import { 9 | Hover, 10 | Position, 11 | TextDocument, 12 | Range, 13 | MarkdownString, 14 | workspace, 15 | Uri, 16 | } from "vscode"; 17 | import PngImage from "../core/png"; 18 | 19 | const kImagePattern = 20 | /(!\[((!\[[^\]]*?\]\(\s*)([^\s\(\)]+?)\s*\)\]|(?:\\\]|[^\]])*\])\(\s*)(([^\s\(\)]|\([^\s\(\)]*?\))+)\s*(".*?")?\)/g; 21 | 22 | export async function imageHover( 23 | doc: TextDocument, 24 | pos: Position 25 | ): Promise { 26 | const lineRange = new Range(pos.line, 0, pos.line + 1, 0); 27 | const line = doc.getText(lineRange).trimEnd(); 28 | for (const match of line.matchAll(kImagePattern)) { 29 | if ( 30 | match.index !== undefined && 31 | pos.character >= match.index && 32 | pos.character < match.index + match[0].length 33 | ) { 34 | // path can be either document relative or workspace rooted w/ "/" 35 | let imagePath = match[5]; 36 | if (imagePath.startsWith("/") && workspace.workspaceFolders) { 37 | for (const wsFolder of workspace.workspaceFolders) { 38 | const wsRoot = wsFolder.uri.fsPath; 39 | imagePath = path.join(wsRoot, imagePath.slice(1)); 40 | break; 41 | } 42 | } else { 43 | imagePath = path.join(path.dirname(doc.uri.fsPath), imagePath); 44 | } 45 | imagePath = path.normalize(imagePath); 46 | if (fs.existsSync(imagePath)) { 47 | const width = await imageWidth(imagePath); 48 | const widthAttrib = width ? `width="${width}"` : ""; 49 | const content = new MarkdownString( 50 | `` 51 | ); 52 | content.supportHtml = true; 53 | content.isTrusted = true; 54 | return { 55 | contents: [content], 56 | range: lineRange, 57 | }; 58 | } 59 | } 60 | } 61 | 62 | return null; 63 | } 64 | 65 | interface ImageWidthInfo { 66 | file: string; 67 | mtime: number; 68 | width: number; 69 | } 70 | 71 | const kMaxImageWidth = 750; 72 | 73 | const imageWidthCache = new Map(); 74 | 75 | async function imageWidth(file: string) { 76 | if (file.toLowerCase().endsWith(".png")) { 77 | try { 78 | // file uri and modification time 79 | const fileUri = Uri.file(file); 80 | const mtime = await (await workspace.fs.stat(fileUri)).mtime; 81 | 82 | // can we serve the width from the cache? 83 | const cachedWidth = imageWidthCache.get(file); 84 | if (cachedWidth && cachedWidth.mtime === mtime) { 85 | return cachedWidth.width; 86 | } 87 | 88 | // crack the image header and see if we need to adjust the width 89 | const imageData = await workspace.fs.readFile(Uri.file(file)); 90 | const pngImage = new PngImage(imageData); 91 | let width = pngImage.width; 92 | if (pngImage.isHighDpi) { 93 | width = Math.round(width / 2); 94 | } 95 | width = Math.min(width, kMaxImageWidth); 96 | imageWidthCache.set(file, { file, mtime, width }); 97 | return width; 98 | } catch (error) { 99 | console.log(error); 100 | return null; 101 | } 102 | } else { 103 | return null; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/providers/insert.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import { 7 | commands, 8 | window, 9 | workspace, 10 | Range, 11 | Position, 12 | WorkspaceEdit, 13 | } from "vscode"; 14 | import { Command } from "../core/command"; 15 | import { isQuartoDoc } from "../core/doc"; 16 | import { MarkdownEngine } from "../markdown/engine"; 17 | import { 18 | isExecutableLanguageBlock, 19 | languageNameFromBlock, 20 | languageBlockAtPosition, 21 | } from "../markdown/language"; 22 | 23 | export function insertCommands(engine: MarkdownEngine): Command[] { 24 | return [new InsertCodeCellCommand(engine)]; 25 | } 26 | 27 | class InsertCodeCellCommand implements Command { 28 | constructor(private readonly engine_: MarkdownEngine) {} 29 | private static readonly id = "quarto.insertCodeCell"; 30 | public readonly id = InsertCodeCellCommand.id; 31 | 32 | async execute(): Promise { 33 | if (window.activeTextEditor) { 34 | const doc = window.activeTextEditor?.document; 35 | if (doc && isQuartoDoc(doc)) { 36 | // determine most recently used language engien above the cursor 37 | const tokens = await this.engine_.parse(doc); 38 | const cursorLine = window.activeTextEditor?.selection.active.line; 39 | let langauge = ""; 40 | let insertTopPaddingLine = false; 41 | 42 | const pos = new Position(cursorLine, 0); 43 | const block = languageBlockAtPosition(tokens, pos, true); 44 | if (block?.map) { 45 | // cursor is in an executable block 46 | langauge = languageNameFromBlock(block); 47 | insertTopPaddingLine = true; 48 | const moveDown = block.map[1] - cursorLine; 49 | await commands.executeCommand("cursorMove", { 50 | to: "down", 51 | value: moveDown, 52 | }); 53 | } else { 54 | // cursor is not in an executable block 55 | for (const executableBlock of tokens.filter( 56 | isExecutableLanguageBlock 57 | )) { 58 | // if this is past the cursor then terminate 59 | if (executableBlock.map && executableBlock.map[0] > cursorLine) { 60 | if (!langauge) { 61 | langauge = languageNameFromBlock(executableBlock); 62 | } 63 | break; 64 | } else { 65 | langauge = languageNameFromBlock(executableBlock); 66 | } 67 | } 68 | 69 | // advance to next blank line if we need to 70 | const currentLine = doc 71 | .getText(new Range(cursorLine, 0, cursorLine + 1, 0)) 72 | .trim(); 73 | if (currentLine.length !== 0) { 74 | insertTopPaddingLine = true; 75 | await commands.executeCommand("cursorMove", { 76 | to: "nextBlankLine", 77 | }); 78 | } 79 | } 80 | 81 | // finally, if we are on the last line of the buffer or the line before us 82 | // has content on it then make sure to insert top padding line 83 | if (cursorLine === window.activeTextEditor.document.lineCount - 1) { 84 | insertTopPaddingLine = true; 85 | } 86 | if (cursorLine > 0) { 87 | const prevLine = doc 88 | .getText(new Range(cursorLine - 1, 0, cursorLine, 0)) 89 | .trim(); 90 | if (prevLine.length > 0) { 91 | insertTopPaddingLine = true; 92 | } 93 | } 94 | 95 | // insert the code cell 96 | const edit = new WorkspaceEdit(); 97 | const kPrefix = "```{"; 98 | edit.insert( 99 | doc.uri, 100 | window.activeTextEditor.selection.active, 101 | (insertTopPaddingLine ? "\n" : "") + kPrefix + langauge + "}\n\n```\n" 102 | ); 103 | await workspace.applyEdit(edit); 104 | await commands.executeCommand("cursorMove", { 105 | to: "up", 106 | value: langauge ? 2 : 3, 107 | }); 108 | if (!langauge) { 109 | await commands.executeCommand("cursorMove", { 110 | to: "right", 111 | value: kPrefix.length, 112 | }); 113 | } 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/providers/newdoc.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import { 7 | workspace, 8 | window, 9 | commands, 10 | NotebookData, 11 | NotebookCellData, 12 | NotebookCellKind, 13 | WorkspaceEdit, 14 | ViewColumn, 15 | } from "vscode"; 16 | import { Command } from "../core/command"; 17 | import { getWholeRange, kQuartoLanguageId } from "../core/doc"; 18 | 19 | export function newDocumentCommands(): Command[] { 20 | return [ 21 | new NewDocumentCommand("quarto.newDocument"), 22 | new NewDocumentCommand("quarto.fileNewDocument"), 23 | new NewPresentationCommand("quarto.newPresentation"), 24 | new NewPresentationCommand("quarto.fileNewPresentation"), 25 | new NewNotebookCommand("quarto.newNotebook"), 26 | new NewNotebookCommand("quarto.fileNewNotebook"), 27 | ]; 28 | } 29 | 30 | class NewNotebookCommand implements Command { 31 | public readonly id: string; 32 | constructor(cmdId: string) { 33 | this.id = cmdId; 34 | } 35 | async execute(): Promise { 36 | const cells: NotebookCellData[] = []; 37 | cells.push( 38 | new NotebookCellData( 39 | NotebookCellKind.Code, 40 | kUntitledHtml.trimEnd(), 41 | "raw" 42 | ) 43 | ); 44 | cells.push(new NotebookCellData(NotebookCellKind.Code, "1 + 1", "python")); 45 | const nbData = new NotebookData(cells); 46 | let notebook = await workspace.openNotebookDocument( 47 | "jupyter-notebook", 48 | nbData 49 | ); 50 | await commands.executeCommand( 51 | "vscode.openWith", 52 | notebook.uri, 53 | "jupyter-notebook" 54 | ); 55 | 56 | const cell = notebook.cellAt(1); 57 | const edit = new WorkspaceEdit(); 58 | edit.replace(cell.document.uri, getWholeRange(cell.document), ""); 59 | 60 | await workspace.applyEdit(edit); 61 | } 62 | } 63 | 64 | abstract class NewFileCommand implements Command { 65 | public readonly id: string; 66 | constructor(cmdId: string, private readonly viewColumn_?: ViewColumn) { 67 | this.id = cmdId; 68 | } 69 | async execute(): Promise { 70 | const doc = await workspace.openTextDocument({ 71 | language: kQuartoLanguageId, 72 | content: this.scaffold(), 73 | }); 74 | await window.showTextDocument(doc, this.viewColumn_, false); 75 | await commands.executeCommand("cursorMove", { to: "viewPortBottom" }); 76 | } 77 | protected abstract scaffold(): string; 78 | } 79 | 80 | class NewDocumentCommand extends NewFileCommand { 81 | constructor(cmdId: string) { 82 | super(cmdId); 83 | } 84 | protected scaffold(): string { 85 | return kUntitledHtml; 86 | } 87 | } 88 | 89 | class NewPresentationCommand extends NewFileCommand { 90 | constructor(cmdId: string) { 91 | super(cmdId); 92 | } 93 | protected scaffold(): string { 94 | return `--- 95 | title: "Untitled" 96 | format: revealjs 97 | --- 98 | 99 | `; 100 | } 101 | } 102 | 103 | const kUntitledHtml = `--- 104 | title: "Untitled" 105 | format: html 106 | --- 107 | 108 | `; 109 | -------------------------------------------------------------------------------- /src/providers/option.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { 7 | window, 8 | workspace, 9 | ExtensionContext, 10 | Position, 11 | TextEditor, 12 | Range, 13 | } from "vscode"; 14 | import { isQuartoDoc } from "../core/doc"; 15 | 16 | import { MarkdownEngine } from "../markdown/engine"; 17 | import { 18 | languageBlockAtPosition, 19 | languageNameFromBlock, 20 | } from "../markdown/language"; 21 | 22 | export function activateOptionEnterProvider( 23 | context: ExtensionContext, 24 | engine: MarkdownEngine 25 | ) { 26 | workspace.onDidChangeTextDocument( 27 | async (event) => { 28 | if (window.activeTextEditor) { 29 | // if we are in an active quarto doc with an empty selection 30 | const doc = window.activeTextEditor.document; 31 | if ( 32 | doc.uri === event.document.uri && 33 | isQuartoDoc(doc) && 34 | window.activeTextEditor.selection.isEmpty 35 | ) { 36 | // check for enter key within a language block 37 | for (const change of event.contentChanges) { 38 | if (change.text === "\n" || change.text === "\r\n") { 39 | const tokens = await engine.parse(doc); 40 | const line = window.activeTextEditor.selection.start.line; 41 | const block = languageBlockAtPosition( 42 | tokens, 43 | new Position(line, 0) 44 | ); 45 | if (block) { 46 | const language = languageNameFromBlock(block); 47 | // handle option enter for the this langauge if we can 48 | const optionComment = languageOptionComment(language); 49 | if (optionComment) { 50 | handleOptionEnter(window.activeTextEditor, optionComment); 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | }, 58 | null, 59 | context.subscriptions 60 | ); 61 | } 62 | 63 | function handleOptionEnter(editor: TextEditor, comment: string) { 64 | // option comment for this language 65 | const optionComment = comment + "| "; 66 | 67 | // get current line 68 | const currentLineNumber = editor.selection.active.line; 69 | const currentLine = editor.document 70 | .getText(new Range(currentLineNumber, 0, currentLineNumber + 1, 0)) 71 | .trim(); 72 | 73 | // if the current line is empty then we might qualify for some auto insert/delete 74 | if (currentLine.length === 0) { 75 | // get the previous line 76 | const previousLine = editor.document 77 | .getText(new Range(currentLineNumber - 1, 0, currentLineNumber, 0)) 78 | .trim(); 79 | if (previousLine.trim() === optionComment.trim()) { 80 | // previous line is an empty option comment -- remove the comment 81 | editor.edit((builder) => { 82 | builder.replace( 83 | new Range( 84 | new Position(editor.selection.active.line - 1, 0), 85 | new Position(editor.selection.active.line, 0) 86 | ), 87 | "\n" 88 | ); 89 | }); 90 | } else if (previousLine.startsWith(optionComment)) { 91 | // previous line starts with option comment -- start this line with a comment 92 | editor.edit((builder) => { 93 | builder.insert(editor.selection.end!, optionComment); 94 | }); 95 | } 96 | } 97 | } 98 | 99 | function languageOptionComment(langauge: string) { 100 | if (Object.keys(kLangCommentChars).includes(langauge)) { 101 | return kLangCommentChars[langauge]; 102 | } else { 103 | return undefined; 104 | } 105 | } 106 | 107 | const kLangCommentChars: Record = { 108 | r: "#", 109 | python: "#", 110 | julia: "#", 111 | scala: "//", 112 | matlab: "%", 113 | csharp: "//", 114 | fsharp: "//", 115 | powershell: "#", 116 | bash: "#", 117 | sql: "--", 118 | mysql: "--", 119 | psql: "--", 120 | lua: "--", 121 | cpp: "//", 122 | cc: "//", 123 | stan: "#", 124 | octave: "#", 125 | fortran: "!", 126 | fortran95: "!", 127 | awk: "#", 128 | gawk: "#", 129 | stata: "*", 130 | java: "//", 131 | groovy: "//", 132 | sed: "#", 133 | perl: "#", 134 | ruby: "#", 135 | tikz: "%", 136 | js: "//", 137 | d3: "//", 138 | node: "//", 139 | sass: "//", 140 | coffee: "#", 141 | go: "//", 142 | asy: "//", 143 | haskell: "--", 144 | dot: "//", 145 | mermaid: "%%", 146 | }; 147 | -------------------------------------------------------------------------------- /src/providers/paste.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) 2017 张宇 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | 7 | import { window, commands, env } from "vscode"; 8 | 9 | import { Command } from "../core/command"; 10 | 11 | export function activatePaste() { 12 | return [new QuartoPasteCommand()]; 13 | } 14 | 15 | export class QuartoPasteCommand implements Command { 16 | constructor() {} 17 | private static readonly id = "quarto.paste"; 18 | public readonly id = QuartoPasteCommand.id; 19 | 20 | async execute() { 21 | const editor = window.activeTextEditor!; 22 | const selection = editor.selection; 23 | if ( 24 | selection.isSingleLine && 25 | !isSingleLink(editor.document.getText(selection)) 26 | ) { 27 | const text = await env.clipboard.readText(); 28 | if (isSingleLink(text)) { 29 | return commands.executeCommand("editor.action.insertSnippet", { 30 | snippet: `[$TM_SELECTED_TEXT$0](${text})`, 31 | }); 32 | } 33 | } 34 | return commands.executeCommand("editor.action.clipboardPasteAction"); 35 | } 36 | } 37 | 38 | /** 39 | * Checks if the string is a link. This code ported from django's 40 | * [URLValidator](https://github.com/django/django/blob/2.2b1/django/core/validators.py#L74) 41 | * with some simplifiations. 42 | */ 43 | 44 | function isSingleLink(text: string): boolean { 45 | return singleLinkRegex.test(text); 46 | } 47 | const singleLinkRegex: RegExp = createLinkRegex(); 48 | 49 | function createLinkRegex(): RegExp { 50 | // unicode letters range(must not be a raw string) 51 | const ul = "\\u00a1-\\uffff"; 52 | // IP patterns 53 | const ipv4_re = 54 | "(?:25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}"; 55 | const ipv6_re = "\\[[0-9a-f:\\.]+\\]"; // simple regex (in django it is validated additionally) 56 | 57 | // Host patterns 58 | const hostname_re = 59 | "[a-z" + ul + "0-9](?:[a-z" + ul + "0-9-]{0,61}[a-z" + ul + "0-9])?"; 60 | // Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1 61 | const domain_re = "(?:\\.(?!-)[a-z" + ul + "0-9-]{1,63}(? path.join(binDir, file)).some(fs.existsSync) 32 | ) { 33 | return true; 34 | } 35 | 36 | // look for conda env 37 | const args = [ 38 | "-c", 39 | "import sys, os; print(os.path.exists(os.path.join(sys.prefix, 'conda-meta')))", 40 | ]; 41 | const output = ( 42 | child_process.execFileSync(shQuote(env.QUARTO_PYTHON), args, { 43 | encoding: "utf-8", 44 | }) as unknown as string 45 | ).trim(); 46 | return output === "True"; 47 | } else { 48 | return false; 49 | } 50 | } catch (err) { 51 | console.error(err); 52 | return false; 53 | } 54 | } 55 | 56 | export function previewEnvsEqual(a?: PreviewEnv, b?: PreviewEnv) { 57 | return ( 58 | a !== undefined && 59 | b !== undefined && 60 | a?.QUARTO_LOG === b?.QUARTO_LOG && 61 | a?.QUARTO_RENDER_TOKEN === b?.QUARTO_RENDER_TOKEN && 62 | a?.QUARTO_PYTHON === b?.QUARTO_PYTHON && 63 | a?.QUARTO_R === b?.QUARTO_R 64 | ); 65 | } 66 | 67 | export class PreviewEnvManager { 68 | constructor( 69 | outputSink: PreviewOutputSink, 70 | private readonly renderToken_: string 71 | ) { 72 | this.outputFile_ = outputSink.outputFile(); 73 | } 74 | 75 | public async previewEnv(uri: Uri) { 76 | // get workspace for uri (if any) 77 | const workspaceFolder = workspace.getWorkspaceFolder(uri); 78 | 79 | // base env 80 | const env: PreviewEnv = { 81 | QUARTO_LOG: this.outputFile_, 82 | QUARTO_RENDER_TOKEN: this.renderToken_, 83 | }; 84 | // QUARTO_PYTHON 85 | const pyExtension = extensions.getExtension("ms-python.python"); 86 | if (pyExtension) { 87 | if (!pyExtension.isActive) { 88 | await pyExtension.activate(); 89 | } 90 | 91 | const execDetails = pyExtension.exports.settings.getExecutionDetails( 92 | workspaceFolder?.uri 93 | ); 94 | if (Array.isArray(execDetails?.execCommand)) { 95 | env.QUARTO_PYTHON = execDetails.execCommand[0]; 96 | } 97 | } 98 | 99 | // QUARTO_R 100 | const rExtension = 101 | extensions.getExtension("REditorSupport.r") || 102 | extensions.getExtension("Ikuyadeu.r"); 103 | if (rExtension) { 104 | const rPath = workspace.getConfiguration("r.rpath", workspaceFolder?.uri); 105 | let quartoR: string | undefined; 106 | switch (os.platform()) { 107 | case "win32": { 108 | quartoR = rPath.get("windows"); 109 | break; 110 | } 111 | case "darwin": { 112 | quartoR = rPath.get("mac"); 113 | break; 114 | } 115 | case "linux": { 116 | quartoR = rPath.get("linux"); 117 | break; 118 | } 119 | } 120 | if (quartoR) { 121 | env.QUARTO_R = quartoR; 122 | } 123 | } 124 | 125 | return env; 126 | } 127 | private readonly outputFile_: string; 128 | } 129 | -------------------------------------------------------------------------------- /src/providers/preview/preview-errors.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as path from "path"; 7 | import * as fs from "fs"; 8 | import { normalizeNewlines } from "../../core/text"; 9 | 10 | export type ErrorLocation = { 11 | lineBegin: number; 12 | lineEnd: number; 13 | file: string; 14 | }; 15 | 16 | export function luaErrorLocation( 17 | output: string, 18 | previewTarget: string, 19 | _previewDir: string 20 | ) { 21 | const luaPattern = /Error running filter ([^:]+):\r?\n[^:]+:(\d+):/; 22 | const luaMatch = output.match(luaPattern); 23 | if (luaMatch) { 24 | console.log(luaMatch); 25 | // if the path is relative then resolve it visa vi the previewTarget 26 | const file = path.isAbsolute(luaMatch[1]) 27 | ? luaMatch[1] 28 | : path.normalize(path.join(path.dirname(previewTarget), luaMatch[1])); 29 | return { 30 | lineBegin: parseInt(luaMatch[2]), 31 | lineEnd: parseInt(luaMatch[2]), 32 | file, 33 | }; 34 | } 35 | 36 | return null; 37 | } 38 | 39 | export function knitrErrorLocation( 40 | output: string, 41 | previewTarget: string, 42 | _previewDir: string 43 | ): ErrorLocation | null { 44 | const knitrPattern = /Quitting from lines (\d+)-(\d+) \(([^)]+)\)/; 45 | const knitrMatch = output.match(knitrPattern); 46 | if (knitrMatch) { 47 | return { 48 | lineBegin: parseInt(knitrMatch[1]), 49 | lineEnd: parseInt(knitrMatch[2]), 50 | file: path.join(path.dirname(previewTarget), knitrMatch[3]), 51 | }; 52 | } 53 | return null; 54 | } 55 | 56 | export function jupyterErrorLocation( 57 | output: string, 58 | previewTarget: string, 59 | _previewDir: string 60 | ): ErrorLocation | null { 61 | const jupyterPattern = 62 | /An error occurred while executing the following cell:\s+(-{3,})\s+([\S\s]+?)\r?\n(\1)[\S\s]+line (\d+)\)/; 63 | const jupyterMatch = output.match(jupyterPattern); 64 | if (jupyterMatch) { 65 | // read target file and searh for the match (normalized) 66 | if (fs.statSync(previewTarget).isFile()) { 67 | const cellSrc = jupyterMatch[2]; 68 | const previewSrc = normalizeNewlines( 69 | fs.readFileSync(previewTarget, { 70 | encoding: "utf-8", 71 | }) 72 | ); 73 | const cellLoc = previewSrc.indexOf(cellSrc); 74 | if (cellLoc !== -1) { 75 | const lineBegin = 76 | previewSrc.slice(0, cellLoc).split("\n").length + 77 | parseInt(jupyterMatch[4]) - 78 | 1; 79 | return { 80 | lineBegin, 81 | lineEnd: lineBegin, 82 | file: previewTarget, 83 | }; 84 | } 85 | } 86 | } 87 | return null; 88 | } 89 | 90 | export function yamlErrorLocation( 91 | output: string, 92 | _previewTarget: string, 93 | previewDir: string 94 | ): ErrorLocation | null { 95 | const yamlPattern = 96 | /\(ERROR\) Validation of YAML.*\n\(ERROR\) In file (.*?)\n\(line (\d+)/; 97 | const yamlMatch = output.match(yamlPattern); 98 | if (yamlMatch) { 99 | const lineBegin = parseInt(yamlMatch[2]); 100 | return { 101 | lineBegin, 102 | lineEnd: lineBegin, 103 | file: path.join(previewDir, yamlMatch[1]), 104 | }; 105 | } 106 | return null; 107 | } 108 | -------------------------------------------------------------------------------- /src/providers/preview/preview-output.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as tmp from "tmp"; 7 | import * as path from "path"; 8 | import * as fs from "fs"; 9 | 10 | export class PreviewOutputSink { 11 | constructor( 12 | readonly handler_: (output: string) => Promise, 13 | readonly tick_: () => Promise 14 | ) { 15 | // allocate a directory for preview output 16 | tmp.setGracefulCleanup(); 17 | const previewDir = tmp.dirSync({ prefix: "quarto-preview" }); 18 | this.outputFile_ = path.join(previewDir.name, "preview.log"); 19 | 20 | // watch for changes 21 | setInterval(async () => { 22 | const lastModified = fs.existsSync(this.outputFile_) 23 | ? fs.statSync(this.outputFile_).mtimeMs 24 | : 0; 25 | if (lastModified > this.lastModified_) { 26 | this.lastModified_ = lastModified; 27 | await this.readOutput(); 28 | } 29 | await this.tick_(); 30 | }, 200); 31 | } 32 | 33 | public dispose() { 34 | this.reset(); 35 | } 36 | 37 | public outputFile() { 38 | return this.outputFile_; 39 | } 40 | 41 | public reset() { 42 | try { 43 | if (this.outputFd_ !== -1) { 44 | fs.closeSync(this.outputFd_); 45 | this.outputFd_ = -1; 46 | } 47 | if (fs.existsSync(this.outputFile_)) { 48 | fs.unlinkSync(this.outputFile_); 49 | } 50 | } catch (e) { 51 | } finally { 52 | this.lastModified_ = 0; 53 | } 54 | } 55 | 56 | private async readOutput() { 57 | // open file on demand 58 | if (this.outputFd_ === -1) { 59 | try { 60 | this.outputFd_ = fs.openSync(this.outputFile_, "r"); 61 | } catch (error) { 62 | console.log("error opening preview output file"); 63 | console.error(error); 64 | return; 65 | } 66 | } 67 | const kBufferSize = 2048; 68 | const buffer = new Buffer(kBufferSize); 69 | const readBuffer = () => { 70 | return fs.readSync(this.outputFd_, buffer, 0, kBufferSize, null); 71 | }; 72 | let bytesRead = readBuffer(); 73 | while (bytesRead > 0) { 74 | await this.handler_(buffer.toString("utf8", 0, bytesRead)); 75 | bytesRead = readBuffer(); 76 | } 77 | } 78 | 79 | private lastModified_ = 0; 80 | private outputFd_ = -1; 81 | private readonly outputFile_: string; 82 | } 83 | -------------------------------------------------------------------------------- /src/providers/preview/preview-reveal.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Position, TextDocument } from "vscode"; 7 | 8 | import { MarkdownEngine } from "../../markdown/engine"; 9 | import { getHeaderLevel } from "../../markdown/toc"; 10 | import { parseFrontMatterStr } from "../../core/yaml"; 11 | 12 | export async function revealSlideIndex( 13 | cursorPos: Position, 14 | doc: TextDocument, 15 | engine: MarkdownEngine 16 | ) { 17 | const location = await revealEditorLocation(cursorPos, doc, engine); 18 | let slideIndex = -1; 19 | for (const item of location.items) { 20 | if (item.type === kCursor) { 21 | return Math.max(slideIndex, 0); 22 | } else if (item.type === kTitle || item.type === kHr) { 23 | slideIndex++; 24 | } else if (item.type === kHeading && item.level <= location.slideLevel) { 25 | slideIndex++; 26 | } 27 | } 28 | return 0; 29 | } 30 | 31 | const kTitle = "title"; 32 | const kHeading = "heading"; 33 | const kHr = "hr"; 34 | const kCursor = "cursor"; 35 | 36 | interface RevealEditorLocation { 37 | items: RevealEditorLocationItem[]; 38 | slideLevel: number; 39 | } 40 | 41 | type RevealEditorLocationItemType = 42 | | typeof kTitle 43 | | typeof kHeading 44 | | typeof kHr 45 | | typeof kCursor; 46 | 47 | interface RevealEditorLocationItem { 48 | type: RevealEditorLocationItemType; 49 | level: number; 50 | row: number; 51 | } 52 | 53 | async function revealEditorLocation( 54 | cursorPos: Position, 55 | doc: TextDocument, 56 | engine: MarkdownEngine 57 | ): Promise { 58 | const items: RevealEditorLocationItem[] = []; 59 | let explicitSlideLevel: number | null = null; 60 | let foundCursor = false; 61 | const tokens = await engine.parse(doc); 62 | for (const token of tokens) { 63 | if (token.map) { 64 | // if the cursor is before this token then add the cursor item 65 | const row = token.map[0]; 66 | if (!foundCursor && cursorPos.line < row) { 67 | foundCursor = true; 68 | items.push(cursorItem(cursorPos.line)); 69 | } 70 | if (token.type === "front_matter") { 71 | explicitSlideLevel = slideLevelFromYaml(token.markup); 72 | items.push(titleItem(0)); 73 | } else if (token.type === "hr") { 74 | items.push(hrItem(row)); 75 | } else if (token.type === "heading_open") { 76 | const level = getHeaderLevel(token.markup); 77 | items.push(headingItem(row, level)); 78 | } 79 | } 80 | } 81 | 82 | // put cursor at end if its not found 83 | if (!foundCursor) { 84 | items.push(cursorItem(doc.lineCount - 1)); 85 | } 86 | 87 | return { items, slideLevel: explicitSlideLevel || 2 }; 88 | } 89 | 90 | function slideLevelFromYaml(str: string) { 91 | try { 92 | const meta = parseFrontMatterStr(str); 93 | if (meta) { 94 | const kSlideLevel = "slide-level"; 95 | return ( 96 | meta[kSlideLevel] || meta["format"]?.["revealjs"]?.[kSlideLevel] || null 97 | ); 98 | } else { 99 | return null; 100 | } 101 | } catch (error) { 102 | return null; 103 | } 104 | } 105 | 106 | function titleItem(row: number): RevealEditorLocationItem { 107 | return simpleItem(kTitle, row); 108 | } 109 | 110 | function cursorItem(row: number): RevealEditorLocationItem { 111 | return simpleItem(kCursor, row); 112 | } 113 | 114 | function hrItem(row: number): RevealEditorLocationItem { 115 | return simpleItem(kHr, row); 116 | } 117 | 118 | function headingItem(row: number, level: number): RevealEditorLocationItem { 119 | return { 120 | type: kHeading, 121 | level, 122 | row, 123 | }; 124 | } 125 | 126 | function simpleItem( 127 | type: RevealEditorLocationItemType, 128 | row: number 129 | ): RevealEditorLocationItem { 130 | return { 131 | type, 132 | level: 0, 133 | row, 134 | }; 135 | } 136 | -------------------------------------------------------------------------------- /src/providers/statusbar.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import { StatusBarAlignment, window } from "vscode"; 7 | import { QuartoContext } from "../shared/quarto"; 8 | 9 | export function activateStatusBar(quartoContext: QuartoContext) { 10 | const statusItem = window.createStatusBarItem( 11 | "quarto.version", 12 | StatusBarAlignment.Left 13 | ); 14 | statusItem.name = "Quarto"; 15 | statusItem.text = `Quarto: ${quartoContext.version}`; 16 | statusItem.tooltip = `${statusItem.text} (${quartoContext.binPath})`; 17 | statusItem.show(); 18 | } 19 | -------------------------------------------------------------------------------- /src/providers/symbol-document.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | *--------------------------------------------------------------------------------------------*/ 6 | 7 | import * as vscode from "vscode"; 8 | import { MarkdownTextDocument } from "../markdown/document"; 9 | import { MarkdownEngine } from "../markdown/engine"; 10 | import { 11 | MarkdownTableOfContents, 12 | TocEntry, 13 | TocEntryType, 14 | } from "../markdown/toc"; 15 | 16 | interface MarkdownSymbol { 17 | readonly level: number; 18 | readonly parent: MarkdownSymbol | undefined; 19 | readonly children: vscode.DocumentSymbol[]; 20 | } 21 | 22 | export default class QuartoDocumentSymbolProvider 23 | implements vscode.DocumentSymbolProvider 24 | { 25 | constructor(private readonly engine: MarkdownEngine) {} 26 | 27 | public async provideDocumentSymbolInformation( 28 | document: MarkdownTextDocument 29 | ): Promise { 30 | const toc = await MarkdownTableOfContents.create(this.engine, document); 31 | return toc.entries.map((entry) => this.toSymbolInformation(entry)); 32 | } 33 | 34 | public async provideDocumentSymbols( 35 | document: MarkdownTextDocument 36 | ): Promise { 37 | const toc = await MarkdownTableOfContents.create(this.engine, document); 38 | const root: MarkdownSymbol = { 39 | level: -Infinity, 40 | children: [], 41 | parent: undefined, 42 | }; 43 | this.buildTree(root, toc.entries); 44 | return root.children; 45 | } 46 | 47 | private buildTree(parent: MarkdownSymbol, entries: readonly TocEntry[]) { 48 | if (!entries.length) { 49 | return; 50 | } 51 | 52 | const entry = entries[0]; 53 | const symbol = this.toDocumentSymbol(entry); 54 | symbol.children = []; 55 | 56 | while (parent && entry.level <= parent.level) { 57 | parent = parent.parent!; 58 | } 59 | parent.children.push(symbol); 60 | this.buildTree( 61 | { level: entry.level, children: symbol.children, parent }, 62 | entries.slice(1) 63 | ); 64 | } 65 | 66 | private toSymbolInformation(entry: TocEntry): vscode.SymbolInformation { 67 | return new vscode.SymbolInformation( 68 | this.getSymbolName(entry), 69 | this.tocEntrySymbolKind(entry), 70 | "", 71 | entry.location 72 | ); 73 | } 74 | 75 | private toDocumentSymbol(entry: TocEntry) { 76 | return new vscode.DocumentSymbol( 77 | this.getSymbolName(entry), 78 | "", 79 | this.tocEntrySymbolKind(entry), 80 | entry.location.range, 81 | entry.location.range 82 | ); 83 | } 84 | 85 | private getSymbolName(entry: TocEntry): string { 86 | return entry.text || " "; 87 | } 88 | 89 | private tocEntrySymbolKind(entry: TocEntry): vscode.SymbolKind { 90 | switch (entry.type) { 91 | case TocEntryType.Title: { 92 | return vscode.SymbolKind.File; 93 | } 94 | case TocEntryType.Heading: { 95 | return vscode.SymbolKind.Constant; 96 | } 97 | case TocEntryType.CodeCell: { 98 | return vscode.SymbolKind.Function; 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/providers/walkthrough.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { window, Uri, workspace, ViewColumn } from "vscode"; 7 | 8 | import * as fs from "fs"; 9 | import * as os from "os"; 10 | import * as path from "path"; 11 | 12 | import { Command } from "../core/command"; 13 | import { QuartoContext } from "../shared/quarto"; 14 | import { hasRequiredExtension } from "./cell/executors"; 15 | import { promptForQuartoInstallation } from "../core/quarto"; 16 | 17 | export function walkthroughCommands(quartoContext: QuartoContext): Command[] { 18 | return [ 19 | new VerifyInstallationCommand(quartoContext), 20 | new WalkthroughNewDocumentCommand(), 21 | ]; 22 | } 23 | 24 | class VerifyInstallationCommand implements Command { 25 | private static readonly id = "quarto.walkthrough.verifyInstallation"; 26 | public readonly id = VerifyInstallationCommand.id; 27 | 28 | constructor(private readonly quartoContext_: QuartoContext) {} 29 | 30 | async execute(): Promise { 31 | if (this.quartoContext_.available) { 32 | window.showInformationMessage("Quarto Installation Verified", { 33 | modal: true, 34 | detail: `Quarto version ${this.quartoContext_.version} installed at ${this.quartoContext_.binPath}`, 35 | }); 36 | } else { 37 | await promptForQuartoInstallation("using the VS Code extension"); 38 | } 39 | } 40 | } 41 | 42 | class WalkthroughNewDocumentCommand implements Command { 43 | private static readonly id = "quarto.walkthrough.newDocument"; 44 | public readonly id = WalkthroughNewDocumentCommand.id; 45 | 46 | async execute(): Promise { 47 | const saveDir = defaultSaveDir(); 48 | const saveOptions = { 49 | defaultUri: Uri.file(path.join(saveDir, "walkthrough.qmd")), 50 | filters: { 51 | Quarto: ["qmd"], 52 | }, 53 | }; 54 | const target = await window.showSaveDialog(saveOptions); 55 | if (target) { 56 | fs.writeFileSync(target.fsPath, this.scaffold(), { 57 | encoding: "utf8", 58 | }); 59 | const doc = await workspace.openTextDocument(target); 60 | await window.showTextDocument(doc, ViewColumn.Beside, false); 61 | } 62 | } 63 | 64 | private scaffold(): string { 65 | // determine which code block to use (default to python) 66 | const kPython = { 67 | lang: "python", 68 | desc: "a Python", 69 | code: "import os\nos.cpu_count()", 70 | suffix: ":", 71 | }; 72 | const kR = { 73 | lang: "r", 74 | desc: "an R", 75 | code: "summary(cars)", 76 | suffix: ":", 77 | }; 78 | const kJulia = { 79 | lang: "julia", 80 | desc: "a Julia", 81 | code: "A = [1 2 3; 4 1 6; 7 8 1]\ninv(A)", 82 | suffix: ":", 83 | }; 84 | const langBlock = [kPython, kR, kJulia].find((lang) => { 85 | return hasRequiredExtension(lang.lang); 86 | }) || { 87 | ...kPython, 88 | suffix: 89 | ".\n\nInstall the VS Code Python Extension to enable\nrunning this cell interactively.", 90 | }; 91 | 92 | return `--- 93 | title: "Hello, Quarto" 94 | format: html 95 | --- 96 | 97 | ## Markdown 98 | 99 | Markdown is an easy to read and write text format: 100 | 101 | - It's _plain text_ so works well with version control 102 | - It can be **rendered** into HTML, PDF, and more 103 | - Learn more at: 104 | 105 | ## Code Cell 106 | 107 | Here is ${langBlock.desc} code cell${langBlock.suffix} 108 | 109 | \`\`\`{${langBlock.lang}} 110 | ${langBlock.code} 111 | \`\`\` 112 | 113 | ## Equation 114 | 115 | Use LaTeX to write equations: 116 | 117 | $$ 118 | \\chi' = \\sum_{i=1}^n k_i s_i^2 119 | $$ 120 | `; 121 | } 122 | } 123 | 124 | function defaultSaveDir() { 125 | if (workspace.workspaceFolders && workspace.workspaceFolders[0]) { 126 | return workspace.workspaceFolders[0].uri.fsPath; 127 | } else { 128 | return os.homedir(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/shared/appdirs.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as path from "path"; 7 | import * as fs from "fs"; 8 | import * as os from "os"; 9 | 10 | export function quartoDataDir(subdir?: string, roaming = false) { 11 | return quartoDir(userDataDir, subdir, roaming); 12 | } 13 | 14 | export function quartoConfigDir(subdir?: string, roaming = false) { 15 | return quartoDir(userConfigDir, subdir, roaming); 16 | } 17 | 18 | export function quartoCacheDir(subdir?: string) { 19 | return quartoDir(userCacheDir, subdir); 20 | } 21 | 22 | export function quartoRuntimeDir(subdir?: string) { 23 | return quartoDir(userRuntimeDir, subdir); 24 | } 25 | 26 | function quartoDir( 27 | sourceFn: (appName: string, roaming?: boolean) => string, 28 | subdir?: string, 29 | roaming?: boolean 30 | ) { 31 | const dir = sourceFn("quarto", roaming); 32 | const fullDir = subdir ? path.join(dir, subdir) : dir; 33 | if (!fs.existsSync(fullDir)) { 34 | fs.mkdirSync(fullDir); 35 | } 36 | return fullDir; 37 | } 38 | 39 | export function userDataDir(appName: string, roaming = false) { 40 | switch (os.platform()) { 41 | case "darwin": 42 | return darwinUserDataDir(appName); 43 | case "win32": 44 | return windowsUserDataDir(appName, roaming); 45 | case "linux": 46 | default: 47 | return xdgUserDataDir(appName); 48 | } 49 | } 50 | 51 | export function userConfigDir(appName: string, roaming = false) { 52 | switch (os.platform()) { 53 | case "darwin": 54 | return darwinUserDataDir(appName); 55 | case "win32": 56 | return windowsUserDataDir(appName, roaming); 57 | case "linux": 58 | default: 59 | return xdgUserConfigDir(appName); 60 | } 61 | } 62 | 63 | export function userCacheDir(appName: string) { 64 | switch (os.platform()) { 65 | case "darwin": 66 | return darwinUserCacheDir(appName); 67 | case "win32": 68 | return windowsUserDataDir(appName); 69 | case "linux": 70 | default: 71 | return xdgUserCacheDir(appName); 72 | } 73 | } 74 | 75 | export function userRuntimeDir(appName: string) { 76 | switch (os.platform()) { 77 | case "darwin": 78 | return darwinUserCacheDir(appName); 79 | case "win32": 80 | return windowsUserDataDir(appName); 81 | case "linux": 82 | default: 83 | return xdgUserRuntimeDir(appName); 84 | } 85 | } 86 | 87 | function darwinUserDataDir(appName: string) { 88 | return path.join( 89 | process.env["HOME"] || "", 90 | "Library", 91 | "Application Support", 92 | appName 93 | ); 94 | } 95 | 96 | function darwinUserCacheDir(appName: string) { 97 | return path.join(process.env["HOME"] || "", "Library", "Caches", appName); 98 | } 99 | 100 | function xdgUserDataDir(appName: string) { 101 | const dataHome = 102 | process.env["XDG_DATA_HOME"] || 103 | path.join(process.env["HOME"] || "", ".local", "share"); 104 | return path.join(dataHome, appName); 105 | } 106 | 107 | function xdgUserConfigDir(appName: string) { 108 | const configHome = 109 | process.env["XDG_CONFIG_HOME"] || 110 | path.join(process.env["HOME"] || "", ".config"); 111 | return path.join(configHome, appName); 112 | } 113 | 114 | function xdgUserCacheDir(appName: string) { 115 | const cacheHome = 116 | process.env["XDG_CACHE_HOME"] || 117 | path.join(process.env["HOME"] || "", ".cache"); 118 | return path.join(cacheHome, appName); 119 | } 120 | 121 | function xdgUserRuntimeDir(appName: string) { 122 | const runtimeDir = process.env["XDG_RUNTIME_DIR"]; 123 | if (runtimeDir) { 124 | return runtimeDir; 125 | } else { 126 | return xdgUserDataDir(appName); 127 | } 128 | } 129 | 130 | function windowsUserDataDir(appName: string, roaming = false) { 131 | const dir = 132 | (roaming ? process.env["APPDATA"] : process.env["LOCALAPPDATA"]) || ""; 133 | return path.join(dir, appName); 134 | } 135 | -------------------------------------------------------------------------------- /src/shared/exec.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as child_process from "child_process"; 7 | 8 | // helper to run a program and capture its output 9 | export function execProgram( 10 | program: string, 11 | args: string[], 12 | options?: child_process.ExecFileSyncOptions 13 | ) { 14 | return ( 15 | child_process.execFileSync(program, args, { 16 | encoding: "utf-8", 17 | ...options, 18 | }) as unknown as string 19 | ).trim(); 20 | } 21 | -------------------------------------------------------------------------------- /src/shared/path.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | export function pathWithForwardSlashes(path: string) { 7 | return path.replace(/\\/g, "/"); 8 | } 9 | -------------------------------------------------------------------------------- /src/shared/storage.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as path from "path"; 7 | import * as fs from "fs"; 8 | 9 | import * as uuid from "uuid"; 10 | 11 | import { quartoCacheDir } from "./appdirs"; 12 | 13 | export function fileCrossrefIndexStorage(file: string) { 14 | return fileScratchStorage(file, "xref.json"); 15 | } 16 | 17 | export function fileScratchStorage(file: string, scope: string, dir?: boolean) { 18 | // determine uuid for file scratch storage 19 | file = path.normalize(file); 20 | const index = readFileScratchStorageIndex(); 21 | let fileStorage = index[file]; 22 | if (!fileStorage) { 23 | fileStorage = uuid.v4(); 24 | index[file] = fileStorage; 25 | writeFileScratchStorageIndex(index); 26 | } 27 | 28 | // ensure the dir exists 29 | const scratchStorageDir = fileScratchStoragePath(fileStorage); 30 | if (!fs.existsSync(scratchStorageDir)) { 31 | fs.mkdirSync(scratchStorageDir); 32 | } 33 | 34 | // return the path for the scope (creating dir as required) 35 | const scopedScratchStorage = path.join(scratchStorageDir, scope); 36 | if (dir) { 37 | if (!fs.existsSync(scopedScratchStorage)) { 38 | fs.mkdirSync(scopedScratchStorage); 39 | } 40 | } 41 | return scopedScratchStorage; 42 | } 43 | 44 | function readFileScratchStorageIndex(): Record { 45 | const index = fileScratchStorageIndexPath(); 46 | if (fs.existsSync(index)) { 47 | return JSON.parse(fs.readFileSync(index, { encoding: "utf-8" })); 48 | } 49 | return {}; 50 | } 51 | 52 | function writeFileScratchStorageIndex(index: Record) { 53 | fs.writeFileSync( 54 | fileScratchStorageIndexPath(), 55 | JSON.stringify(index, undefined, 2), 56 | { encoding: "utf-8" } 57 | ); 58 | } 59 | 60 | const fileScratchStorageIndexPath = () => fileScratchStoragePath("INDEX"); 61 | 62 | function fileScratchStoragePath(file?: string) { 63 | const storagePath = path.join(quartoCacheDir(), "file-storage"); 64 | if (!fs.existsSync(storagePath)) { 65 | fs.mkdirSync(storagePath); 66 | } 67 | return file ? path.join(storagePath, file) : storagePath; 68 | } 69 | -------------------------------------------------------------------------------- /src/shared/strings.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export function escapeRegExpCharacters(value: string): string { 7 | return value.replace(/[\\\{\}\*\+\?\|\^\$\.\[\]\(\)]/g, "\\$&"); 8 | } 9 | 10 | export function shQuote(value: string): string { 11 | if (/\s/g.test(value)) { 12 | return `"${value}"`; 13 | } else { 14 | return value; 15 | } 16 | } 17 | 18 | export function winShEscape(value: string): string { 19 | return value.replace(" ", "^ "); 20 | } 21 | -------------------------------------------------------------------------------- /src/vdoc/languages.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | export interface EmbeddedLanguage { 7 | ids: string[]; 8 | extension: string; 9 | type: "content" | "tempfile"; 10 | trigger?: string[]; 11 | inject?: string[]; 12 | reuseVdoc?: boolean; 13 | } 14 | 15 | export function embeddedLanguage(langauge: string) { 16 | return kEmbededLanguages.find((lang) => lang.ids.includes(langauge)); 17 | } 18 | 19 | const kEmbededLanguages = [ 20 | // these langauges required creatinga a temp file 21 | defineLanguage("python", { 22 | ext: "py", 23 | inject: ["# type: ignore", "# flake8: noqa"], 24 | trigger: ["."], 25 | }), 26 | defineLanguage("r", { trigger: ["$", "@", ":", "."], reuseVdoc: true }), 27 | defineLanguage("julia", { ext: "jl", trigger: ["."] }), 28 | defineLanguage("sql", { trigger: ["."] }), 29 | defineLanguage("bash", { ext: "sh" }), 30 | defineLanguage("ruby", { ext: "rb", trigger: ["."] }), 31 | defineLanguage("rust", { ext: "rs", trigger: ["."] }), 32 | defineLanguage("java", { trigger: ["."] }), 33 | defineLanguage(["cpp"], { trigger: [".", ">", ":"] }), 34 | defineLanguage("go", { trigger: ["."] }), 35 | // these langauges work w/ text document content provider 36 | defineLanguage("html", { type: "content" }), 37 | defineLanguage("css", { type: "content" }), 38 | defineLanguage(["ts", "typescript"], { 39 | ext: "ts", 40 | type: "content", 41 | trigger: ["."], 42 | }), 43 | defineLanguage(["js", "javascript", "d3", "ojs"], { 44 | ext: "js", 45 | type: "content", 46 | trigger: ["."], 47 | }), 48 | defineLanguage("jsx", { trigger: ["."], type: "content" }), 49 | ]; 50 | 51 | interface LanguageOptions { 52 | ext?: string; 53 | type?: "content" | "tempfile"; 54 | trigger?: string[]; 55 | inject?: string[]; 56 | reuseVdoc?: boolean; 57 | } 58 | 59 | function defineLanguage( 60 | language: string | string[], 61 | options?: LanguageOptions 62 | ): EmbeddedLanguage { 63 | language = Array.isArray(language) ? language : [language]; 64 | return { 65 | ids: language, 66 | extension: options?.ext || language[0], 67 | type: options?.type || "tempfile", 68 | trigger: options?.trigger, 69 | inject: options?.inject, 70 | reuseVdoc: options?.reuseVdoc, 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /src/vdoc/vdoc-content.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) Microsoft Corporation. All rights reserved. 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | * ------------------------------------------------------------------------------------------ */ 6 | 7 | import { Uri, workspace } from "vscode"; 8 | import { VirtualDoc } from "./vdoc"; 9 | 10 | const kQmdEmbeddedContent = "quarto-qmd-embedded-content"; 11 | const virtualDocumentContents = new Map(); 12 | 13 | export function activateVirtualDocEmbeddedContent() { 14 | workspace.registerTextDocumentContentProvider(kQmdEmbeddedContent, { 15 | provideTextDocumentContent: (uri) => { 16 | const path = uri.path.slice(1); 17 | const originalUri = path.slice(0, path.lastIndexOf(".")); 18 | const decodedUri = decodeURIComponent(originalUri); 19 | const content = virtualDocumentContents.get(decodedUri); 20 | return content; 21 | }, 22 | }); 23 | } 24 | 25 | export function virtualDocUriFromEmbeddedContent( 26 | virtualDoc: VirtualDoc, 27 | parentUri: Uri 28 | ) { 29 | // set virtual doc 30 | const originalUri = parentUri.toString(); 31 | virtualDocumentContents.set(originalUri, virtualDoc.content); 32 | 33 | // form uri 34 | const vdocUriString = `${kQmdEmbeddedContent}://${ 35 | virtualDoc.language 36 | }/${encodeURIComponent(originalUri)}.${virtualDoc.language.extension}`; 37 | const vdocUri = Uri.parse(vdocUriString); 38 | 39 | // return it 40 | return vdocUri; 41 | } 42 | -------------------------------------------------------------------------------- /src/vdoc/vdoc-tempfile.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Copyright (c) 2019 Takashi Tamura 4 | * Licensed under the MIT License. See LICENSE in the project root for license information. 5 | * ------------------------------------------------------------------------------------------ */ 6 | 7 | import * as fs from "fs"; 8 | import * as path from "path"; 9 | import * as tmp from "tmp"; 10 | import { 11 | commands, 12 | Hover, 13 | Position, 14 | TextDocument, 15 | Uri, 16 | workspace, 17 | WorkspaceEdit, 18 | } from "vscode"; 19 | import { getWholeRange } from "../core/doc"; 20 | import { VirtualDoc } from "./vdoc"; 21 | 22 | // one virtual doc per language file extension 23 | const languageVirtualDocs = new Map(); 24 | 25 | export async function virtualDocUriFromTempFile(virtualDoc: VirtualDoc) { 26 | // do we have an existing document? 27 | const langVdoc = languageVirtualDocs.get(virtualDoc.language.extension); 28 | if (langVdoc && !langVdoc.isClosed) { 29 | // some lsps require re-use of the vdoc (or else they exit) 30 | if (virtualDoc.language.reuseVdoc) { 31 | if (langVdoc.getText() !== virtualDoc.content) { 32 | const wholeDocRange = getWholeRange(langVdoc); 33 | const edit = new WorkspaceEdit(); 34 | edit.replace(langVdoc.uri, wholeDocRange, virtualDoc.content); 35 | await workspace.applyEdit(edit); 36 | await langVdoc.save(); 37 | } 38 | return langVdoc.uri; 39 | } else if (langVdoc.getText() === virtualDoc.content) { 40 | // if its content is identical to what's passed in then just return it 41 | return langVdoc.uri; 42 | } else { 43 | // otherwise remove it (it will get recreated below) 44 | await deleteDocument(langVdoc); 45 | languageVirtualDocs.delete(virtualDoc.language.extension); 46 | } 47 | } 48 | 49 | // write the virtual doc as a temp file 50 | const vdocTempFile = createVirtualDocTempFile(virtualDoc); 51 | 52 | // open the document and save a reference to it 53 | const vdocUri = Uri.file(vdocTempFile); 54 | const doc = await workspace.openTextDocument(vdocUri); 55 | languageVirtualDocs.set(virtualDoc.language.extension, doc); 56 | 57 | // if this is the first time getting a virtual doc for this 58 | // language then execute a dummy request to cause it to load 59 | if (!langVdoc) { 60 | await commands.executeCommand( 61 | "vscode.executeHoverProvider", 62 | vdocUri, 63 | new Position(0, 0) 64 | ); 65 | } 66 | 67 | // return the uri 68 | return doc.uri; 69 | } 70 | 71 | // delete any vdocs left open 72 | export async function deactivateVirtualDocTempFiles() { 73 | languageVirtualDocs.forEach(async (doc) => { 74 | await deleteDocument(doc); 75 | }); 76 | } 77 | 78 | // delete a document 79 | async function deleteDocument(doc: TextDocument) { 80 | const edit = new WorkspaceEdit(); 81 | edit.deleteFile(doc.uri); 82 | await workspace.applyEdit(edit); 83 | } 84 | 85 | // create temp files for vdocs. use a base directory that has a subdirectory 86 | // for each extension used within the document. this is a no-op if the 87 | // file already exists 88 | tmp.setGracefulCleanup(); 89 | const vdocTempDir = tmp.dirSync().name; 90 | function createVirtualDocTempFile(virtualDoc: VirtualDoc) { 91 | const ext = virtualDoc.language.extension; 92 | const dir = path.join(vdocTempDir, ext); 93 | if (!fs.existsSync(dir)) { 94 | fs.mkdirSync(dir); 95 | } 96 | const tmpPath = path.join(vdocTempDir, ext, "intellisense." + ext); 97 | 98 | fs.writeFileSync(tmpPath, virtualDoc.content); 99 | 100 | return tmpPath; 101 | } 102 | -------------------------------------------------------------------------------- /src/vdoc/vdoc.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) RStudio, PBC. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import Token from "markdown-it/lib/token"; 7 | import { Position, TextDocument, Uri } from "vscode"; 8 | import { isQuartoDoc } from "../core/doc"; 9 | import { MarkdownEngine } from "../markdown/engine"; 10 | import { 11 | isExecutableLanguageBlock, 12 | languageBlockAtPosition, 13 | languageNameFromBlock, 14 | } from "../markdown/language"; 15 | import { embeddedLanguage, EmbeddedLanguage } from "./languages"; 16 | import { virtualDocUriFromEmbeddedContent } from "./vdoc-content"; 17 | import { virtualDocUriFromTempFile } from "./vdoc-tempfile"; 18 | 19 | export interface VirtualDoc { 20 | language: EmbeddedLanguage; 21 | content: string; 22 | } 23 | 24 | export async function virtualDoc( 25 | document: TextDocument, 26 | position: Position, 27 | engine: MarkdownEngine 28 | ): Promise { 29 | // make sure this is a quarto doc 30 | if (!isQuartoDoc(document)) { 31 | return undefined; 32 | } 33 | 34 | // check if the cursor is in a fenced code block 35 | const tokens = await engine.parse(document); 36 | const language = languageAtPosition(tokens, position); 37 | 38 | if (language) { 39 | // filter out lines that aren't of this language 40 | const lines: string[] = []; 41 | for (let i = 0; i < document.lineCount; i++) { 42 | lines.push(""); 43 | } 44 | for (const languageBlock of tokens.filter(isBlockOfLanguage(language))) { 45 | if (languageBlock.map) { 46 | for ( 47 | let line = languageBlock.map[0] + 1; 48 | line < languageBlock.map[1] - 1 && line < document.lineCount; 49 | line++ 50 | ) { 51 | lines[line] = document.lineAt(line).text; 52 | } 53 | } 54 | } 55 | 56 | // perform inject if necessary 57 | if (language.inject) { 58 | lines.unshift(...language.inject); 59 | } 60 | 61 | // return the language and the content 62 | return { 63 | language, 64 | content: lines.join("\n"), 65 | }; 66 | } else { 67 | return undefined; 68 | } 69 | } 70 | 71 | export async function virtualDocUri(virtualDoc: VirtualDoc, parentUri: Uri) { 72 | return virtualDoc.language.type === "content" 73 | ? virtualDocUriFromEmbeddedContent(virtualDoc, parentUri) 74 | : await virtualDocUriFromTempFile(virtualDoc); 75 | } 76 | 77 | export function languageAtPosition(tokens: Token[], position: Position) { 78 | const block = languageBlockAtPosition(tokens, position); 79 | if (block) { 80 | return languageFromBlock(block); 81 | } else { 82 | return undefined; 83 | } 84 | } 85 | 86 | export function languageFromBlock(token: Token) { 87 | const name = languageNameFromBlock(token); 88 | return embeddedLanguage(name); 89 | } 90 | 91 | export function isBlockOfLanguage(language: EmbeddedLanguage) { 92 | return (token: Token) => { 93 | return ( 94 | isExecutableLanguageBlock(token) && 95 | languageFromBlock(token)?.ids.some((id) => language.ids.includes(id)) 96 | ); 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": [ 5 | "ES2016", 6 | "ES2017.Object", 7 | "ES2017.String", 8 | "ES2017.Intl", 9 | "ES2017.TypedArrays", 10 | "ES2018.AsyncIterable", 11 | "ES2018.AsyncGenerator", 12 | "ES2018.Promise", 13 | "ES2018.Regexp", 14 | "ES2018.Intl", 15 | "ES2019.Array", 16 | "ES2019.Object", 17 | "ES2019.String", 18 | "ES2019.Symbol", 19 | "ES2020.BigInt", 20 | "ES2020.Promise", 21 | "ES2020.String", 22 | "ES2020.Symbol.WellKnown", 23 | "ES2020.Intl", 24 | "dom" 25 | ], 26 | "outDir": "out", 27 | "module": "commonjs", 28 | "strict": true, 29 | "exactOptionalPropertyTypes": false, 30 | "useUnknownInCatchVariables": false, 31 | "alwaysStrict": true, 32 | "noImplicitAny": true, 33 | "noImplicitReturns": true, 34 | "noImplicitOverride": true, 35 | "noUnusedLocals": true, 36 | "esModuleInterop": true, 37 | "noUnusedParameters": true 38 | }, 39 | "exclude": ["node_modules", ".vscode-test"] 40 | } 41 | -------------------------------------------------------------------------------- /tsconfig.webpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2020", 5 | "outDir": "out", 6 | "lib": ["es2020", "WebWorker"], 7 | "esModuleInterop": true, 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "strict": true /* enable all strict type-checking options */ 11 | /* Additional Checks */ 12 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 13 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 14 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 15 | }, 16 | "exclude": ["node_modules", ".vscode-test-web", "server"] 17 | } 18 | -------------------------------------------------------------------------------- /visx/quarto-1.57.0.vsix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quarto-dev/quarto-vscode/53f3621d4e3753ffd6a43d15fcdb0947d767dfe1/visx/quarto-1.57.0.vsix -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | 4 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 5 | /** @type WebpackConfig */ 6 | const webExtensionConfig = { 7 | mode: "none", // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 8 | target: "webworker", // extensions run in a webworker context 9 | entry: { 10 | extension: "./src/browser.ts", // source of the web extension main file 11 | }, 12 | output: { 13 | filename: "browser.js", 14 | path: path.join(__dirname, "./out"), 15 | libraryTarget: "commonjs", 16 | devtoolModuleFilenameTemplate: "../../[resource-path]", 17 | }, 18 | resolve: { 19 | mainFields: ["browser", "module", "main"], // look for `browser` entry point in imported node modules 20 | extensions: [".ts", ".js"], // support ts-files and js-files 21 | alias: { 22 | // provides alternate implementation for node module and source files 23 | }, 24 | fallback: { 25 | // Webpack 5 no longer polyfills Node.js core modules automatically. 26 | // see https://webpack.js.org/configuration/resolve/#resolvefallback 27 | // for the list of Node.js core module polyfills. 28 | assert: require.resolve("assert"), 29 | path: false, 30 | }, 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /.ts$/, 36 | exclude: [/node_modules/], 37 | use: [ 38 | { 39 | loader: "ts-loader", 40 | options: { 41 | configFile: "tsconfig.webpack.json", 42 | }, 43 | }, 44 | ], 45 | }, 46 | ], 47 | }, 48 | plugins: [ 49 | new webpack.ProvidePlugin({ 50 | process: "process/browser", // provide a shim for the global `process` variable 51 | }), 52 | ], 53 | externals: { 54 | vscode: "commonjs vscode", // ignored because it doesn't exist 55 | }, 56 | performance: { 57 | hints: false, 58 | }, 59 | devtool: "nosources-source-map", // create a source map that points to the original source file 60 | }; 61 | module.exports = [webExtensionConfig]; 62 | --------------------------------------------------------------------------------