diff --git a/.eslintignore b/.eslintignore index b744996d7..f0121db00 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ scripts +*.d.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fa7dda10c..7e1db9564 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,20 +1,37 @@ name: main -on: [push, pull_request] -env: - SCRIPT_DIR: ./.github/scripts +on: + push: + branches: + - master + pull_request: + branches: + - master + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: test: strategy: + fail-fast: false matrix: os: [macos-latest, ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: r-lib/actions/setup-r@v2 with: - node-version: 18 + use-public-rspm: true + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' - run: npm install + - name: Install remotes + run: install.packages("remotes") + shell: Rscript {0} + - run: npm run build - name: Run tests uses: GabrielBB/xvfb-action@v1.0 with: @@ -24,41 +41,48 @@ jobs: env: VSIX_FILE: vscode-R.vsix steps: - - uses: actions/checkout@v3 - - run: npm install - - uses: lannonbr/vsce-action@4.0.0 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - args: "package -o $VSIX_FILE" + node-version: 20 + cache: 'npm' + - run: npm install + - name: Package extension + run: npx @vscode/vsce package -o $VSIX_FILE - uses: actions/upload-artifact@v4 with: name: ${{ env.VSIX_FILE }} path: ${{ env.VSIX_FILE }} - eslint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 18 - - run: npm install - - run: npm run lint lint: runs-on: ubuntu-latest env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 - - uses: r-lib/actions/setup-r@v2 with: use-public-rspm: true - - name: Install lintr run: install.packages("lintr") shell: Rscript {0} - - name: Lint root directory + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - run: npm install + - name: eslint + run: npx eslint src --ext ts + + - name: Lint R directory run: lintr::lint_dir("./R") shell: Rscript {0} env: LINTR_ERROR_ON_LINT: true + + - name: Lint root directory + run: lintr::lint_package("sess") + shell: Rscript {0} + env: + LINTR_ERROR_ON_LINT: true diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 9e02c3233..9388c3af7 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -8,47 +8,50 @@ on: env: FILE_OUT: r-latest.vsix - SCRIPT_DIR: ./.github/scripts + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + jobs: build: runs-on: ubuntu-latest + env: + VSIX_FILE: vscode-R.vsix steps: - - uses: actions/checkout@v3 - - run: npm install - - uses: lannonbr/vsce-action@4.0.0 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - args: "package -o $FILE_OUT" + node-version: 20 + cache: 'npm' + - run: npm install + - name: Package extension + run: npx @vscode/vsce package -o $VSIX_FILE - uses: actions/upload-artifact@v4 with: - name: "${{ env.FILE_OUT }}" - path: "${{ env.FILE_OUT }}" + name: "${{ env.VSIX_FILE }}" + path: "${{ env.VSIX_FILE }}" pre-release: name: Pre-Release needs: build runs-on: ubuntu-latest - + env: + VSIX_FILE: vscode-R.vsix + permissions: + contents: write steps: - - name: Update tag - uses: richardsimko/update-tag@v1 - with: - tag_name: latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v4 - name: Download artifacts - uses: actions/download-artifact@v4.1.7 + uses: actions/download-artifact@v4 with: + pattern: "${{ env.VSIX_FILE }}" path: "artifacts/" - - name: Upload artifacts - uses: meeDamian/github-release@2.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - tag: latest - commitish: master - name: Development Build - body: Contains the vsix-file from the latest push to master. - prerelease: true - files: "artifacts/*/*" - gzip: false - allow_override: true + - name: Create or update pre-release + run: | + gh release delete latest --yes || true + git tag -d latest || true + git tag latest + git push origin latest --force + gh release create latest artifacts/${{ env.VSIX_FILE }}/${{ env.VSIX_FILE }} \ + --title "Development Build" \ + --notes "Contains the vsix-file from the latest push to master." \ + --prerelease diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ff5de961..ac34ce271 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,62 +8,68 @@ on: tags: ["v*"] env: - SCRIPT_DIR: ./.github/scripts + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - run: npm install - - uses: lannonbr/vsce-action@4.0.0 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - args: "package" - - name: Identify output file # can be retrieved as steps.filenames.outputs.file_out - id: filenames - run: echo "::set-output name=file_out::$(ls | grep "^.*\.vsix$" | head -1)" + node-version: 20 + cache: 'npm' + - run: npm install + - name: Package extension + id: package + run: | + npx @vscode/vsce package + echo "VSIX_FILE=$(ls *.vsix)" >> $GITHUB_OUTPUT - uses: actions/upload-artifact@v4 with: - name: ${{ steps.filenames.outputs.file_out }} - path: ${{ steps.filenames.outputs.file_out }} + name: vsix-artifact + path: "*.vsix" release: name: Release needs: build runs-on: ubuntu-latest + permissions: + contents: write steps: + - uses: actions/checkout@v4 - name: Download artifacts - uses: actions/download-artifact@v4.1.7 + uses: actions/download-artifact@v4 with: - path: "artifacts/" - - name: Get version from tag - id: get_version - run: echo ::set-output name=version::${GITHUB_REF/refs\/tags\/v/} + name: vsix-artifact + path: artifacts/ - name: Create release - uses: marvinpinto/action-automatic-releases@latest - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - files: "artifacts/*/*" - prerelease: false - draft: false - + run: | + VERSION=${GITHUB_REF#refs/tags/v} + gh release create v$VERSION artifacts/*.vsix \ + --title "v$VERSION" \ + --notes "Release v$VERSION" \ + --generate-notes publish: name: Publish timeout-minutes: 30 runs-on: ubuntu-latest + needs: build steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' - run: npm install - - name: Publish to Open VSX Registry - uses: HaaLeo/publish-vscode-extension@v1 - id: publishToOpenVSX + - name: Download artifacts + uses: actions/download-artifact@v4 with: - pat: ${{ secrets.OPEN_VSX_TOKEN }} + name: vsix-artifact + path: artifacts/ - name: Publish to Visual Studio Marketplace - uses: HaaLeo/publish-vscode-extension@v1 - with: - pat: ${{ secrets.VSCE_TOKEN }} - registryUrl: https://marketplace.visualstudio.com - extensionFile: ${{ steps.publishToOpenVSX.outputs.vsixPath }} + run: npx @vscode/vsce publish -p ${{ secrets.VSCE_TOKEN }} --packagePath artifacts/*.vsix + - name: Publish to Open VSX Registry + run: npx ovsx publish -p ${{ secrets.OPEN_VSX_TOKEN }} --packagePath artifacts/*.vsix diff --git a/.gitignore b/.gitignore index 0841696a1..b9ae4d20a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,6 @@ node_modules *.vsix dist .vscode-test - -html/**/*.js - *.temp *.tmp tmp.* diff --git a/.vscode-test.mjs b/.vscode-test.mjs new file mode 100644 index 000000000..fdb5cde4e --- /dev/null +++ b/.vscode-test.mjs @@ -0,0 +1,13 @@ +import { defineConfig } from '@vscode/test-cli'; + +export default defineConfig({ + files: 'out/test/suite/**/*.test.js', + mocha: { + ui: 'tdd', + color: true, + timeout: 20000 + }, + desktop: { + installExtensions: ['REditorSupport.r-syntax'] + } +}); diff --git a/.vscode/launch.json b/.vscode/launch.json index 977a98811..41ef57814 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,11 +10,13 @@ "request": "launch", "runtimeExecutable": "${execPath}", "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" + "--extensionDevelopmentPath=${workspaceFolder}", + "--disable-extension", "google.geminicodeassist" ], "outFiles": [ "${workspaceFolder}/out/src/**/*.js" ], + "sourceMaps": true, }, { "name": "Launch Extension (--disable-extensions)", @@ -28,6 +30,7 @@ "outFiles": [ "${workspaceFolder}/out/src/**/*.js" ], + "sourceMaps": true, }, { "name": "Extension Tests", @@ -44,4 +47,4 @@ "preLaunchTask": "npm: pretest" } ] -} +} \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore index 471c3f970..590602feb 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -6,6 +6,7 @@ .vscode/** .xls .xlsx +cliff.toml **/*.map html/**/*.ts out/test/** @@ -13,7 +14,6 @@ src/ src/** test/** **/tsconfig.json -vsc-extension-quickstart.md esbuild.js .markdownlint.json .eslintignore diff --git a/CHANGELOG.md b/CHANGELOG.md index 1466f8524..9544dcf67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,42 @@ # Changelog +## Unreleased + +### Bug Fixes + +* fix(rstudioapi): resolve emulation issues and viewer routing +* fix(liveshare): resolve activation errors, file reading bugs, and add hooks for sess compatibility + +### Features + +* feat(sess): migrate session watcher to WebSockets/JSON-RPC 2.0 +* feat: implement rstudioapi::showPrompt() and rstudioapi::askForPassword() for sess package +* feat: evaluate params from YAML header in Rmd files before running code +* feat: check sess package version and prompt for update +* feat(session): implement file-based reconnection and suppress verbose logs + +### Performance + +* perf: optimize package monitoring in helpServer.R + +### Styling + +* style: fix line length lint error in sess/R/rstudioapi.R + +### Testing + +* test: implement comprehensive integration test suite and modernize CI +* test: add Rmd params tests and cleanup test files +* test(session): add retry logic for plot tests to avoid timeouts on Windows +* test(session): add version check, retry logic, and fix lint warnings + ## 2.8.8 - 2026-03-24 ### Features * feat: change default of r.lsp.multiServer to false -**Full Changelog**: +**Full Changelog**: ## 2.8.7 - 2026-03-15 @@ -39,1784 +69,6 @@ **Full Changelog**: -## 2.8.5 - 2025-04-10 - -### Other - -* Don't close stale issues ([#1522](https://github.com/REditorSupport/vscode-R/issues/1522)) -* Do not mark issues as stale -* Do not mark issues as stale ([#1561](https://github.com/REditorSupport/vscode-R/issues/1561)) -* Rsyntax ([#1560](https://github.com/REditorSupport/vscode-R/issues/1560)) -* Bump path-to-regexp from 6.2.2 to 6.3.0 ([#1563](https://github.com/REditorSupport/vscode-R/issues/1563)) -* chore ([#1598](https://github.com/REditorSupport/vscode-R/issues/1598)) -* Release 2.8.5 ([#1599](https://github.com/REditorSupport/vscode-R/issues/1599)) - -**Full Changelog**: - -## 2.8.4 - 2024-05-18 - -### Bug Fixes - -* Fix code - -### Other - -* Upgrade dependencies -* Upgrade dependencies -* Update package.json -* release 2.8.4 - -**Full Changelog**: - -## 2.8.3 - 2024-05-07 - -### Bug Fixes - -* Fix multiline smart-knit ([#1493](https://github.com/REditorSupport/vscode-R/issues/1493)) -* Fix RMD Progress Bar ([#1491](https://github.com/REditorSupport/vscode-R/issues/1491)) - -### Other - -* Substitute variables in `r.rpath` and `r.rterm` settings ([#1444](https://github.com/REditorSupport/vscode-R/issues/1444)) -* Substitute variables in r.rterm.option and r.lsp.args settings -* getRLibPaths uses substituteVariables -* Improve code chunk handling in base `.R` files ([#1454](https://github.com/REditorSupport/vscode-R/issues/1454)) -* Improvements to `.R` file chunks: ([#1455](https://github.com/REditorSupport/vscode-R/issues/1455)) -* remove '.' as an R language editor.wordSeparators ([#1503](https://github.com/REditorSupport/vscode-R/issues/1503)) -* `numeric_version()` wants character as of R 4.4 ([#1520](https://github.com/REditorSupport/vscode-R/issues/1520)) -* handling terminals created by vscode-Python ([#1511](https://github.com/REditorSupport/vscode-R/issues/1511)) -* `numeric_version()` with character arg ([#1523](https://github.com/REditorSupport/vscode-R/issues/1523)) -* Bump ejs from 3.1.7 to 3.1.10 ([#1519](https://github.com/REditorSupport/vscode-R/issues/1519)) -* Update init.R ([#1525](https://github.com/REditorSupport/vscode-R/issues/1525)) -* release 2.8.3 - -**Full Changelog**: - -## 2.8.2 - 2023-10-08 - -### Other - -* Bump semver from 7.3.5 to 7.5.3 ([#1388](https://github.com/REditorSupport/vscode-R/issues/1388)) -* Bump word-wrap from 1.2.3 to 1.2.4 ([#1395](https://github.com/REditorSupport/vscode-R/issues/1395)) -* Update built-in function match regex ([#1431](https://github.com/REditorSupport/vscode-R/issues/1431)) -* Add Option to Sync renv Cache ([#1423](https://github.com/REditorSupport/vscode-R/issues/1423)) -* Add task to run `testthat::test_file()` on current file ([#1415](https://github.com/REditorSupport/vscode-R/issues/1415)) -* Fix indentation_linter message ([#1433](https://github.com/REditorSupport/vscode-R/issues/1433)) -* allow to invoke R terminal also in relative paths ([#1398](https://github.com/REditorSupport/vscode-R/issues/1398)) -* Upgrade ag-grid-community to v30.2.0 ([#1434](https://github.com/REditorSupport/vscode-R/issues/1434)) -* Upgrade vscode-languageclient to 9.0.1 ([#1435](https://github.com/REditorSupport/vscode-R/issues/1435)) -* release 2.8.2 - -**Full Changelog**: - -## 2.8.1 - 2023-06-09 - -### Bug Fixes - -* Fix handling pageSize=0 ([#1364](https://github.com/REditorSupport/vscode-R/issues/1364)) -* Fix help panel in remote host ([#1374](https://github.com/REditorSupport/vscode-R/issues/1374)) -* Fix install package name ([#1377](https://github.com/REditorSupport/vscode-R/issues/1377)) - -### Other - -* Add `r.lsp.multiServer` setting ([#1375](https://github.com/REditorSupport/vscode-R/issues/1375)) -* Upgrade to ag-grid-community-v30.0.0 ([#1379](https://github.com/REditorSupport/vscode-R/issues/1379)) -* release 2.8.1 - -**Full Changelog**: - -## 2.8.0 - 2023-04-28 - -### Bug Fixes - -* Fix markdown lint - -### Other - -* Hide most help panel commands in command palette ([#1327](https://github.com/REditorSupport/vscode-R/issues/1327)) -* Hide liveshare toggle command ([#1330](https://github.com/REditorSupport/vscode-R/issues/1330)) -* Bump webpack from 5.38.1 to 5.76.0 ([#1331](https://github.com/REditorSupport/vscode-R/issues/1331)) -* Handle errors in `getAliases.R` ([#1334](https://github.com/REditorSupport/vscode-R/issues/1334)) -* Remove unused ag-theme-balham-dark.min.css -* Upgrade to ag-grid-community-v29.3.0 ([#1346](https://github.com/REditorSupport/vscode-R/issues/1346)) -* Websocket communication ([#1151](https://github.com/REditorSupport/vscode-R/issues/1151)) -* RMD Preview: font-size setting ([#1333](https://github.com/REditorSupport/vscode-R/issues/1333)) -* release 2.8.0 -* release 2.8.0 - -**Full Changelog**: - -## 2.7.2 - 2023-03-06 - -### Other - -* Basic unit test for workspace viewer ([#1305](https://github.com/REditorSupport/vscode-R/issues/1305)) -* Update yarn.lock -* Upgrade vscode-languageclient to 8.1.0 ([#1315](https://github.com/REditorSupport/vscode-R/issues/1315)) -* Explicit workspace behaviour ([#1317](https://github.com/REditorSupport/vscode-R/issues/1317)) -* Ensure workspace is cleared ([#1318](https://github.com/REditorSupport/vscode-R/issues/1318)) -* Added r.view command: View(variable) ([#1319](https://github.com/REditorSupport/vscode-R/issues/1319)) -* Always check pid before clearing workspace ([#1321](https://github.com/REditorSupport/vscode-R/issues/1321)) -* Workspace viewer command visibility ([#1323](https://github.com/REditorSupport/vscode-R/issues/1323)) -* release 2.7.2 - -**Full Changelog**: - -## 2.7.1 - 2023-02-15 - -### Bug Fixes - -* Fix broken tests ([#1302](https://github.com/REditorSupport/vscode-R/issues/1302)) - -### Other - -* Bump minimatch from 3.0.4 to 3.1.2 ([#1271](https://github.com/REditorSupport/vscode-R/issues/1271)) -* Make help preview async ([#1273](https://github.com/REditorSupport/vscode-R/issues/1273)) -* Add the column name to the tooltip on data viewer ([#1278](https://github.com/REditorSupport/vscode-R/issues/1278)) -* Ability to set echo=TRUE in Run source by default ([#1286](https://github.com/REditorSupport/vscode-R/issues/1286)) -* Remove leading comments from terminal submission (#1244) ([#1245](https://github.com/REditorSupport/vscode-R/issues/1245)) -* Upgrade ag-grid-community to v29.0.0 ([#1290](https://github.com/REditorSupport/vscode-R/issues/1290)) -* Migrate to @vscode/test-electron ([#1303](https://github.com/REditorSupport/vscode-R/issues/1303)) -* release 2.7.1 - -**Full Changelog**: - -## 2.7.0 - 2022-12-04 - -### Bug Fixes - -* fix typo in dialog ([#1249](https://github.com/REditorSupport/vscode-R/issues/1249)) - -### Other - -* Enable publishToOpenVSX -* Add Open VSX Registry installation option to README ([#1102](https://github.com/REditorSupport/vscode-R/issues/1102)) -* Use `force=TRUE` when viewing data.frame and list ([#1255](https://github.com/REditorSupport/vscode-R/issues/1255)) -* fixed broken tasks problemMatcher regex ([#1257](https://github.com/REditorSupport/vscode-R/issues/1257)) -* Fix webview: Add webview csp directives for web workers ([#1261](https://github.com/REditorSupport/vscode-R/issues/1261)) -* Add language support for NAMESPACE & .Rbuildignore ([#1221](https://github.com/REditorSupport/vscode-R/issues/1221)) -* Implement help preview for local package(s) ([#1259](https://github.com/REditorSupport/vscode-R/issues/1259)) -* Encoding fix -* Respect RdMacros ([#1266](https://github.com/REditorSupport/vscode-R/issues/1266)) -* Change code block detection to include parentheses ([#1269](https://github.com/REditorSupport/vscode-R/issues/1269)) -* Remove script tags in R v4.2.x help pages ([#1268](https://github.com/REditorSupport/vscode-R/issues/1268)) -* release 2.7.0 - -**Full Changelog**: - -## 2.6.1 - 2022-10-31 - -### Bug Fixes - -* Fix checking `request.viewer` ([#1234](https://github.com/REditorSupport/vscode-R/issues/1234)) - -### Other - -* Tweak settings ([#1235](https://github.com/REditorSupport/vscode-R/issues/1235)) -* Support trailing slash in code-server's URI template ([#1241](https://github.com/REditorSupport/vscode-R/issues/1241)) -* release 2.6.1 - -**Full Changelog**: - -## 2.6.0 - 2022-10-11 - -### Bug Fixes - -* Fix empty code examples ([#1194](https://github.com/REditorSupport/vscode-R/issues/1194)) -* Fix help -* Fix help katex support under remote development ([#1217](https://github.com/REditorSupport/vscode-R/issues/1217)) -* Fix use of asExternalUri - -### Other - -* Avoid code highlighting in DESCRIPTION files ([#1199](https://github.com/REditorSupport/vscode-R/issues/1199)) -* Avoid .R file lock on windows ([#1192](https://github.com/REditorSupport/vscode-R/issues/1192)) -* devTasks ([#1200](https://github.com/REditorSupport/vscode-R/issues/1200)) -* Add generated .js files to .gitignore -* Bug report template: Minor spelling and formatting adjustments ([#1206](https://github.com/REditorSupport/vscode-R/issues/1206)) -* Implement c_cpp_properties.json file generator fixes #1201 ([#1205](https://github.com/REditorSupport/vscode-R/issues/1205), [#1201](https://github.com/REditorSupport/vscode-R/issues/1201)) -* Refactoring: Strict TypeScript Fix #1208 ([#1209](https://github.com/REditorSupport/vscode-R/issues/1209), [#1208](https://github.com/REditorSupport/vscode-R/issues/1208)) -* [Chore] Remove unused NPM dependencies fix #232 ([#1216](https://github.com/REditorSupport/vscode-R/issues/1216)) -* Support KaTeX in help page viewer ([#1213](https://github.com/REditorSupport/vscode-R/issues/1213)) -* Use asExternalUri instead -* Not trigger preview on file change when rmd is still rendering ([#1219](https://github.com/REditorSupport/vscode-R/issues/1219)) -* release 2.6.0 - -**Full Changelog**: - -## 2.5.3 - 2022-09-06 - -### Other - -* Bump terser from 5.7.0 to 5.14.2 ([#1154](https://github.com/REditorSupport/vscode-R/issues/1154)) -* Remove encoding param from knitting ([#1167](https://github.com/REditorSupport/vscode-R/issues/1167)) -* Reload help pages on help refresh ([#1188](https://github.com/REditorSupport/vscode-R/issues/1188)) -* Upgrade to vscode-languageclient 8.0.2 ([#1173](https://github.com/REditorSupport/vscode-R/issues/1173)) -* release 2.5.3 - -**Full Changelog**: - -## 2.5.2 - 2022-07-15 - -### Bug Fixes - -* Workspace viewer fix ([#1150](https://github.com/REditorSupport/vscode-R/issues/1150)) - -### Other - -* Publish to Open VSX Registry ([#1101](https://github.com/REditorSupport/vscode-R/issues/1101)) -* Create issues.yml ([#1061](https://github.com/REditorSupport/vscode-R/issues/1061)) -* Remove nesting: guard clauses ([#1110](https://github.com/REditorSupport/vscode-R/issues/1110)) -* Hide preview env values to prevent accidental deletion ([#1117](https://github.com/REditorSupport/vscode-R/issues/1117)) -* Add file creation to file/newFile ([#1119](https://github.com/REditorSupport/vscode-R/issues/1119)) -* Bump jquery.json-viewer from 1.4.0 to 1.5.0 ([#1123](https://github.com/REditorSupport/vscode-R/issues/1123)) -* Update data viewer column resizing ([#1121](https://github.com/REditorSupport/vscode-R/issues/1121)) -* Click code in help views ([#1138](https://github.com/REditorSupport/vscode-R/issues/1138)) -* Adapt to lintr 3.0 ([#1141](https://github.com/REditorSupport/vscode-R/issues/1141)) -* Whitespace in typescript files ([#1142](https://github.com/REditorSupport/vscode-R/issues/1142)) -* Add `Create .linr` command ([#1112](https://github.com/REditorSupport/vscode-R/issues/1112)) -* Add .yarnrc -* Upgrade to ag-grid-community 28.0.0 ([#1144](https://github.com/REditorSupport/vscode-R/issues/1144)) -* release 2.5.1 -* Disable publish to openvsx -* release 2.5.2 - -**Full Changelog**: - -## 2.5.0 - 2022-05-14 - -### Bug Fixes - -* Fix console err ([#1034](https://github.com/REditorSupport/vscode-R/issues/1034)) -* Fix lintr complain - -### Other - -* add R package build task - build source and build binary like RStudio ([#1029](https://github.com/REditorSupport/vscode-R/issues/1029)) -* Use `bindingIsActive` ([#1031](https://github.com/REditorSupport/vscode-R/issues/1031)) -* guard against evaluation of active bindings ([#1038](https://github.com/REditorSupport/vscode-R/issues/1038), [#1030](https://github.com/REditorSupport/vscode-R/issues/1030)) -* Upgrade `ag-grid-community` to v27.1.0 ([#1049](https://github.com/REditorSupport/vscode-R/issues/1049)) -* Hide smart knit env values to prevent accidental deletion ([#1060](https://github.com/REditorSupport/vscode-R/issues/1060)) -* Add `r.session.data.pageSize` ([#1068](https://github.com/REditorSupport/vscode-R/issues/1068)) -* Bump ansi-regex from 3.0.0 to 3.0.1 ([#1070](https://github.com/REditorSupport/vscode-R/issues/1070)) -* Bump minimist from 1.2.5 to 1.2.6 ([#1069](https://github.com/REditorSupport/vscode-R/issues/1069)) -* Update rGitignore.ts -* Add lsp settings to support disabling prompt and additional libPaths ([#1071](https://github.com/REditorSupport/vscode-R/issues/1071)) -* More choices for code chunk snippet ([#1082](https://github.com/REditorSupport/vscode-R/issues/1082)) -* Take out http from `Insert image` Rmd snippet ([#1084](https://github.com/REditorSupport/vscode-R/issues/1084)) -* Take out http from link snippet ([#1085](https://github.com/REditorSupport/vscode-R/issues/1085)) -* Bump cross-fetch from 3.1.4 to 3.1.5 ([#1087](https://github.com/REditorSupport/vscode-R/issues/1087)) -* Bump ejs from 3.1.6 to 3.1.7 ([#1088](https://github.com/REditorSupport/vscode-R/issues/1088)) -* Use `loadNamespace` ([#1086](https://github.com/REditorSupport/vscode-R/issues/1086)) -* Prompt when no templates found ([#1089](https://github.com/REditorSupport/vscode-R/issues/1089)) -* Update publisher id ([#1093](https://github.com/REditorSupport/vscode-R/issues/1093)) -* R language server and help supports additional libPaths ([#1097](https://github.com/REditorSupport/vscode-R/issues/1097)) -* Move r.libPaths in setting -* Update `r.libPaths` behavior ([#1098](https://github.com/REditorSupport/vscode-R/issues/1098)) -* release 2.5.0 - -**Full Changelog**: - -## 2.4.0 - 2022-03-07 - -### Bug Fixes - -* Fix commented pipe bug ([#988](https://github.com/REditorSupport/vscode-R/issues/988)) -* Fix resolveTask ([#994](https://github.com/REditorSupport/vscode-R/issues/994)) -* Fix incorrect syntax highlighting for variables starting with "function" ([#992](https://github.com/REditorSupport/vscode-R/issues/992), [#982](https://github.com/REditorSupport/vscode-R/issues/982)) - -### Other - -* Use spawn ([#985](https://github.com/REditorSupport/vscode-R/issues/985)) -* WIP: Add problemMatching to task ([#989](https://github.com/REditorSupport/vscode-R/issues/989)) -* R markdown templates ([#984](https://github.com/REditorSupport/vscode-R/issues/984)) -* Preserve selected text in rmd snippet ([#1001](https://github.com/REditorSupport/vscode-R/issues/1001)) -* Provide optional 'code' argument to r.runSelection command ([#1017](https://github.com/REditorSupport/vscode-R/issues/1017)) -* Add Shiny snippets ([#1012](https://github.com/REditorSupport/vscode-R/issues/1012), [#1011](https://github.com/REditorSupport/vscode-R/issues/1011)) -* Add lambda to function-declarations ([#1025](https://github.com/REditorSupport/vscode-R/issues/1025)) -* Enhance workspace viewer ([#1022](https://github.com/REditorSupport/vscode-R/issues/1022)) -* Share httpgd url for LiveShare ([#1026](https://github.com/REditorSupport/vscode-R/issues/1026)) -* Add some useful Rmd snippets ([#1009](https://github.com/REditorSupport/vscode-R/issues/1009)) -* release 2.4.0 - -**Full Changelog**: - -## 2.3.8 - 2022-02-07 - -### Other - -* Avoid rpath quoting ([#981](https://github.com/REditorSupport/vscode-R/issues/981)) -* release 2.3.8 - -**Full Changelog**: - -## 2.3.7 - 2022-02-07 - -### Bug Fixes - -* Fix rmd comment ([#958](https://github.com/REditorSupport/vscode-R/issues/958)) - -### Other - -* correcting typo on command argument (slient instead of silent) ([#954](https://github.com/REditorSupport/vscode-R/issues/954)) -* update the .gitignore file for R ([#949](https://github.com/REditorSupport/vscode-R/issues/949)) -* Add delay before refreshing plots ([#956](https://github.com/REditorSupport/vscode-R/issues/956)) -* Add row limit setting of data viewer and support Apache Arrow Table ([#945](https://github.com/REditorSupport/vscode-R/issues/945)) -* Bump node-fetch from 2.6.1 to 2.6.7 ([#962](https://github.com/REditorSupport/vscode-R/issues/962)) -* set the LANG env when rendering rmarkdown ([#961](https://github.com/REditorSupport/vscode-R/issues/961)) -* should use Strong quote for shell commands ([#964](https://github.com/REditorSupport/vscode-R/issues/964)) -* Add note about required httpgd package version ([#972](https://github.com/REditorSupport/vscode-R/issues/972)) -* update dcf syntax and add support ".lintr" file ([#970](https://github.com/REditorSupport/vscode-R/issues/970)) -* prompt to install languageserver is not available ([#965](https://github.com/REditorSupport/vscode-R/issues/965)) -* [data frame viewer] Add type of column to headerTooltip ([#974](https://github.com/REditorSupport/vscode-R/issues/974)) -* Upgrade ag-grid-community to 26.2.1 ([#975](https://github.com/REditorSupport/vscode-R/issues/975)) -* Activate extension on subfolder ([#979](https://github.com/REditorSupport/vscode-R/issues/979)) -* release 2.3.7 - -**Full Changelog**: - -## 2.3.6 - 2022-01-16 - -### Bug Fixes - -* Fix syntax file ([#939](https://github.com/REditorSupport/vscode-R/issues/939)) - -### Other - -* Add raw string tokens ([#922](https://github.com/REditorSupport/vscode-R/issues/922)) -* Support both single and double brackets in code-server's URI template ([#934](https://github.com/REditorSupport/vscode-R/issues/934)) -* Rename `R/session/.Rprofile` to `R/session/profile.R` ([#938](https://github.com/REditorSupport/vscode-R/issues/938)) -* Use taskkill for win32 ([#936](https://github.com/REditorSupport/vscode-R/issues/936)) -* Fixed `"punctuation.section.parens.end.r"` under `"function-parameters"` ([#931](https://github.com/REditorSupport/vscode-R/issues/931)) -* Use `taskkill` for win32 ([#941](https://github.com/REditorSupport/vscode-R/issues/941)) -* release 2.3.6 - -**Full Changelog**: - -## 2.3.5 - 2021-12-18 - -### Bug Fixes - -* Fix dcf syntax ([#920](https://github.com/REditorSupport/vscode-R/issues/920)) - -### Other - -* adding devtools tasks to command palette ([#880](https://github.com/REditorSupport/vscode-R/issues/880)) -* RMD - don't set undefined wd ([#914](https://github.com/REditorSupport/vscode-R/issues/914)) -* Use `SIGKILL` to kill help server ([#912](https://github.com/REditorSupport/vscode-R/issues/912)) -* readability adjustments for help pages ([#915](https://github.com/REditorSupport/vscode-R/issues/915)) -* Clean-up child processes on dispose ([#918](https://github.com/REditorSupport/vscode-R/issues/918)) -* release 2.3.5 - -**Full Changelog**: - -## 2.3.4 - 2021-11-30 - -### Bug Fixes - -* Fix helpserver issue ([#893](https://github.com/REditorSupport/vscode-R/issues/893)) - -### Other - -* Remove quotes from rpath if necessary ([#884](https://github.com/REditorSupport/vscode-R/issues/884)) -* Try different CRAN URLs ([#885](https://github.com/REditorSupport/vscode-R/issues/885)) -* Use `Uri.file` instead of `Uri.parse` ([#888](https://github.com/REditorSupport/vscode-R/issues/888)) -* Update bug_report.md -* Clean up of help related files ([#887](https://github.com/REditorSupport/vscode-R/issues/887)) -* Use httpgd NPM package ([#823](https://github.com/REditorSupport/vscode-R/issues/823)) -* release 2.3.4 - -**Full Changelog**: - -## 2.3.3 - 2021-11-21 - -### Bug Fixes - -* Fix package installation ([#846](https://github.com/REditorSupport/vscode-R/issues/846)) -* Fix detecting yaml frontmatter ([#856](https://github.com/REditorSupport/vscode-R/issues/856)) -* Fix rmd preview chunk colouring ([#867](https://github.com/REditorSupport/vscode-R/issues/867)) - -### Other - -* Add R info to status bar item text and tooltip ([#836](https://github.com/REditorSupport/vscode-R/issues/836)) -* get knit command from settings ([#841](https://github.com/REditorSupport/vscode-R/issues/841)) -* Add support for indented Roxygen ([#847](https://github.com/REditorSupport/vscode-R/issues/847)) -* Syntax highlighting for indented roxygen ([#850](https://github.com/REditorSupport/vscode-R/issues/850)) -* Use new terminal API ([#851](https://github.com/REditorSupport/vscode-R/issues/851)) -* Add backtick to list of quote characters for syntax highlighting. ([#859](https://github.com/REditorSupport/vscode-R/issues/859)) -* Auto refresh help ([#863](https://github.com/REditorSupport/vscode-R/issues/863)) -* Show httpgd plot on attach ([#852](https://github.com/REditorSupport/vscode-R/issues/852)) -* Update lim ([#868](https://github.com/REditorSupport/vscode-R/issues/868)) -* release 2.3.3 - -**Full Changelog**: - -## 2.3.2 - 2021-10-22 - -### Other - -* Httpgd plot viewer respects `r.session.viewers.viewColumn.plot` ([#816](https://github.com/REditorSupport/vscode-R/issues/816)) -* Completely replace `View()` ([#818](https://github.com/REditorSupport/vscode-R/issues/818)) -* Change help cache default ([#819](https://github.com/REditorSupport/vscode-R/issues/819)) -* browser handles `file://` ([#817](https://github.com/REditorSupport/vscode-R/issues/817)) -* Add `r.session.levelOfObjectDetail=Normal` for `max.level=1` ([#815](https://github.com/REditorSupport/vscode-R/issues/815)) -* Update address -* Check workspace folder with both original and real path ([#827](https://github.com/REditorSupport/vscode-R/issues/827)) -* release 2.3.2 - -**Full Changelog**: - -## 2.3.1 - 2021-10-07 - -### Other - -* Support `VSCODE_PROXY_URI` ([#803](https://github.com/REditorSupport/vscode-R/issues/803)) -* Reenable 'unsafe-eval' in script-src CSP ([#805](https://github.com/REditorSupport/vscode-R/issues/805)) -* Use r.session.viewers.viewColumn.helpPanel ([#804](https://github.com/REditorSupport/vscode-R/issues/804)) -* Use cwd in knit process ([#807](https://github.com/REditorSupport/vscode-R/issues/807)) -* Bump version -* release 2.3.1 - -**Full Changelog**: - -## 2.3.0 - 2021-09-23 - -### Bug Fixes - -* Fix RMD requireNamespace ([#784](https://github.com/REditorSupport/vscode-R/issues/784)) -* Fix hljs usage - -### Other - -* Enable rstudioapi by default ([#769](https://github.com/REditorSupport/vscode-R/issues/769)) -* Use unsafe-inline for script-src ([#771](https://github.com/REditorSupport/vscode-R/issues/771)) -* R Markdown Enhancements (Knit Manager) ([#765](https://github.com/REditorSupport/vscode-R/issues/765)) -* (Refactoring) Simplify RMD child process disposal ([#773](https://github.com/REditorSupport/vscode-R/issues/773)) -* Add object length limit ([#778](https://github.com/REditorSupport/vscode-R/issues/778)) -* Write NA as string ([#780](https://github.com/REditorSupport/vscode-R/issues/780)) -* Use R files for background process ([#783](https://github.com/REditorSupport/vscode-R/issues/783)) -* Respect preview output format ([#785](https://github.com/REditorSupport/vscode-R/issues/785)) -* Bump @types/vscode from 1.57.0 to 1.60.0 ([#786](https://github.com/REditorSupport/vscode-R/issues/786)) -* Extend providers to rmd ([#787](https://github.com/REditorSupport/vscode-R/issues/787)) -* Bump nth-check from 2.0.0 to 2.0.1 ([#795](https://github.com/REditorSupport/vscode-R/issues/795)) -* Update vscode and ag-grid version -* Update dependencies -* Update highlight.js version -* release 2.3.0 - -**Full Changelog**: - -## 2.2.0 - 2021-08-21 - -### Bug Fixes - -* Fix date filter in data viewer ([#736](https://github.com/REditorSupport/vscode-R/issues/736)) -* Fix README -* Fix issues with c() in function args ([#751](https://github.com/REditorSupport/vscode-R/issues/751), [#713](https://github.com/REditorSupport/vscode-R/issues/713)) - -### Other - -* Check conflict extension ([#733](https://github.com/REditorSupport/vscode-R/issues/733)) -* Rename liveshare folder to liveShare ([#738](https://github.com/REditorSupport/vscode-R/issues/738)) -* Viewer fix: invalid html_widget resource paths ([#739](https://github.com/REditorSupport/vscode-R/issues/739)) -* Accessing VS Code settings in R ([#743](https://github.com/REditorSupport/vscode-R/issues/743)) -* Use .DollarNames with default pattern ([#750](https://github.com/REditorSupport/vscode-R/issues/750)) -* Handle error in capture_str ([#756](https://github.com/REditorSupport/vscode-R/issues/756)) -* Add icons to webviews ([#759](https://github.com/REditorSupport/vscode-R/issues/759)) -* release 2.2.0 - -**Full Changelog**: - -## 2.1.0 - 2021-07-20 - -### Bug Fixes - -* Fix README links -* Fix typo in url -* Fix typo - -### Other - -* Minor workspace-related changes ([#672](https://github.com/REditorSupport/vscode-R/issues/672)) -* Update README ([#669](https://github.com/REditorSupport/vscode-R/issues/669)) -* Enable r.sessionWatcher by default -* Update previewDataframe and previewEnvironment -* Enable r.sessionWatcher by default ([#670](https://github.com/REditorSupport/vscode-R/issues/670)) -* Add customization options to plot viewer ([#678](https://github.com/REditorSupport/vscode-R/issues/678)) -* Catch LiveShare API errors ([#679](https://github.com/REditorSupport/vscode-R/issues/679)) -* Small Plot Viewer adjustments ([#681](https://github.com/REditorSupport/vscode-R/issues/681)) -* Integer syntax supports e.g. 1e2L -* Integer syntax supports e.g. 1e2L ([#683](https://github.com/REditorSupport/vscode-R/issues/683)) -* Change License owner -* Update url and author -* Update url and author ([#694](https://github.com/REditorSupport/vscode-R/issues/694)) -* Preview R Markdown files via background process ([#692](https://github.com/REditorSupport/vscode-R/issues/692)) -* RMD Preview fixes ([#699](https://github.com/REditorSupport/vscode-R/issues/699)) -* Update r.rmarkdown.codeLensCommands ([#707](https://github.com/REditorSupport/vscode-R/issues/707)) -* Remove show plot history command ([#706](https://github.com/REditorSupport/vscode-R/issues/706)) -* Add full window mode for plots ([#709](https://github.com/REditorSupport/vscode-R/issues/709)) -* Use ag-grid in data viewer ([#708](https://github.com/REditorSupport/vscode-R/issues/708)) -* Integrate vscode-r-lsp ([#695](https://github.com/REditorSupport/vscode-R/issues/695)) -* prerelease 2.1.0 -* release 2.1.0 -* release 2.1.0 - -**Full Changelog**: - -## 2.0.0 - 2021-06-12 - -### Bug Fixes - -* Fix getCurrentChunk and use chunks.find for most cases - -### Other - -* Use .DollarNames for object with class in completion -* Use `.DollarNames` for object with class in completion ([#660](https://github.com/REditorSupport/vscode-R/issues/660)) -* Code cells in .R files -* Update rmarkdown.ts -* Remove unused languages -* Code cells in .R files ([#662](https://github.com/REditorSupport/vscode-R/issues/662)) -* Squash bugs -* Change source & knit icons -* Jump to cursor -* Remove image files not used -* rmarkdown bug squashing and minor changes ([#663](https://github.com/REditorSupport/vscode-R/issues/663)) -* Bump css-what from 5.0.0 to 5.0.1 -* Bump css-what from 5.0.0 to 5.0.1 ([#664](https://github.com/REditorSupport/vscode-R/issues/664)) -* Bugfix -* LiveShare Functionality ([#626](https://github.com/REditorSupport/vscode-R/issues/626)) -* prerelease 2.0.0 ([#667](https://github.com/REditorSupport/vscode-R/issues/667)) - -**Full Changelog**: - -## 1.6.8 - 2021-05-31 - -### Bug Fixes - -* Fix typo in internal file name -* Fix Webview Identification Function ([#650](https://github.com/REditorSupport/vscode-R/issues/650)) -* Fix bugs in helpviewer ([#658](https://github.com/REditorSupport/vscode-R/issues/658)) - -### Other - -* Fix typo in internal file name ([#643](https://github.com/REditorSupport/vscode-R/issues/643)) -* Revert syntax for lambda -* Revert syntax for lambda ([#657](https://github.com/REditorSupport/vscode-R/issues/657)) -* version 1.6.8 - -**Full Changelog**: - -## 1.6.7 - 2021-05-31 - -### Bug Fixes - -* Fix replacing base::.External.graphics -* Fix Github actions -* fix highlight.js - -### Other - -* Adding markdownlint on extension.json and GitHub Actions ([#591](https://github.com/REditorSupport/vscode-R/issues/591)) -* Fix replacing `base::.External.graphics` ([#625](https://github.com/REditorSupport/vscode-R/issues/625)) -* Integrate httpgd ([#620](https://github.com/REditorSupport/vscode-R/issues/620)) -* Minor fix of README -* Prefer rPath from PATH -* Prefer rPath from PATH ([#649](https://github.com/REditorSupport/vscode-R/issues/649)) -* Improve development workflow ([#641](https://github.com/REditorSupport/vscode-R/issues/641)) -* Don't run chunks with eval = FALSE. Fixes #651 ([#651](https://github.com/REditorSupport/vscode-R/issues/651)) -* Don't run chunks with eval = FALSE. Fixes #651 ([#653](https://github.com/REditorSupport/vscode-R/issues/653), [#651](https://github.com/REditorSupport/vscode-R/issues/651)) -* Add pipe and lambda to syntax -* Update doesLineEndInOperator -* Update R syntax ([#647](https://github.com/REditorSupport/vscode-R/issues/647)) -* prerelease 1.6.7 -* update changelog -* Prerelease 1.6.7 ([#655](https://github.com/REditorSupport/vscode-R/issues/655)) - -**Full Changelog**: - -## 1.6.6 - 2021-04-11 - -### Bug Fixes - -* Fix leading/trailing newlines -* fix package volunerability - -### Other - -* Update vscode engine -* Update vscode engine ([#586](https://github.com/REditorSupport/vscode-R/issues/586)) -* Satisfy markdownlint -* Satisfy markdownlint ([#587](https://github.com/REditorSupport/vscode-R/issues/587)) -* Initial Workspace Viewer str() functionality (#577) -* Initial Workspace Viewer str() functionality ([#583](https://github.com/REditorSupport/vscode-R/issues/583)) -* Thread execute argument through to term.sendText() ([#585](https://github.com/REditorSupport/vscode-R/issues/585)) -* Remove object.size -* Use cache to store object size for objects in globalenv -* Add option vsc.show_object_size -* Update object size when length changes -* Update getSizeString() to be consistent with format() in R -* Being more conservative to call object.size() in task callback ([#581](https://github.com/REditorSupport/vscode-R/issues/581)) -* Send code to debug repl -* Send code to debug repl ([#582](https://github.com/REditorSupport/vscode-R/issues/582)) -* Clarify R path error messages ([#596](https://github.com/REditorSupport/vscode-R/issues/596)) -* Add tasks Check, Document, Install, Test ([#603](https://github.com/REditorSupport/vscode-R/issues/603)) -* shim the rstudioapi if it has already been loaded -* make lintr happy -* shim the rstudioapi if it has already been loaded ([#610](https://github.com/REditorSupport/vscode-R/issues/610)) -* Clarify error messages ([#607](https://github.com/REditorSupport/vscode-R/issues/607)) -* version 1.6.6 - -### Refactor - -* Refactor R code ([#602](https://github.com/REditorSupport/vscode-R/issues/602)) - -**Full Changelog**: - -## 1.6.5 - 2021-03-15 - -### Bug Fixes - -* Fix get_timestamp() so that it will not be affected by e.g. options(digits=3) ([#550](https://github.com/REditorSupport/vscode-R/issues/550)) -* Fix so code can be run after creating terminal -* Fix #572 ([#572](https://github.com/REditorSupport/vscode-R/issues/572)) - -### Other - -* Change workspace tooltip ([#544](https://github.com/REditorSupport/vscode-R/issues/544)) -* add option vsc.hover.str.max.level ([#545](https://github.com/REditorSupport/vscode-R/issues/545)) -* Refactoring and implementation of webviewPanelSerializer ([#556](https://github.com/REditorSupport/vscode-R/issues/556)) -* Scroll to bottom after running a command ([#559](https://github.com/REditorSupport/vscode-R/issues/559)) -* Add option to keep terminal hidden -* Clarify r.source.focus options in description -* Add option to keep terminal hidden after running code ([#566](https://github.com/REditorSupport/vscode-R/issues/566)) -* Check rTerm is defined before showing -* Fix so code can be run after creating terminal ([#567](https://github.com/REditorSupport/vscode-R/issues/567)) -* Move `r.runSource` and `r.knitRmd` to `editor/title/run` (Fix #572) ([#573](https://github.com/REditorSupport/vscode-R/issues/573), [#572](https://github.com/REditorSupport/vscode-R/issues/572)) -* Add links to help pages in hover -* Improve the formatting -* Add links to help pages in hover ([#578](https://github.com/REditorSupport/vscode-R/issues/578)) -* version 1.6.5 -* update vscode engine - -**Full Changelog**: - -## 1.6.4 - 2021-02-01 - -### Other - -* Better error message when reading aliases ([#518](https://github.com/REditorSupport/vscode-R/issues/518)) -* Keep promises and active bindings in globalenv ([#521](https://github.com/REditorSupport/vscode-R/issues/521)) -* (Maybe) Fix Aliases ([#526](https://github.com/REditorSupport/vscode-R/issues/526)) -* add sendToConsole to rstudioapi emulation ([#535](https://github.com/REditorSupport/vscode-R/issues/535)) -* Add updatePackage command ([#532](https://github.com/REditorSupport/vscode-R/issues/532)) -* Add initial pipeline completion support ([#530](https://github.com/REditorSupport/vscode-R/issues/530)) -* Add function to open help for selected text ([#531](https://github.com/REditorSupport/vscode-R/issues/531)) -* Preserve focus when opening help view ([#541](https://github.com/REditorSupport/vscode-R/issues/541)) -* update packages -* version 1.6.4 -* Prerelease version 1.6.4 ([#542](https://github.com/REditorSupport/vscode-R/issues/542)) -* version 1.6.4 - -### Refactor - -* Refactor extension.ts ([#525](https://github.com/REditorSupport/vscode-R/issues/525)) -* refactor import statements - -**Full Changelog**: - -## 1.6.3 - 2021-01-01 - -### Other - -* Implement R workspace viewer ([#476](https://github.com/REditorSupport/vscode-R/issues/476)) -* add a new collaborator -* Conditionally show view ([#487](https://github.com/REditorSupport/vscode-R/issues/487)) -* Enable find widget in WebViews ([#490](https://github.com/REditorSupport/vscode-R/issues/490)) -* Find in topic (help panel) #463 ([#488](https://github.com/REditorSupport/vscode-R/issues/488)) -* Disable alwaysShow for addin items ([#491](https://github.com/REditorSupport/vscode-R/issues/491)) -* Only show R menu items in R view ([#493](https://github.com/REditorSupport/vscode-R/issues/493)) -* Modify pre-release action ([#492](https://github.com/REditorSupport/vscode-R/issues/492)) -* Add browser WebView command buttons ([#494](https://github.com/REditorSupport/vscode-R/issues/494)) -* typo fix -* typo fix ([#500](https://github.com/REditorSupport/vscode-R/issues/500)) -* Improve help view ([#502](https://github.com/REditorSupport/vscode-R/issues/502)) -* releaseAction ([#505](https://github.com/REditorSupport/vscode-R/issues/505)) -* Strict null checking in help related code ([#507](https://github.com/REditorSupport/vscode-R/issues/507)) -* WIP: Pre fixing for version 1.6.3 ([#508](https://github.com/REditorSupport/vscode-R/issues/508)) -* version 1.6.3 - -**Full Changelog**: - -## 1.6.2 - 2020-12-06 - -### Bug Fixes - -* Fix bug that would leave background R processes running ([#475](https://github.com/REditorSupport/vscode-R/issues/475)) -* Fix whole of style (Extends #361) ([#474](https://github.com/REditorSupport/vscode-R/issues/474)) - -### Other - -* Add pre-release ([#468](https://github.com/REditorSupport/vscode-R/issues/468)) -* Improve Help Panel ([#470](https://github.com/REditorSupport/vscode-R/issues/470)) -* RMarkdown: Add run & navigation commands. More customization. Refactor. ([#465](https://github.com/REditorSupport/vscode-R/issues/465)) -* Fixes #443 ([#462](https://github.com/REditorSupport/vscode-R/issues/462)) -* Reorganize helppanel, add `?` function ([#477](https://github.com/REditorSupport/vscode-R/issues/477)) -* Update README for #465 -* Update README for #465 ([#480](https://github.com/REditorSupport/vscode-R/issues/480)) -* Improve style of help pages ([#481](https://github.com/REditorSupport/vscode-R/issues/481)) -* Modify config ([#467](https://github.com/REditorSupport/vscode-R/issues/467)) -* update devreplay rules -* back the previous custom rules -* version 1.6.2 - -**Full Changelog**: - -## 1.6.1 - 2020-11-24 - -### Bug Fixes - -* Fix type in help panel path config -* Fix lint: missing semicolon -* Fix checking workspaceFolders in rHelpProviderOptions - -### Other - -* Don't use rterm as fallback for help panel R path -* Fix typo in help panel path config ([#457](https://github.com/REditorSupport/vscode-R/issues/457)) -* New feature r.runFromLineToEnd -* New feature r.runFromLineToEnd ([#448](https://github.com/REditorSupport/vscode-R/issues/448)) -* Also check length -* Fix checking workspaceFolders in rHelpProviderOptions ([#456](https://github.com/REditorSupport/vscode-R/issues/456)) -* Highlight all chunks -* Highlight all chunks ([#453](https://github.com/REditorSupport/vscode-R/issues/453)) -* Add GitHub Action for release -* Add GitHub Action for release ([#449](https://github.com/REditorSupport/vscode-R/issues/449)) -* Add Rmd fenced_block_* for julia, python, etc -* Rmd fenced block syntax highlighting for julia, python, etc ([#460](https://github.com/REditorSupport/vscode-R/issues/460)) -* Update README: Add options(vsc.helpPanel = ...) -* Update README: Add options(vsc.helpPanel = ...) ([#461](https://github.com/REditorSupport/vscode-R/issues/461)) -* version 1.6.1 - -**Full Changelog**: - -## 1.6.0 - 2020-11-21 - -### Bug Fixes - -* Fix usage of GlobPattern -* Fix leading slash in tmpDir path -* Fix showWebView -* Fix build.yml -* Fix handling group_df in dataview_table -* fix extension tslint to eslint -* fix #265 ([#265](https://github.com/REditorSupport/vscode-R/issues/265)) -* Fix main.yml -* Fix eslint -* fix vlunerability -* fix vscode version -* Fix code formatting -* Fix main.yml -* Fix .lintr -* Fix .Rprofile line length -* fix package dependencies -* fix package-lock.json -* Fix typo in showWebView -* fix devreplay error -* Fix lintr github action -* Fix new test cases -* Fix outFiles in launch.json -* Fix View empty environment -* Fix typo in README.md -* Fix homedir on Windows -* fix for new conventions -* fix config title r to R -* Fix viewer and page_viewer url -* Fix plot viewer -* Fix typo -* Fix typo -* fix dependencies -* Fix previewDataframe for 2+ letter variables -* Fix so rTerm is undefined when deleting terminal -* fix package volunerability -* Fix more functions to use runTextInTerm -* fix package dependencies -* fix webpack for the new webpack interface -* Fix linting message -* fix and enhance navigateToFile -* fix toString -> toString() - -### Other - -* version 1.2.2 -* Update FAQ -* Add logging to session watcher -* Add more logging to session watcher ([#208](https://github.com/REditorSupport/vscode-R/issues/208)) -* updateResponse only handles response when responseLineCount increases -* updateResponse only handles response when responseLineCount changes -* Avoid duplicate handling of response update ([#211](https://github.com/REditorSupport/vscode-R/issues/211)) -* first tslint cleanup -* Update activationEvents -* Update activationEvents -* Update activationEvents ([#224](https://github.com/REditorSupport/vscode-R/issues/224)) -* Add syntax highlighting for R code in Rcpp comment -* Add syntax highlighting for R code in Rcpp comment ([#225](https://github.com/REditorSupport/vscode-R/issues/225)) -* Fixed the function snippet (Issue #230) -* Fixed the function snippet (Issue #230) ([#231](https://github.com/REditorSupport/vscode-R/issues/231)) -* add collaborator -* version 1.2.3 -* update vscode -* Add statement of languageserver features to bug_report.md -* Add statement of languageserver features to bug report template ([#229](https://github.com/REditorSupport/vscode-R/issues/229)) -* string interpolation command runner -* add runCommandWithPath and clean -* swtich to '$$' replacement target, editor paths unquoted -* Add command runner function doco to README -* add quotes to runCommandWithPath example -* handle unsaved and unititled files -* global replace enabled for $$ -* use escaped double quotes in keybinding example -* Add configurable command runner functions ([#237](https://github.com/REditorSupport/vscode-R/issues/237)) -* Minor refine README.md -* Fix leading slash in tmpDir path ([#221](https://github.com/REditorSupport/vscode-R/issues/221)) -* Change platform gui in init.R -* Change .Platform$GUI to vscode on session start ([#234](https://github.com/REditorSupport/vscode-R/issues/234)) -* Inject R Markdown features into Markdown grammar -* Remove non-bracket code chunks from R Markdown grammar -* Inject R Markdown features into Markdown grammar ([#228](https://github.com/REditorSupport/vscode-R/issues/228)) -* version 1.2.4 -* version 1.2.5 -* Check untitled document and save result before running command -* Update code -* Check untitled document and save result before running command ([#239](https://github.com/REditorSupport/vscode-R/issues/239)) -* Fix showWebView ([#246](https://github.com/REditorSupport/vscode-R/issues/246)) -* version 1.2.6 -* Add lint workflows -* Rename lint action name -* Combine lint workflows -* Use GitHub Actions for linting ([#251](https://github.com/REditorSupport/vscode-R/issues/251)) -* Add build.yml -* Combine to single main.yml -* Refine main.yml -* Use GitHub Actions to build extension ([#253](https://github.com/REditorSupport/vscode-R/issues/253)) -* Use Windows registry to find R path -* Refine getRpath -* Update package dependencies -* Add logging -* Use async getRpath() -* Update README.md -* Use Windows registry to find R path ([#252](https://github.com/REditorSupport/vscode-R/issues/252)) -* Fix handling grouped_df in dataview_table ([#248](https://github.com/REditorSupport/vscode-R/issues/248)) -* try to adding eslint -* version 1.2.7 -* try to adding eslint ([#254](https://github.com/REditorSupport/vscode-R/issues/254)) -* Add wiki link on README -* support single quote -* add backtick support -* Support single quote (Fix #260) ([#264](https://github.com/REditorSupport/vscode-R/issues/264), [#260](https://github.com/REditorSupport/vscode-R/issues/260)) -* Use eslint in GitHub Actions -* Update package-lock.json -* Use setup-node -* Use eslint in GitHub Actions ([#266](https://github.com/REditorSupport/vscode-R/issues/266)) -* Add languages embedded in markdown -* Add surround support for R Markdown files -* Add backtick support for R documentation files -* Remove backtick auto-closing pair for R Markdown -* Add R Markdown surround and frontmatter comments ([#269](https://github.com/REditorSupport/vscode-R/issues/269)) -* update eslint rules -* back tsconfig and remove tslint -* version 1.2.8 -* Update lintr -* Fix R code formatting according to linting results ([#278](https://github.com/REditorSupport/vscode-R/issues/278)) -* Use env to specify vsix file in github actions build -* Make rebind also work in attached packages -* Make rebind also work with attached packages ([#268](https://github.com/REditorSupport/vscode-R/issues/268)) -* Make plot update more smart using magic null dev size -* Refine code -* Make plot update smarter using magic null dev size ([#274](https://github.com/REditorSupport/vscode-R/issues/274)) -* lintr action error on any non-empty lintr result -* Fix lintr action ([#280](https://github.com/REditorSupport/vscode-R/issues/280)) -* Remove --no-site-file from default r.rterm.option -* Remove --no-site-file from default r.rterm.option ([#284](https://github.com/REditorSupport/vscode-R/issues/284)) -* Source Rprofile.site at last -* Remove Rprofile.site -* Update .Rprofile -* Update .Rprofile -* Improve .Rprofile ([#282](https://github.com/REditorSupport/vscode-R/issues/282)) -* update vscode engine -* Change so setting changes take effect immediately ([#301](https://github.com/REditorSupport/vscode-R/issues/301)) -* version 1.3.0 -* update contributing.md -* Refine .Rprofile -* Refine .Rprofile to remove unnecessary printing in R startup message. ([#303](https://github.com/REditorSupport/vscode-R/issues/303)) -* Change runTextInTerm to string from string[] -* Change signatures to string from string[] -* Simplify removeCommentedLines -* Use bracketed paste for more commands #294 ([#305](https://github.com/REditorSupport/vscode-R/issues/305)) -* Add command Run from Beginning to Line -* Add command Run from Beginning to Line ([#290](https://github.com/REditorSupport/vscode-R/issues/290)) -* add devreplay -* Making the vscode-r original coding conventions ([#308](https://github.com/REditorSupport/vscode-R/issues/308)) -* merge 3 repeated keyword.operator.comparison.r in r.json file. -* merge 3 repeated keyword.operator.comparison.r in r.json file. ([#311](https://github.com/REditorSupport/vscode-R/issues/311)) -* Fix typo in showWebView ([#310](https://github.com/REditorSupport/vscode-R/issues/310)) -* No remove blank or comment lines -* Remove removeCommentedLines as unused -* Remove checkForBlankOrComment as unused -* No remove blank or comment lines ([#313](https://github.com/REditorSupport/vscode-R/issues/313)) -* Add vscode-test dependency -* Update package-lock.json after adding vscode-test -* Update scripts for test and pretest -* Add runTest.ts and index.ts for new test format -* Remove old format test index.ts -* Move test file -* Update path -* Change double quotes to single quotes -* Update tsconfig.json for new test format -* Remove launch configuration Launch Tests -* Ignore .vscode-test -* Add test to github actions -* Refine test github action -* Migrate to vscode-test #315 ([#317](https://github.com/REditorSupport/vscode-R/issues/317)) -* Fix lintr github action ([#319](https://github.com/REditorSupport/vscode-R/issues/319)) -* extendSelection only handles brackets outside quotes -* Add test cases for extendSelection and fix formatting -* Refine condition flow -* Add test/*.ts to eslint -* Refine eslint github action -* extendSelection only handles brackets outside quotes ([#314](https://github.com/REditorSupport/vscode-R/issues/314)) -* Add r.runSelectionRetainCursor -* Add r.runSelectionRetainCursor ([#325](https://github.com/REditorSupport/vscode-R/issues/325)) -* Add launch tests to launch.json -* Update launch.json -* Update tasks.json and launch.json -* Update launch.json and tasks.json -* Update launch.json and tasks.json -* Add launch tests to launch.json ([#320](https://github.com/REditorSupport/vscode-R/issues/320)) -* supress auto-opening quote in roxygen comment -* supress auto-opening quote in roxygen comment ([#328](https://github.com/REditorSupport/vscode-R/issues/328)) -* converted Rmd file to json format -* converted Rcpp language file to json -* converted RD language file to json -* converted indentation to spaces to be consistent -* Convert language files to Json ([#333](https://github.com/REditorSupport/vscode-R/issues/333)) -* exposure send text delay as a parameter -* changed description of rtermsenddelay; stopped multiple config reads -* expose send text delay as a parameter ([#336](https://github.com/REditorSupport/vscode-R/issues/336)) -* Added functionality to switch to active R terminal -* fixed rTermNameOptions typo -* updated to select last created Rterminal -* Added functionality to switch to an existing R terminal ([#338](https://github.com/REditorSupport/vscode-R/issues/338)) -* added functionality to search PATH in mac/linux -* updated to auto detect R on windows as well -* added missing argument type -* Enable default R location to be used on mac/linux if none is supplied ([#340](https://github.com/REditorSupport/vscode-R/issues/340)) -* add dcf -* add syntax highlight for DESCRIPTION and .Rproj ([#342](https://github.com/REditorSupport/vscode-R/issues/342)) -* Define lint in package.json and use it in GitHub Actions -* Update main.yml -* Define lint in package.json and use it in GitHub Actions ([#344](https://github.com/REditorSupport/vscode-R/issues/344)) -* version 1.4.0 -* Fix View empty environment ([#350](https://github.com/REditorSupport/vscode-R/issues/350)) -* Use fs.watch instead of vscode.FileSystemWatcher -* Only handle request of R session started from workspace folders or subfolders -* Use request lock file to avoid partial change -* Use plot.lock and globalenv.lock -* Update README.md -* Update init.R -* Not source init.R in RStudio -* Use fs.watch instead of vscode.FileSystemWatcher ([#348](https://github.com/REditorSupport/vscode-R/issues/348)) -* Improve getBrowserHtml -* Improve getBrowserHtml ([#353](https://github.com/REditorSupport/vscode-R/issues/353)) -* Change runSelectionInActiveTerm effect to warning -* Change runSelectionInActiveTerm effect to warning ([#351](https://github.com/REditorSupport/vscode-R/issues/351)) -* update/remove packages -* version 1.4.1 -* Initial rewrite of init.R -* Rewrite session watcher -* Update init.R -* Update options -* Update options -* Update options -* Use viewer === false to open externally -* Clean up and use .vsc.attach() -* Support htmlwidget input and title in viewer and page_viewer -* Rename parseResult to request -* normalizePath in webview -* Session watcher options ([#359](https://github.com/REditorSupport/vscode-R/issues/359)) -* Remove single quote from doesLineEndInOperator -* Add test cases for extendSelection with quotes -* Remove single quote from doesLineEndInOperator ([#357](https://github.com/REditorSupport/vscode-R/issues/357)) -* update CHANGELOG for following #359 -* Update changelog -* Update changelog ([#362](https://github.com/REditorSupport/vscode-R/issues/362)) -* version 1.4.2 -* update change log links -* Add session watcher functions and options to README -* Update README -* Update README -* Update README -* Update README -* Update README -* Update README -* Fix plot viewer ([#365](https://github.com/REditorSupport/vscode-R/issues/365)) -* Minor update readme -* Accept all dir when no workspace folder is open -* Only accept session started from home folder -* Handle undefined workspace folders ([#367](https://github.com/REditorSupport/vscode-R/issues/367)) -* On Mac & Linux, rely on PATH being set up -* Update package.json -* On Mac & Linux, rely on PATH being set up ([#374](https://github.com/REditorSupport/vscode-R/issues/374)) -* version 1.4.3 -* update webpack options -* update mocha option -* version 1.4.4 -* Fix previewDataframe for 2+ letter variables ([#390](https://github.com/REditorSupport/vscode-R/issues/390)) -* fixed typo and added sep choice -* fixed typo and added sep choice ([#397](https://github.com/REditorSupport/vscode-R/issues/397)) -* Fix so rTerm is undefined when deleting terminal ([#403](https://github.com/REditorSupport/vscode-R/issues/403)) -* Restore R_PROFILE_USER -* Restore R_PROFILE_USER ([#392](https://github.com/REditorSupport/vscode-R/issues/392)) -* Remove Ctrl + 1, 2, 3, 4, 5 shortcuts -* Update CHANGELOG for removing Ctrl + 1, 2, 3, 4, 5 -* Remove Ctrl + 1, 2, 3, 4, 5 shortcuts ([#401](https://github.com/REditorSupport/vscode-R/issues/401)) -* version 1.4.5 -* Check url in browser -* Use path_to_uri in browser -* Check url in browser ([#406](https://github.com/REditorSupport/vscode-R/issues/406)) -* Remove active parameter from chooseTerminal() -* Remove term parameter from runTextInTerm() -* Remove chooseTerminalAndSendText() -* Remove term parameter from runSelectionInTerm() -* Remove command runSelectionInActiveTerm -* Remove trailing whitespace -* Remove command Run Selection/Line in Active Terminal ([#409](https://github.com/REditorSupport/vscode-R/issues/409)) -* Remove Run in Active Terminal from README -* Remove Run in Active Terminal from README ([#413](https://github.com/REditorSupport/vscode-R/issues/413)) -* version 1.4.6 -* update change log -* Recommend radian in README -* Recommend radian in README ([#420](https://github.com/REditorSupport/vscode-R/issues/420)) -* RStudio Addin Support ([#408](https://github.com/REditorSupport/vscode-R/issues/408)) -* remove winattr for the character error -* add missed file -* version 1.5.0 -* make update addin registry a safe call -* tested glitch protection in addin.dcf read -* supply actual version numbers to keep {cli} happy -* require rstudioapi emulation be enabled via option -* note about option in README -* move tryCatch to dcf read/parse -* move guards for vsc.rstudioapi -* move rstudioapi_enabled to init.R -* lintr fixes -* don't need active rTerm -* Fix issues in rstudioapi emulation ([#422](https://github.com/REditorSupport/vscode-R/issues/422)) -* Rename init functions -* Rename init functions ([#425](https://github.com/REditorSupport/vscode-R/issues/425)) -* version 1.5.1 -* Use help_type='html' only when unspecified -* Print url before sending browser request to trigger auto port-forwarding -* Improve handling html help ([#427](https://github.com/REditorSupport/vscode-R/issues/427)) -* fix and enhance navigateToFile ([#430](https://github.com/REditorSupport/vscode-R/issues/430)) -* Enhance R markdown support ([#429](https://github.com/REditorSupport/vscode-R/issues/429)) -* version 1.5.2 -* Add runAboveChunks command -* Drop empty chunks in runChunksInTerm -* Send trimmed text from chunks -* Add runAboveChunks command ([#434](https://github.com/REditorSupport/vscode-R/issues/434)) -* patform independent content string splitting -* nicer error when can't find list of rstudio addins -* platform independent content string splitting ([#436](https://github.com/REditorSupport/vscode-R/issues/436)) -* Merge remote-tracking branch 'upstream/master' -* remove full stop -* Friendly error message when trying to launch addin picker and vsc.rstudioapi = FALSE ([#441](https://github.com/REditorSupport/vscode-R/issues/441)) -* Send code at EOF appends new line -* Send code at EOF appends new line ([#444](https://github.com/REditorSupport/vscode-R/issues/444)) -* Add terminal information to chooseTerminal error ([#447](https://github.com/REditorSupport/vscode-R/issues/447)) -* Integrate help view from vscode-R-help ([#433](https://github.com/REditorSupport/vscode-R/issues/433)) -* version 1.6.0 - -### Refactor - -* refactor - -### Styling - -* style fix - -**Full Changelog**: - -## 1.2.1-20200124-625ae35 - 2020-01-24 - -### Bug Fixes - -* fix PositionNeg implementation -* fix depencency -* fix defaul runSelection -* fix SetFocus to choose terminal -* Fix to allow creation of first terminal -* Fix check for Excel Viewer extension -* fix for valunerability -* Fix for R markdown config -* Fix Preview Environment for multi-class objects -* Fix Preview Environment for variable x -* fix for tslint -* fix package dependencies -* fix behaviour when workplacefolders is Undefiend -* fix LICENSE to MIT -* fix vlunerable packages -* Fix function call closing bracket highlight -* Fix typo in init.R -* Fix for tslint -* Fix error message -* Fix bootstrap dependency -* Fix #168 ([#168](https://github.com/REditorSupport/vscode-R/issues/168)) -* Fix session watcher init.R path on Windows -* Fix typo -* Fix typo -* fix style -* Fix type check for completion of function -* Fix usage of CancellationToken -* Fix WebView Uri replacing -* Fix dataview_table handling single row data - -### Other - -* Update ISSUE_TEMPLATE.md -* add wordPattern (fixed #75) ([#75](https://github.com/REditorSupport/vscode-R/issues/75)) -* version 0.6.2 -* sorry, I can not continue support -* version 1.0.1 -* version 1.0.2 -* Update issue templates -* Create PULL_REQUEST_TEMPLATE.md -* Update bug_report.md -* typo -* Preview Dataframe checks for whitespace -* Preview Dataframe command works again -* Fix Preview Dataframe command #67 ([#97](https://github.com/REditorSupport/vscode-R/issues/97)) -* version 1.0.3 -* Adapt runSelection to use RCommands as Shortcut -* Adapt runSelection to use RCommands as Shortcut ([#101](https://github.com/REditorSupport/vscode-R/issues/101)) -* version 1.0.4 -* version 1.0.5 -* Add runSelectionInActiveTerm -* Add first terminal check to chooseTerminal -* Add animation showing SSH use -* Add runSelectionInActiveTerm command #80 #102 ([#104](https://github.com/REditorSupport/vscode-R/issues/104)) -* miss to merge -* version 1.0.7 -* .gitignore is now working -* version 1.0.8 -* Fix check for Excel Viewer extension #96 ([#108](https://github.com/REditorSupport/vscode-R/issues/108)) -* add gc-excel installer -* version 1.0.9 -* version 1.1.0 -* Fix Preview Environment for multi-class objects #111 ([#113](https://github.com/REditorSupport/vscode-R/issues/113)) -* Fix Preview Environment for variable x #111 ([#115](https://github.com/REditorSupport/vscode-R/issues/115)) -* version 1.1.1 -* version 1.1.2 -* Add bracketed paste mode option -* Do not send blank lines to console -* Fix send code for newlines and Radian #114 #117 ([#119](https://github.com/REditorSupport/vscode-R/issues/119)) -* Added Rmd knit shortcut -* Add knit to PDF command -* Added HTMl and all as Knit options -* Remove icons for all but Knit default -* RMarkdown knit support ([#122](https://github.com/REditorSupport/vscode-R/issues/122)) -* version 1.1.3 -* Fixed spelling, improved formatting -* Fixed spelling, improved formatting ([#129](https://github.com/REditorSupport/vscode-R/issues/129)) -* Automatically comment new lines in roxygen sections -* Automatically comment new lines in roxygen sections #124 ([#130](https://github.com/REditorSupport/vscode-R/issues/130)) -* Do not send blank lines ending in CRLF to console -* Fix send code for newlines on Windows #114 ([#125](https://github.com/REditorSupport/vscode-R/issues/125)) -* add roxygen comments -* Add auto-completion of roxygen tags -* Add auto-completion of roxygen tags #128 ([#131](https://github.com/REditorSupport/vscode-R/issues/131)) -* Change cursorMove -* Change cursorMove to wrappedLineFirstNonWhitespaceCharacter ([#127](https://github.com/REditorSupport/vscode-R/issues/127)) -* version 1.1.4 -* replace deprecated function -* Remove redundant functions -* Move send text functions into rTerminal -* Add alwaysUseActiveTerminal setting -* Add alwaysUseActiveTerminal to README, templates -* Add alwaysUseActiveTerminal setting #123 ([#133](https://github.com/REditorSupport/vscode-R/issues/133)) -* version 1.1.5 -* Show r.term.option value in settings UI -* Show r.term.option value in settings UI ([#136](https://github.com/REditorSupport/vscode-R/issues/136)) -* fix behaviour when workplacefolders is Undefiend ([#138](https://github.com/REditorSupport/vscode-R/issues/138)) -* refactoring -* version 1.1.6 -* remove duplicated quote #139 -* version 1.1.7 -* Use word under cursor for previewDataframe, nrow -* Apply functions once instead of to each line -* remove extra calling -* Use word under cursor for previewDataframe, nrow #137 ([#141](https://github.com/REditorSupport/vscode-R/issues/141)) -* Delete LICENSE -* Create LICENSE -* version 1.1.8 -* version 1.1.8 -* Delete LICENSE -* Send code all at once in bracketed paste mode -* Use no bracketed paste characters on Windows -* Fix bracketed paste on Windows ([#149](https://github.com/REditorSupport/vscode-R/issues/149)) -* Fix function call closing bracket highlight ([#151](https://github.com/REditorSupport/vscode-R/issues/151)) -* version 1.1.9 -* Hover works with update -* Attach active command switches session -* Detects changes to plot -* First implementation of showWebView -* Not change pid on webview response -* Use vscode.open to open plot file on update -* Markdown hover text -* Update view column -* Update webview options -* Use console logging -* start log watcher on activation -* Add status bar item -* Update status bar -* Add session init R script -* Add opt-in r.sessionWatcher option -* Implement deploySessionWatcher -* Read file in async method -* Refine updateSessionWatcher -* Remove unused data output init.R -* Add plot hook in session init.R to support ggplot2 -* Remove session files on terminal close -* Add rebind to init.R -* Add time stamp in respond -* Implement showDataView -* Force color in showWebView -* Update table class -* Refine table font size -* Include dataview resources -* Support View matrix -* Not rely on tempdir(check=TRUE) which requires R >= 3.5.0 -* Support browser command -* Add portMapping to WebView created in showBrowser -* Change name and title of browser WebView -* Change WebView title of browser -* Use workspaceFolders instead of deprecated rootPath -* Use WebView.asWebviewUri -* Use WebView.asWebviewUri -* Add R session watcher section to README.md -* Update README.md -* Use json for View(data.frame) -* Refine table_to_json in init.R -* Check windows in source script -* Check if init.R already sourced -* Use DataTables JS sourced data for View(data.frame) -* Refine showDataView -* remove outside files -* Use webpack to copy resources to dist -* Remove resources folder as no longer needed -* Update README.md -* Use column.type to fix ordering in View -* R session watcher ([#150](https://github.com/REditorSupport/vscode-R/issues/150)) -* version 1.2.0 -* Use empty order when creating DataTables in getTableHtml -* Use empty order when creating DataTables in getTableHtml ([#157](https://github.com/REditorSupport/vscode-R/issues/157)) -* Use utils::str in init.R ([#169](https://github.com/REditorSupport/vscode-R/issues/169)) -* Fix session watcher init.R path on Windows ([#177](https://github.com/REditorSupport/vscode-R/issues/177)) -* Support View(environment) -* Add initial support of View(function) -* Handle function in showDataView -* Support View any object -* Support View list that cannot be converted to json -* Make init.R more robust -* Make init.R more robust -* Refine init.R and use html help by default -* Use retainContextWhenHidden in all WebViews -* Update init.R -* Use FixedHeader extension in View(data.frame) -* Add row id to dataview_table for table without row names -* Extend View ([#161](https://github.com/REditorSupport/vscode-R/issues/161)) -* version 1.2.1 -* Update issue templates -* Provide completion for session symbols -* Update README.md -* Provide completion for elements in list-like objects -* Implement bracket completions -* Unify completion provider -* Use tryCatch in update -* Provide bracket completion with condition -* Provide completion for session symbols ([#165](https://github.com/REditorSupport/vscode-R/issues/165)) -* Initial implementation of plot history -* Make image viewer center of page -* Update plot history WebView and resources -* Add some error handling -* Show plot history ([#181](https://github.com/REditorSupport/vscode-R/issues/181)) -* Add row hover and select -* Use table-active as selected row style -* Add row hover and select ([#186](https://github.com/REditorSupport/vscode-R/issues/186)) -* Fix WebView Uri replacing ([#188](https://github.com/REditorSupport/vscode-R/issues/188)) -* Show page_viewer WebView in Active column -* Show WebView triggered by page_viewer in Active column ([#189](https://github.com/REditorSupport/vscode-R/issues/189)) -* Fix dataview_table handling single row data ([#198](https://github.com/REditorSupport/vscode-R/issues/198)) -* Use dev.args option when creating png device before replay -* Use dev.args option when creating png device before replay ([#182](https://github.com/REditorSupport/vscode-R/issues/182)) -* Update session watcher section in README.md -* Update README.md -* Update README.md -* Update README.md -* Add link and short description of radian -* Use R_PROFILE_USER -* init.R only work with TERM_PROGRAM=vscode -* Update README.md -* Respect existing R_PROFILE_USER -* Update README.md -* Update README.md -* Improve session watcher initialization ([#184](https://github.com/REditorSupport/vscode-R/issues/184)) - -### Refactor - -* refactor -* refactor and add webpack - -### Styling - -* style fix - -**Full Changelog**: - -## 0.6.1 - 2018-08-17 - -### Bug Fixes - -* Fix for #42 -* fix CO -* fix #65 ([#65](https://github.com/REditorSupport/vscode-R/issues/65)) -* fix tsconfig to publish -* fix dependencies -* fix categories -* fix dependency and lintr -* fix readability - -### Other - -* update dependency -* Fix for #42 ([#63](https://github.com/REditorSupport/vscode-R/issues/63)) -* revert fix -* version 0.5.8 -* version 0.5.9 -* remove lint function -* version 0.6.0 -* Issue 26: Added detection of bracket and pipe blocks. -* Issue 26: Added detection of bracket and pipe blocks. ([#82](https://github.com/REditorSupport/vscode-R/issues/82)) -* version 0.6.1 - -**Full Changelog**: - -## 0.5.7 - 2018-04-22 - -### Bug Fixes - -* fix grammer based on atom -* Fix for #61 - -### Other - -* update some dependencies -* version 0.5.6 -* disable lintr from default -* Additional Fix #61 ([#61](https://github.com/REditorSupport/vscode-R/issues/61)) -* version 0.5.7 - -### Refactor - -* refactor - -**Full Changelog**: - -## 0.5.5 - 2018-03-21 - -### Other - -* Add package dev commands -* Add package dev commands ([#58](https://github.com/REditorSupport/vscode-R/issues/58)) -* version 0.5.5 - -**Full Changelog**: - -## 0.5.4 - 2018-02-17 - -### Bug Fixes - -* fix -* fix lint -* fix Readme -* fix change log -* fix import order -* fix R syntax grammer -* fix light icon path -* Fix syntax -* fix by tslint -* fix lintr issue on windows -* fix document style -* fix default rterm.option -* fix default rterm -* fix snippets - -### Other - -* Update README.md -* Update README.md -* Update README.md -* add shortcut -* version 0.4.0 -* source r short cut -* add SourcewithEcho -* version 0.4.2 -* init next function -* Added dataframe viewer -* Added sample data sets to demonstrate 5mb bug -* Mac and Linux hidden folder and disposal -* Cleaned up Preview Dataframe -* Cleaned package.json -* Restored package.json -* Added DS_Store to .gitignore -* Removed DS_Store -* Added Dataviewer Command ([#20](https://github.com/REditorSupport/vscode-R/issues/20)) -* version 0.4.3 -* lint fix -* add run source icon -* version 0.4.4 -* add document -* Create CODE_OF_CONDUCT.md -* Added data frame preview GIF -* Added Data frame GIF as img -* Corrected Markdown to Display GIF -* little fix -* update run icon(fix #21) ([#21](https://github.com/REditorSupport/vscode-R/issues/21)) -* remove extra test -* Fixed dataframe preview on win32; hidden folder and longer write wait -* Updated Dataframe Preview filesize limit -* remove extra dependencies -* Merge remote-tracking branch 'upstream/master' -* Update TODO -* version 0.4.5 -* Environment preview #23 -* version 0.4.5 -* remove extra files -* update typescript version -* update syntax -* mobr test files -* Update Readme and vesion up -* update some snippets from VS -* Attend win short cut -* version 0.4.8 -* Create ISSUE_TEMPLATE.md -* Slowed commands being pushed on RTerm -* Removed new line -* Proposed fix for Load Chunk problems #27 ([#31](https://github.com/REditorSupport/vscode-R/issues/31)) -* Added block detection and execute whole block -* Added warning if R client is not located. Corrected space in warning -* Added block detection and execute whole block ([#32](https://github.com/REditorSupport/vscode-R/issues/32)) -* add white space -* add shebang support for R syntax highlight ([#33](https://github.com/REditorSupport/vscode-R/issues/33)) -* update snippets -* version 0.4.9 -* support lint package -* version 0.5.0 -* fix lintr issue on windows ([#35](https://github.com/REditorSupport/vscode-R/issues/35)) -* return lintr code -* support code region -* version 0.5.1 -* little fix -* version 0.5.2 -* version 0.5.3 -* R term name to R interactive (fix #46) ([#46](https://github.com/REditorSupport/vscode-R/issues/46)) -* Send code from Rmd chunk to terminal #49 -* version 0.5.4 - -### Refactor - -* refactor -* refactor -* refactor -* refactor -* refactor - -**Full Changelog**: - -## 0.3.9 - 2017-07-08 - -### Bug Fixes - -* fix -* fix -* fix lintr - -### Other - -* Added cursorMove down on line execution -* Don't pass Rterm comments -* Cleaned up skip comments -* Added cursorMove after line execution -* Update extension.ts -* Added cursorMove after line execution ([#13](https://github.com/REditorSupport/vscode-R/issues/13)) -* Don't pass Rterm comments ([#14](https://github.com/REditorSupport/vscode-R/issues/14)) -* version 0.3.8 -* update logo -* version 0.3.9 - -**Full Changelog**: - -## 0.3.7 - 2017-07-02 - -### Other - -* auto lintr -* version v0.3.7 - -**Full Changelog**: - -## 0.3.6 - 2017-06-23 - -### Bug Fixes - -* fix -* fix syntax -* fix #7 ([#7](https://github.com/REditorSupport/vscode-R/issues/7)) - -### Other - -* version 0.3.5 -* update -* little fix syntax -* version 0.3.6 - -### Refactor - -* refactor - -**Full Changelog**: - -## 0.3.4 - 2017-06-17 - -### Bug Fixes - -* fix something - -### Other - -* clean -* Merge pull request #1 from Ikuyadeu/master -* Fixed typos -* Fixed typos -* Fixed typos -* Fixing typos ([#12](https://github.com/REditorSupport/vscode-R/issues/12)) -* use rbox -* version 0.3.1 -* version 0.3.4 - -**Full Changelog**: - -## 0.3.1 - 2017-06-15 - -### Bug Fixes - -* fix #9 ([#9](https://github.com/REditorSupport/vscode-R/issues/9)) - -### Other - -* version 0.3.1 - -**Full Changelog**: - -## 0.3.0 - 2017-06-09 - -### Bug Fixes - -* fix lintr onMac -* fix lintr output - -### Other - -* version 0.2.9 -* update package -* version 0.3.0 - -**Full Changelog**: - -## 0.2.8 - 2017-06-04 - -### Other - -* add runSelection/Line -* version 0.2.8 - -**Full Changelog**: - -## 0.2.7 - 2017-06-04 - -### Other - -* update based project in README.md -* tslint -* set focus #5 -* version 0.2.7 - -**Full Changelog**: - -## 0.2.6 - 2017-06-02 - -### Bug Fixes - -* fix Terminal type -* fix #1 ([#1](https://github.com/REditorSupport/vscode-R/issues/1)) -* fix keywords -* fix for windows -* fix -* fix #2 ([#2](https://github.com/REditorSupport/vscode-R/issues/2)) - -### Other - -* update icon -* Create LICENCE -* add license -* version 0.1.8 -* add tslint -* setup use lintr -* support lintr -* version 0.2.0 -* add install lintr -* version 0.2.1 -* Delete vsc-extension-quickstart.md -* version 0.2.2 -* add option -* version 0.2.3 -* update README.md -* version 0.2.4 -* add selectedLine -* version 0.2.5 -* add keywords -* Add support for custom encoding (so that UTF-8 scripts can be executed properly) -* Custom encoding support ([#4](https://github.com/REditorSupport/vscode-R/issues/4)) -* add lintr term -* update r-snippets.json from atom language-r -* save before Run Source -* update README.md -* version 0.2.6 - -**Full Changelog**: - -## 0.1.3 - 2017-05-03 - -### Bug Fixes - -* fix perform - -### Other - -* v0.1.3 - -**Full Changelog**: - -## 0.1.2 - 2017-04-30 - -### Bug Fixes - -* fix Readme.md - -### Other - -* update summary in package and readme -* update package.json -* version 0.1.2 - -**Full Changelog**: - -## 0.1.1 - 2017-04-29 - -### Bug Fixes - -* fix change log -* fix run r perform -* fix for unix os - -### Other - -* version 0.0.9 -* update figure -* support r gitignore -* version 1.1 - -**Full Changelog**: - -## 0.0.8 - 2017-04-11 - -### Other - -* remove rd-snippets -* add rmd snippets -* version 0.0.8 - -**Full Changelog**: - -## 0.0.7 - 2017-04-09 - -### Bug Fixes - -* fix readme -* fix readme - -### Other - -* update document -* update document -* support R Markdown -* version 0.0.7 - -**Full Changelog**: - -## 0.0.6 - 2017-04-07 - -### Other - -* make create R terminal -* integrated R -* version 0.0.6 - -**Full Changelog**: - -## 0.0.5 - 2017-04-05 - -### Bug Fixes - -* fix Run .R -* fix test.r -* fix extension's name -* fix summary -* fix publisher - -### Other - -* init -* add contributes -* update package.json -* add runR -* Update README.md -* createRterm only name -* set multi platform -* Tool to Tools -* update configuration -* add repository -* add feature.png -* Update README.md -* add icon -* update version -* support R doumantation -* version 0.0.3 -* add snippets -* version 0.0.4 -* support rd-snippets -* version 0.0.5 - See [CHANGELOG.old.md](https://github.com/REditorSupport/vscode-R/blob/master/CHANGELOG.old.md) for changes before v2.8.5. diff --git a/R/help/helpServer.R b/R/help/helpServer.R index e657965a9..69cdcdd15 100644 --- a/R/help/helpServer.R +++ b/R/help/helpServer.R @@ -30,15 +30,20 @@ cat( sep = "" ) -currentPackages <- NULL +lib_dirs <- .libPaths() +last_mtimes <- file.info(lib_dirs)$mtime +currentPackages <- installed.packages(fields = "Packaged")[, c("Version", "Packaged")] while (TRUE) { - newPackages <- installed.packages(fields = "Packaged")[, c("Version", "Packaged")] - if (!identical(currentPackages, newPackages)) { - if (!is.null(currentPackages)) { + Sys.sleep(5) + current_mtimes <- file.info(lib_dirs)$mtime + if (!identical(last_mtimes, current_mtimes)) { + newPackages <- installed.packages(fields = "Packaged")[, c("Version", "Packaged")] + if (!identical(currentPackages, newPackages)) { cat(NEW_PACKAGE_STRING, "\n") + currentPackages <- newPackages } - currentPackages <- newPackages + last_mtimes <- current_mtimes + gc() } - Sys.sleep(1) } diff --git a/R/install_sess.R b/R/install_sess.R new file mode 100644 index 000000000..4901e656d --- /dev/null +++ b/R/install_sess.R @@ -0,0 +1,31 @@ +local({ + args <- commandArgs(trailingOnly = TRUE) + if (length(args) < 2) { + stop("Missing arguments: pkg_path and repo") + } + pkg_path <- args[1] + repo <- args[2] + + if (!file.exists(file.path(pkg_path, "DESCRIPTION"))) { + stop(paste("DESCRIPTION file not found in", pkg_path)) + } + + desc <- read.dcf(file.path(pkg_path, "DESCRIPTION")) + deps <- if ("Imports" %in% colnames(desc)) desc[, "Imports"] else "" + deps <- unlist(strsplit(deps, ",")) + deps <- gsub("\\s*\\(.*\\)", "", deps) + deps <- trimws(deps) + # Filter out base packages and already installed packages + deps <- deps[nzchar(deps)] + installed <- rownames(installed.packages()) + base_pkgs <- rownames(installed.packages(priority = "base")) + deps <- deps[!deps %in% base_pkgs & !deps %in% installed] + + if (length(deps) > 0) { + message("Installing dependencies: ", paste(deps, collapse = ", ")) + install.packages(deps, repos = repo) + } + + message("Installing sess package from: ", pkg_path) + install.packages(pkg_path, repos = NULL, type = "source") +}) diff --git a/R/profile.R b/R/profile.R new file mode 100644 index 000000000..0a9b081ff --- /dev/null +++ b/R/profile.R @@ -0,0 +1,34 @@ +# Source the original .Rprofile +local({ + try_source <- function(file) { + if (file.exists(file)) { + source(file) + TRUE + } else { + FALSE + } + } + + r_profile <- Sys.getenv("R_PROFILE_USER_OLD") + Sys.setenv( + R_PROFILE_USER_OLD = "", + R_PROFILE_USER = r_profile + ) + + if (nzchar(r_profile)) { + try_source(r_profile) + } else { + try_source(".Rprofile") || try_source(file.path("~", ".Rprofile")) + } + + invisible() +}) + +if (requireNamespace("sess", quietly = TRUE)) { + sess::sess_app( + port = as.integer(Sys.getenv("SESS_PORT")), + token = Sys.getenv("SESS_TOKEN"), + use_rstudioapi = as.logical(Sys.getenv("SESS_RSTUDIOAPI", "TRUE")), + use_httpgd = as.logical(Sys.getenv("SESS_USE_HTTPGD", "TRUE")) + ) +} diff --git a/README.md b/README.md index a2a3671fa..d5908d7cd 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,24 @@ # R Extension for Visual Studio Code -[![Badge](https://aka.ms/vsls-badge)](https://aka.ms/vsls) - This [VS Code](https://code.visualstudio.com/) extension provides support for the [R programming language](https://www.r-project.org), including features such as R language service based on code analysis, interacting with R terminals, viewing data, plots, workspace variables, help pages, managing packages, and working with [R Markdown](https://rmarkdown.rstudio.com/) documents. The R and R Markdown syntaxes are located in a slibing package [vscode-R-syntax](https://github.com/REditorSupport/vscode-R-syntax). Go to the [wiki](https://github.com/REditorSupport/vscode-R/wiki) to view the documentation of the extension. +## What's new in 3.0.0-rc + +Version 3.0.0 introduces a major architectural shift for session watching: + +* **`sess` R Package**: Replaces the legacy file-based IPC with a modern, + in-memory WebSocket architecture using JSON-RPC 2.0. +* **Better Performance and Reliability**: No more OS-level file watchers. + Communication is faster and more robust. +* **Automatic Installation**: The extension will prompt you to install the + `sess` package when you start an R session if it is not available. + +See the [sess package README](./sess/README.md) for more details on the protocol. + ## Getting started 1. [Install R](https://cloud.r-project.org/) (>= 3.4.0) on your system. For Windows users, Writing R Path to the registry is recommended in the installation. @@ -66,8 +77,6 @@ Go to the installation wiki pages ([Windows](https://github.com/REditorSupport/v * Full support of [Remote Development](https://code.visualstudio.com/docs/remote/remote-overview) via [SSH](https://code.visualstudio.com/docs/remote/ssh), [Containers](https://code.visualstudio.com/docs/remote/containers) and [WSL](https://code.visualstudio.com/docs/remote/wsl). -* [Live share collaboration](https://github.com/REditorSupport/vscode-R/wiki/Live-share-collaboration): Shared workspace, terminal, and viewer in R pair programming. - ## Questions, issues, feature requests, and contributions * If you have a question about accomplishing something in general with the extension, please [ask on Stack Overflow](https://stackoverflow.com/questions/tagged/visual-studio-code+r). diff --git a/esbuild.js b/esbuild.js index b505d1d7e..20a4d5f99 100644 --- a/esbuild.js +++ b/esbuild.js @@ -11,7 +11,8 @@ function copyResources() { const resources = [ './node_modules/jquery/dist/jquery.min.js', - './node_modules/jquery.json-viewer/json-viewer', + './node_modules/jquery.json-viewer/json-viewer/jquery.json-viewer.js', + './node_modules/jquery.json-viewer/json-viewer/jquery.json-viewer.css', './node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js', './node_modules/ag-grid-community/styles/ag-grid.min.css', './node_modules/ag-grid-community/styles/ag-theme-balham.min.css' @@ -22,7 +23,7 @@ function copyResources() { const destName = path.basename(srcPath); const destPath = path.resolve(destDir, destName); if (fs.existsSync(srcPath)) { - fs.cpSync(srcPath, destPath, { recursive: true }); + fs.copyFileSync(srcPath, destPath); } else { console.warn(`Warning: Resource not found: ${srcPath}`); } @@ -30,10 +31,34 @@ function copyResources() { console.log('Resources copied.'); } +function copyWebviewAssets() { + const views = [ + { name: 'help', src: 'src/helpViewer/webview' }, + { name: 'httpgd', src: 'src/plotViewer/webview' }, + { name: 'webview', src: 'src/webViewer/webview' } + ]; + + for (const view of views) { + const srcDir = path.join(__dirname, view.src); + const destDir = path.join(__dirname, 'dist', 'webviews', view.name); + fs.mkdirSync(destDir, { recursive: true }); + + const files = fs.readdirSync(srcDir); + for (const file of files) { + if (!file.endsWith('.ts')) { + fs.copyFileSync(path.join(srcDir, file), path.join(destDir, file)); + } + } + } + console.log('Webview assets copied.'); +} + async function main() { copyResources(); + copyWebviewAssets(); - const ctx = await esbuild.context({ + // Extension context (Node) + const extensionCtx = await esbuild.context({ entryPoints: ['./src/extension.ts'], bundle: true, format: 'cjs', @@ -46,12 +71,33 @@ async function main() { logLevel: 'info', }); + // Webview context (Browser) + const webviewCtx = await esbuild.context({ + entryPoints: { + 'help/index': './src/helpViewer/webview/index.ts', + 'httpgd/index': './src/plotViewer/webview/index.ts', + 'webview/index': './src/webViewer/webview/index.ts' + }, + bundle: true, + minify: production, + sourcemap: !production, + format: 'iife', + platform: 'browser', + outdir: 'dist/webviews', + logLevel: 'info', + }); + if (watch) { - await ctx.watch(); + await Promise.all([ + extensionCtx.watch(), + webviewCtx.watch() + ]); console.log('Watching for changes...'); } else { - await ctx.rebuild(); - await ctx.dispose(); + await extensionCtx.rebuild(); + await webviewCtx.rebuild(); + await extensionCtx.dispose(); + await webviewCtx.dispose(); } } diff --git a/package-lock.json b/package-lock.json index 5f82bb2f3..a13d75e36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,56 +1,58 @@ { "name": "r", - "version": "2.8.7", + "version": "3.0.0-rc.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "r", - "version": "2.8.7", + "version": "3.0.0-rc.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "ag-grid-community": "^31.3.2", + "ag-grid-community": "^31.3.4", "cheerio": "1.0.0-rc.12", "crypto": "^1.0.1", "ejs": "^3.1.10", - "fs-extra": "^10.0.0", - "highlight.js": "^11.9.0", + "fs-extra": "^10.1.0", + "highlight.js": "^11.11.1", "httpgd": "0.1.6", "jquery": "^3.7.1", "jquery.json-viewer": "^1.5.0", - "js-yaml": "^4.1.0", - "node-fetch": "^2.6.7", + "js-yaml": "^4.1.1", + "node-fetch": "^2.7.0", "vscode-languageclient": "^9.0.1", "vsls": "^1.0.4753", - "winreg": "^1.2.4" + "winreg": "^1.2.5", + "ws": "^8.19.0" }, "devDependencies": { - "@types/cheerio": "^0.22.29", - "@types/ejs": "^3.0.6", - "@types/express": "^4.17.12", - "@types/fs-extra": "^9.0.11", - "@types/glob": "^8.0.0", + "@types/cheerio": "^0.22.35", + "@types/ejs": "^3.1.5", + "@types/express": "^4.17.25", + "@types/fs-extra": "^9.0.13", "@types/highlight.js": "^10.1.0", - "@types/js-yaml": "^4.0.2", - "@types/mocha": "^8.2.2", - "@types/node": "^18.17.1", - "@types/node-fetch": "^2.5.10", - "@types/sinon": "^10.0.13", + "@types/js-yaml": "^4.0.9", + "@types/mocha": "^8.2.3", + "@types/node": "^18.19.130", + "@types/node-fetch": "^2.6.13", + "@types/sinon": "^10.0.20", "@types/vscode": "^1.75.0", - "@types/winreg": "^1.2.31", - "@typescript-eslint/eslint-plugin": "^5.30.0", - "@typescript-eslint/parser": "^5.30.0", - "@vscode/test-electron": "^2.2.3", - "esbuild": "^0.27.3", - "eslint": "^7.28.0", - "eslint-plugin-jsdoc": "^35.1.3", + "@types/winreg": "^1.2.36", + "@types/ws": "^8.18.1", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "@vscode/test-cli": "^0.0.12", + "@vscode/test-electron": "^2.5.2", + "esbuild": "^0.27.4", + "eslint": "^7.32.0", + "eslint-plugin-jsdoc": "^35.5.1", "git-cliff": "^2.12.0", - "mocha": "^11.1.0", - "sinon": "^15.0.1", - "typescript": "^4.7.2" + "mocha": "^11.7.5", + "sinon": "^15.2.0", + "typescript": "^4.9.5" }, "engines": { - "vscode": "^1.75.0" + "vscode": "^1.110.0" } }, "node_modules/@babel/code-frame": { @@ -144,16 +146,6 @@ "node": ">=0.8.0" } }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/highlight/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -167,6 +159,16 @@ "node": ">=4" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@es-joy/jsdoccomment": { "version": "0.9.0-alpha.1", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.9.0-alpha.1.tgz", @@ -694,6 +696,17 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@eslint/eslintrc/node_modules/ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -718,6 +731,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", @@ -734,6 +760,30 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", @@ -789,6 +839,44 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@microsoft/servicehub-framework": { "version": "2.6.74", "resolved": "https://registry.npmjs.org/@microsoft/servicehub-framework/-/servicehub-framework-2.6.74.tgz", @@ -1003,17 +1091,6 @@ "@types/node": "*" } }, - "node_modules/@types/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/minimatch": "^5.1.2", - "@types/node": "*" - } - }, "node_modules/@types/highlight.js": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-10.1.0.tgz", @@ -1032,6 +1109,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/js-yaml": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", @@ -1053,13 +1137,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/mocha": { "version": "8.2.3", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.3.tgz", @@ -1159,9 +1236,9 @@ "license": "MIT" }, "node_modules/@types/vscode": { - "version": "1.110.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", - "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.75.0.tgz", + "integrity": "sha512-SAr0PoOhJS6FUq5LjNr8C/StBKALZwDVm3+U4pjF/3iYkt3GioJOPV/oB1Sf1l7lROe4TgrMyL5N1yaEgTWycw==", "dev": true, "license": "MIT" }, @@ -1377,6 +1454,37 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vscode/test-cli": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.12.tgz", + "integrity": "sha512-iYN0fDg29+a2Xelle/Y56Xvv7Nc8Thzq4VwpzAF/SIE6918rDicqfsQxV6w1ttr2+SOm+10laGuY9FG2ptEKsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mocha": "^10.0.10", + "c8": "^10.1.3", + "chokidar": "^3.6.0", + "enhanced-resolve": "^5.18.3", + "glob": "^10.3.10", + "minimatch": "^9.0.3", + "mocha": "^11.7.4", + "supports-color": "^10.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "vscode-test": "out/bin.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vscode/test-cli/node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@vscode/test-electron": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz", @@ -1486,6 +1594,20 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1537,6 +1659,19 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -1544,14 +1679,12 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -1574,6 +1707,40 @@ "dev": true, "license": "ISC" }, + "node_modules/c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1640,6 +1807,29 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cheerio": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", @@ -1679,19 +1869,28 @@ } }, "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 8.10.0" }, "funding": { "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, "node_modules/cli-cursor": { @@ -1828,6 +2027,13 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2077,6 +2283,20 @@ "dev": true, "license": "MIT" }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -2363,6 +2583,17 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", @@ -2397,6 +2628,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/espree": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", @@ -2638,15 +2882,6 @@ "minimatch": "^5.0.1" } }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/filelist/node_modules/minimatch": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", @@ -2776,6 +3011,21 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3015,40 +3265,14 @@ "node": ">= 6" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" + "type-fest": "^0.20.2" }, "engines": { "node": ">=8" @@ -3105,13 +3329,13 @@ "license": "MIT" }, "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/has-symbols": { @@ -3175,6 +3399,13 @@ "node": ">=12.0.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -3333,6 +3564,19 @@ "integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==", "license": "MIT" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3460,6 +3704,68 @@ "ws": "*" } }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -3695,6 +4001,22 @@ "dev": true, "license": "ISC" }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3766,16 +4088,19 @@ } }, "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.2" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { @@ -3825,30 +4150,44 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/mocha/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/mocha/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/mocha/node_modules/supports-color": { @@ -3959,6 +4298,16 @@ } } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-run-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", @@ -4459,17 +4808,16 @@ } }, "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 14.18.0" + "dependencies": { + "picomatch": "^2.2.1" }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=8.10.0" } }, "node_modules/regexpp": { @@ -4570,6 +4918,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4592,6 +4951,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4718,6 +5090,29 @@ "node": ">=0.3.1" } }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -4931,16 +5326,16 @@ } }, "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/table": { @@ -5006,6 +5401,74 @@ "node": ">=8" } }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5157,6 +5620,21 @@ "dev": true, "license": "MIT" }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", @@ -5180,15 +5658,6 @@ "vscode": "^1.82.0" } }, - "node_modules/vscode-languageclient/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/vscode-languageclient/node_modules/minimatch": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", diff --git a/package.json b/package.json index d9d5fc636..9c9888326 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "r", "displayName": "R", "description": "R Extension for Visual Studio Code", - "version": "2.8.8", + "version": "3.0.0-rc.0", "author": "REditorSupport", "license": "SEE LICENSE IN LICENSE", "publisher": "REditorSupport", @@ -26,7 +26,7 @@ "R Markdown" ], "engines": { - "vscode": "^1.75.0" + "vscode": "^1.110.0" }, "activationEvents": [ "workspaceContains:**/*.{rproj,Rproj,r,R,rd,Rd,rmd,Rmd}", @@ -67,14 +67,6 @@ "icon": "./images/Rlogo.svg", "contextualTitle": "R", "when": "r.helpViewer.show" - }, - { - "id": "rLiveShare", - "name": "Live Share Controls", - "icon": "./images/Rlogo.svg", - "contextualTitle": "R", - "when": "r.WorkspaceViewer:show && !r.liveShare:isGuest", - "visibility": "collapsed" } ] }, @@ -87,16 +79,6 @@ { "view": "rHelpPages", "contents": "R Help Pages" - }, - { - "view": "rLiveShare", - "contents": "R Live Share not active.", - "when": "!r.liveShare:aborted" - }, - { - "view": "rLiveShare", - "contents": "Could not connect to Live Share service.", - "when": "r.liveShare:aborted" } ], "languages": [ @@ -193,6 +175,16 @@ } ], "commands": [ + { + "command": "r.liveShare.toggle", + "title": "Toggle Live Share", + "category": "R" + }, + { + "command": "r.liveShare.retry", + "title": "Retry Live Share", + "category": "R" + }, { "command": "r.workspaceViewer.refreshEntry", "title": "Manual Refresh", @@ -393,9 +385,14 @@ "command": "r.generateCCppProperties" }, { - "title": "Attach Active Terminal", + "title": "Activate R Session", + "category": "R", + "command": "r.activateRSession" + }, + { + "title": "Attach External R Session (Copy command)", "category": "R", - "command": "r.attachActive" + "command": "r.connectToSession" }, { "title": "Run Command With Selection or Word in Terminal", @@ -679,17 +676,6 @@ "category": "R Help Panel", "command": "r.helpPanel.openForPath" }, - { - "command": "r.liveShare.toggle", - "category": "R Live Share", - "title": "Toggle" - }, - { - "command": "r.liveShare.retry", - "title": "Retry connection to Live Share service", - "category": "R Live Share", - "icon": "$(refresh)" - }, { "title": "Toggle Style", "category": "R Plot", @@ -1615,26 +1601,26 @@ "r.session.useWebServer": { "type": "boolean", "default": false, - "markdownDescription": "Enable experimental use of web server in the R session to handle session requests from the extension. Changes the option `vsc.use_webserver` in R. Requires `#r.sessionWatcher#` to be set to `true`. Requires the `httpuv` R package." + "markdownDescription": "Enable experimental use of web server in the R session to handle session requests from the extension. Requires `#r.sessionWatcher#` to be set to `true`. Requires the `httpuv` R package." }, "r.session.watchGlobalEnvironment": { "type": "boolean", "default": true, - "markdownDescription": "Watch the global environment to provide hover, autocompletions, and workspace viewer information. Changes the option `vsc.globalenv` in R. Requires `#r.sessionWatcher#` to be set to `true`." + "markdownDescription": "Watch the global environment to provide hover, autocompletions, and workspace viewer information. Requires `#r.sessionWatcher#` to be set to `true`." }, "r.session.objectLengthLimit": { "type": "integer", "default": 2000, - "markdownDescription": "The upper limit of object length to show object details in workspace viewer and provide session symbol completion. Decrease this value if you experience significant delay after executing R commands caused by large global objects with many elements. Changes the option `vsc.object_length_limit` in R. Requires `#r.sessionWatcher#` to be set to `true`." + "markdownDescription": "The upper limit of object length to show object details in workspace viewer and provide session symbol completion. Decrease this value if you experience significant delay after executing R commands caused by large global objects with many elements. Requires `#r.sessionWatcher#` to be set to `true`." }, "r.session.objectTimeout": { "type": "integer", "default": 50, - "markdownDescription": "The maximum number of milliseconds to get information of a single object in the global environment. Decrease this value if you experience significant delay after executing R commands caused by large global objects with many elements. Changes the option `vsc.object_timeout` in R. Requires `#r.sessionWatcher#` to be set to `true`." + "markdownDescription": "The maximum number of milliseconds to get information of a single object in the global environment. Decrease this value if you experience significant delay after executing R commands caused by large global objects with many elements. Requires `#r.sessionWatcher#` to be set to `true`." }, "r.session.levelOfObjectDetail": { "type": "string", - "markdownDescription": "How much of the object to show on hover, autocompletion, and in the workspace viewer? Changes the option `vsc.str.max.level` in R. Requires `#r.sessionWatcher#` to be set to `true`.", + "markdownDescription": "How much of the object to show on hover, autocompletion, and in the workspace viewer? Requires `#r.sessionWatcher#` to be set to `true`.", "default": "Minimal", "enum": [ "Minimal", @@ -1650,12 +1636,12 @@ "r.session.emulateRStudioAPI": { "type": "boolean", "default": true, - "markdownDescription": "Emulate the RStudio API for addin support and other {rstudioapi} calls. Changes the option `vsc.rstudioapi` in R. Requires `#r.sessionWatcher#` to be set to `true`." + "markdownDescription": "Emulate the RStudio API for addin support and other {rstudioapi} calls. Requires `#r.sessionWatcher#` to be set to `true`." }, "r.session.data.rowLimit": { "type": "integer", "default": 0, - "markdownDescription": "The maximum number of rows to be displayed in the data viewer. `0` means no limit. Changes the option `vsc.row_limit` in R. Requires `#r.sessionWatcher#` to be set to `true`." + "markdownDescription": "The maximum number of rows to be displayed in the data viewer. `0` means no limit. Changes the option `sess.row_limit` in R. Requires `#r.sessionWatcher#` to be set to `true`." }, "r.session.data.pageSize": { "type": "integer", @@ -1676,7 +1662,7 @@ "properties": { "plot": { "type": "string", - "description": "Which view column to show the plot file on graphics update? \n\nChanges the option 'vsc.plot' in R.", + "description": "Which view column to show the plot viewer on graphics update?", "enum": [ "Two", "Active", @@ -1693,7 +1679,7 @@ }, "browser": { "type": "string", - "description": "Which view column to show the WebView triggered by browser (e.g. shiny apps)? \n\nChanges the option 'vsc.browser' in R.", + "description": "Which view column to show the WebView triggered by browser (e.g. shiny apps)?", "enum": [ "Two", "Active", @@ -1710,7 +1696,7 @@ }, "viewer": { "type": "string", - "description": "Which view column to show the WebView triggered by viewer (e.g. htmlwidgets)? \n\nChanges the option 'vsc.viewer' in R.", + "description": "Which view column to show the WebView triggered by viewer (e.g. htmlwidgets)?", "enum": [ "Two", "Active", @@ -1727,7 +1713,7 @@ }, "pageViewer": { "type": "string", - "description": "Which view column to show the WebView triggered by the page viewer (e.g. profvis)? \n\nChanges the option 'vsc.page_viewer' in R.", + "description": "Which view column to show the WebView triggered by the page viewer (e.g. profvis)?", "enum": [ "Two", "Active", @@ -1744,7 +1730,7 @@ }, "view": { "type": "string", - "description": "Which view column to show the WebView triggered by View()? \n\nChanges the option 'vsc.view' in R.", + "description": "Which view column to show the WebView triggered by View()? \n\nChanges the option 'sess.dataview' in R.", "enum": [ "Two", "Active", @@ -1761,7 +1747,7 @@ }, "helpPanel": { "type": "string", - "description": "Which view column to show the WebView triggered by the help panel? \n\nChanges the option 'vsc.help_panel' in R.", + "description": "Which view column to show the WebView triggered by the help panel? \n\nChanges the option 'sess.helpPanel' in R.", "enum": [ "Two", "Active", @@ -1787,7 +1773,7 @@ "r.workspaceViewer.showObjectSize": { "type": "boolean", "default": false, - "markdownDescription": "Show object size when hovering over a workspace viewer item. Changes the option `vsc.show_object_size` in R." + "markdownDescription": "Show object size when hovering over a workspace viewer item." }, "r.workspaceViewer.removeHiddenItems": { "type": "boolean", @@ -1821,7 +1807,7 @@ }, "r.plot.devArgs": { "type": "object", - "markdownDescription": "The arguments for the png device to replay user graphics to show in VSCode. Requires `#r.plot.useHttpgd#` to be set to `false`. \n\nChanges the option `vsc.dev.args` in R.", + "markdownDescription": "The supplementary arguments for the rendering device (e.g., `png()` or `svglite()`) used by the standard plot viewer. Note that width and height are now handled dynamically by the responsive viewer and will be overridden. Requires `#r.plot.useHttpgd#` to be set to `false`. \n\nChanges the option `sess.devArgs` in R.", "default": { "width": 800, "height": 1200 @@ -1829,12 +1815,12 @@ "properties": { "width": { "type": "number", - "description": "Width of the graphic device.", + "description": "Width of the graphic device (Note: This is now handled dynamically by the responsive viewer).", "default": 480 }, "height": { "type": "number", - "description": "Height of the graphic device.", + "description": "Height of the graphic device (Note: This is now handled dynamically by the responsive viewer).", "default": 480 }, "units": { @@ -1855,7 +1841,16 @@ "r.plot.useHttpgd": { "type": "boolean", "default": false, - "markdownDescription": "Use the httpgd-based plot viewer instead of the base VSCode-R plot viewer. Changes the option `vsc.use_httpgd` in R.\n\nRequires the `httpgd` R package version 1.2.0 or later." + "markdownDescription": "Use the httpgd-based plot viewer instead of the base VSCode-R plot viewer.\n\nRequires the `httpgd` R package version 1.2.0 or later." + }, + "r.plot.format": { + "type": "string", + "default": "svglite", + "enum": [ + "png", + "svglite" + ], + "description": "The graphics format to use for the standard plot viewer. Requires `#r.plot.useHttpgd#` to be set to `false`." }, "r.plot.defaults.colorTheme": { "type": "string", @@ -1983,56 +1978,56 @@ ] }, "scripts": { - "vscode:prepublish": "tsc -p ./html/help && tsc -p ./html/httpgd && node esbuild.js --production", + "vscode:prepublish": "node esbuild.js --production", "changelog": "npx git-cliff v2.8.5.. -o", - "build": "tsc -p ./html/help && tsc -p ./html/httpgd && node esbuild.js", + "build": "node esbuild.js && Rscript -e \"remotes::install_local('sess', force=TRUE)\"", "watch": "node esbuild.js --watch", - "watchHelp": "tsc -p ./html/help --watch", - "watchHttpgd": "tsc -p ./html/httpgd --watch", - "pretest": "tsc -p ./", - "test": "node ./out/test/runTest.js", - "lint": "eslint src --ext ts" + "clean": "rimraf out", + "pretest": "npm run clean && tsc -p ./", + "test": "vscode-test", + "lint": "eslint src --ext ts && Rscript -e \"lintr::lint_package('sess')\"" }, "devDependencies": { - "@types/cheerio": "^0.22.29", - "@types/ejs": "^3.0.6", - "@types/express": "^4.17.12", - "@types/fs-extra": "^9.0.11", - "@types/glob": "^8.0.0", + "@types/cheerio": "^0.22.35", + "@types/ejs": "^3.1.5", + "@types/express": "^4.17.25", + "@types/fs-extra": "^9.0.13", "@types/highlight.js": "^10.1.0", - "@types/js-yaml": "^4.0.2", - "@types/mocha": "^8.2.2", - "@types/node": "^18.17.1", - "@types/node-fetch": "^2.5.10", - "@types/sinon": "^10.0.13", + "@types/js-yaml": "^4.0.9", + "@types/mocha": "^8.2.3", + "@types/node": "^18.19.130", + "@types/node-fetch": "^2.6.13", + "@types/sinon": "^10.0.20", "@types/vscode": "^1.75.0", - "@types/winreg": "^1.2.31", - "@typescript-eslint/eslint-plugin": "^5.30.0", - "@typescript-eslint/parser": "^5.30.0", - "@vscode/test-electron": "^2.2.3", - "esbuild": "^0.27.3", - "eslint": "^7.28.0", - "eslint-plugin-jsdoc": "^35.1.3", + "@types/winreg": "^1.2.36", + "@types/ws": "^8.18.1", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "@vscode/test-cli": "^0.0.12", + "@vscode/test-electron": "^2.5.2", + "esbuild": "^0.27.4", + "eslint": "^7.32.0", + "eslint-plugin-jsdoc": "^35.5.1", "git-cliff": "^2.12.0", - "mocha": "^11.1.0", - "sinon": "^15.0.1", - "typescript": "^4.7.2" + "mocha": "^11.7.5", + "sinon": "^15.2.0", + "typescript": "^4.9.5" }, "dependencies": { - "ag-grid-community": "^31.3.2", + "ag-grid-community": "^31.3.4", "cheerio": "1.0.0-rc.12", "crypto": "^1.0.1", "ejs": "^3.1.10", - "fs-extra": "^10.0.0", - "highlight.js": "^11.9.0", + "fs-extra": "^10.1.0", + "highlight.js": "^11.11.1", "httpgd": "0.1.6", "jquery": "^3.7.1", "jquery.json-viewer": "^1.5.0", - "js-yaml": "^4.1.0", - "node-fetch": "^2.6.7", + "js-yaml": "^4.1.1", + "node-fetch": "^2.7.0", "vscode-languageclient": "^9.0.1", - "vsls": "^1.0.4753", - "winreg": "^1.2.4" + "winreg": "^1.2.5", + "ws": "^8.19.0" }, "extensionDependencies": [ "REditorSupport.r-syntax" diff --git a/sess/.lintr b/sess/.lintr new file mode 100644 index 000000000..a52d190b8 --- /dev/null +++ b/sess/.lintr @@ -0,0 +1,7 @@ +linters: linters_with_defaults( + line_length_linter = line_length_linter(100), + object_usage_linter = NULL, + object_length_linter = NULL, + object_name_linter = NULL, + commented_code_linter = NULL, + pipe_continuation_linter = NULL) diff --git a/sess/DESCRIPTION b/sess/DESCRIPTION new file mode 100644 index 000000000..812a9656b --- /dev/null +++ b/sess/DESCRIPTION @@ -0,0 +1,19 @@ +Package: sess +Type: Package +Title: Modern R IPC Server +Version: 3.0.0 +Author: Gemini +Maintainer: Gemini +Description: Implements a high-performance HTTP and WebSocket server for R Inter-Process Communication, replacing legacy file-system watchers. This package provides a generic protocol for IDEs and other clients to communicate with an R session. +License: MIT +Encoding: UTF-8 +LazyData: true +Imports: + websocket, + later, + jsonlite, + utils, + methods, + rstudioapi, + svglite +RoxygenNote: 7.3.3 diff --git a/sess/NAMESPACE b/sess/NAMESPACE new file mode 100644 index 000000000..b4d6a57d9 --- /dev/null +++ b/sess/NAMESPACE @@ -0,0 +1,6 @@ +# Generated by roxygen2: do not edit by hand + +export(notify_client) +export(register_hooks) +export(request_client) +export(sess_app) diff --git a/sess/R/dispatch.R b/sess/R/dispatch.R new file mode 100644 index 000000000..207cb3bb0 --- /dev/null +++ b/sess/R/dispatch.R @@ -0,0 +1,81 @@ +#' Send a message to the client via WebSocket (JSON-RPC 2.0) +#' +#' This is the internal workhorse for both Notifications and Requests. +#' +#' @param method String. The JSON-RPC method. +#' @param params List. The parameters for the method. +#' @param request Logical. If TRUE, sends a Request and waits for a Response. +#' @return The result of the request if request=TRUE, otherwise TRUE if sent. +#' @keywords internal +rpc_send <- function(method, params = list(), request = FALSE) { + if (is.null(.sess_env$ws)) { + return(invisible(FALSE)) + } + + msg <- list( + jsonrpc = "2.0", + method = method, + params = params + ) + + req_id <- NULL + if (request) { + req_id <- basename(tempfile("req_", tmpdir = .sess_env$tempdir)) + msg$id <- req_id + } + + # Push over the websocket + payload <- jsonlite::toJSON(msg, auto_unbox = TRUE, null = "null", force = TRUE) + tryCatch( + { + .sess_env$ws$send(payload) + }, + error = function(e) { + warning("Failed to send IPC message: ", e$message) + invisible(FALSE) + } + ) + + if (!request) { + return(invisible(TRUE)) + } + + # NON-BLOCKING WAIT: + # Process HTTP/WS events in the background while blocking the R console execution + # This prevents the R event loop from locking up. + while (is.null(.sess_env$pending_responses[[req_id]])) { + later::run_now() + Sys.sleep(0.01) + } + + # Retrieve and clean up response + response <- .sess_env$pending_responses[[req_id]] + .sess_env$pending_responses[[req_id]] <- NULL + + # Handle JSON-RPC Errors if any + if (inherits(response, "json_rpc_error")) { + stop(sprintf("JSON-RPC Error [%d]: %s", response$code, response$message)) + } + + response +} + +#' Notify the client via WebSocket (JSON-RPC 2.0 Notification) +#' +#' Pushes an event instantly to the client extension via the active WebSocket connection. +#' +#' @param method A string representing the action (e.g., "dataview", "plot_updated") +#' @param params A list containing the arguments for the command +#' @export +notify_client <- function(method, params = list()) { + rpc_send(method, params, request = FALSE) +} + +#' Emulate rstudioapi (or any client action) synchronously but without blocking the R Event Loop +#' +#' @param action String of the action name +#' @param args List of arguments +#' @export +request_client <- function(action, args = list()) { + rpc_send(action, args, request = TRUE) +} diff --git a/sess/R/handlers.R b/sess/R/handlers.R new file mode 100644 index 000000000..05e257710 --- /dev/null +++ b/sess/R/handlers.R @@ -0,0 +1,155 @@ +# Handlers for the client Pull Requests (HTTP GET/POST) + +get_workspace_data <- function() { + env <- .GlobalEnv + all_names <- ls(env) + + objs <- lapply(all_names, function(name) { + obj <- env[[name]] + list( + class = class(obj), + type = typeof(obj), + length = length(obj), + # Create a concise string representation + str = paste0( + utils::capture.output(utils::str(obj, max.level = 0, give.attr = FALSE)), + collapse = "\n" + ) + ) + }) + names(objs) <- all_names + + list( + globalenv = objs, + search = search()[-1], + loaded_namespaces = loadedNamespaces() + ) +} + +handle_hover <- function(expr_str) { + tryCatch( + { + expr <- parse(text = expr_str, keep.source = FALSE)[[1]] + obj <- eval(expr, .GlobalEnv) + str_preview <- paste0( + utils::capture.output(utils::str(obj, max.level = 0, give.attr = FALSE)), + collapse = "\n" + ) + list(str = str_preview) + }, + error = function(e) NULL + ) +} + +handle_complete <- function(expr_str, trigger = NULL) { + obj <- tryCatch( + { + expr <- parse(text = expr_str, keep.source = FALSE)[[1]] + eval(expr, .GlobalEnv) + }, + error = function(e) NULL + ) + + if (is.null(obj) || is.null(trigger)) { + return(NULL) + } + + if (trigger == "$") { + nms <- if (is.object(obj)) { + utils::.DollarNames(obj, pattern = "") + } else if (is.recursive(obj)) { + names(obj) + } else { + NULL + } + + if (is.null(nms)) { + return(NULL) + } + + return(lapply(nms, function(n) { + item <- obj[[n]] + list( + name = n, + type = typeof(item), + str = paste0(class(item), collapse = ", ") + ) + })) + } + + if (trigger == "@" && methods::isS4(obj)) { + nms <- methods::slotNames(obj) + return(lapply(nms, function(n) { + item <- methods::slot(obj, n) + list( + name = n, + type = typeof(item), + str = paste0(class(item), collapse = ", ") + ) + })) + } + + NULL +} + +handle_plot_latest <- function(params) { + record <- .sess_env$latest_plot_record + if (is.null(record)) { + return(list(data = NULL)) + } + + width <- if (is.null(params$width)) 800 else as.numeric(params$width) + height <- if (is.null(params$height)) 600 else as.numeric(params$height) + format <- if (is.null(params$format)) "svglite" else as.character(params$format) + + plot_file <- tempfile(tmpdir = .sess_env$tempdir, fileext = paste0(".", format)) + + dev_args <- params$devArgs + if (is.null(dev_args)) { + dev_args <- list() + } + # Remove arguments that we handle ourselves + dev_args$filename <- NULL + dev_args$file <- NULL + dev_args$width <- NULL + dev_args$height <- NULL + dev_args$res <- NULL + + if (format == "svglite") { + if (requireNamespace("svglite", quietly = TRUE)) { + do.call(svglite::svglite, c(list( + filename = plot_file, width = width / 72, height = height / 72 + ), dev_args)) + } else { + # Fallback to png + do.call(grDevices::png, c(list( + filename = plot_file, width = width, height = height, res = 72 + ), dev_args)) + } + } else { + do.call(grDevices::png, c(list( + filename = plot_file, width = width, height = height, res = 72 + ), dev_args)) + } + + on.exit({ + if (file.exists(plot_file)) unlink(plot_file) + }) + + grDevices::replayPlot(record) + grDevices::dev.off() + + if (file.exists(plot_file)) { + raw_img <- readBin(plot_file, "raw", file.info(plot_file)$size) + list( + data = as.character(jsonlite::base64_enc(raw_img)), + format = if (format == "svglite" && !requireNamespace("svglite", quietly = TRUE)) { + "png" + } else { + format + } + ) + } else { + list(data = NULL) + } +} diff --git a/sess/R/hooks.R b/sess/R/hooks.R new file mode 100644 index 000000000..5f7dcda49 --- /dev/null +++ b/sess/R/hooks.R @@ -0,0 +1,296 @@ +dataview_data_type <- function(x) { + if (is.numeric(x)) { + if (is.null(attr(x, "class"))) { + "num" + } else { + "num-fmt" + } + } else if (inherits(x, "Date")) { + "date" + } else { + "string" + } +} + +dataview_table <- function(data) { + if (is.data.frame(data)) { + nrow <- nrow(data) + colnames <- colnames(data) + if (is.null(colnames)) { + colnames <- sprintf("(X%d)", seq_len(ncol(data))) + } else { + colnames <- trimws(colnames) + } + if (.row_names_info(data) > 0L) { + rownames <- rownames(data) + rownames(data) <- NULL + } else { + rownames <- seq_len(nrow) + } + data <- c(list(" " = rownames), .subset(data)) + colnames <- c(" ", colnames) + types <- vapply(data, dataview_data_type, + character(1L), + USE.NAMES = FALSE + ) + data <- vapply(data, function(x) { + trimws(format(x)) + }, character(nrow), USE.NAMES = FALSE) + dim(data) <- c(length(rownames), length(colnames)) + } else if (is.matrix(data)) { + if (is.factor(data)) { + data <- format(data) + } + types <- rep(dataview_data_type(data), ncol(data)) + colnames <- colnames(data) + colnames(data) <- NULL + if (is.null(colnames)) { + colnames <- sprintf("(X%d)", seq_len(ncol(data))) + } else { + colnames <- trimws(colnames) + } + rownames <- rownames(data) + rownames(data) <- NULL + data <- trimws(format(data)) + if (is.null(rownames)) { + types <- c("num", types) + rownames <- seq_len(nrow(data)) + } else { + types <- c("string", types) + rownames <- trimws(rownames) + } + dim(data) <- c(length(rownames), length(colnames)) + colnames <- c(" ", colnames) + data <- cbind(rownames, data) + } else { + stop("data must be data.frame or matrix") + } + columns <- .mapply(function(title, type, index) { + class <- if (type == "string") "text-left" else "text-right" + list( + headerName = jsonlite::unbox(title), + field = jsonlite::unbox(as.character(index - 1L)), + cellClass = jsonlite::unbox(class), + type = jsonlite::unbox(if (type == "date") "dateColumn" else type) + ) + }, list(colnames, types, seq_along(colnames)), NULL) + list(columns = columns, data = data) +} + +#' Register hooks for the client IPC +#' +#' @param use_rstudioapi Logical. Enable rstudioapi emulation. +#' @param use_httpgd Logical. Enable httpgd plot device if available. +#' @export +register_hooks <- function(use_rstudioapi = TRUE, use_httpgd = TRUE) { + # 1. Override View() to push data directly via WebSocket + show_dataview <- function(x, title = deparse(substitute(x))) { + # make sure title is computed. + force(title) + # Dump to a temporary file locally so the payload size over WS isn't massive + file_path <- tempfile(tmpdir = .sess_env$tempdir, fileext = ".json") + + row_limit <- abs(getOption("sess.row_limit", 100)) + + as_truncated_data <- function(.data) { + .nrow <- nrow(.data) + if (row_limit != 0 && row_limit < .nrow) { + title <<- sprintf("%s (limited to %d/%d)", title, row_limit, .nrow) + .data <- utils::head(.data, n = row_limit) + } + .data + } + + if (inherits(x, "ArrowTabular")) { + x <- as_truncated_data(x) + x <- as.data.frame(x) + } + + if (is.data.frame(x) || is.matrix(x)) { + x <- as_truncated_data(x) + data <- dataview_table(x) + jsonlite::write_json( + data, file_path, + matrix = "rowmajor", auto_unbox = TRUE, null = "null", na = "string" + ) + + notify_client("dataview", list( + title = title, + file = file_path, + source = "table", + type = "json" + )) + } else if (is.list(x)) { + jsonlite::write_json(x, file_path, auto_unbox = TRUE, null = "null", na = "string") + notify_client("dataview", list( + title = title, + file = file_path, + source = "list", + type = "json" + )) + } else { + code <- if (is.primitive(x)) utils::capture.output(print(x)) else deparse(x) + file_path <- tempfile(tmpdir = .sess_env$tempdir, fileext = ".R") + writeLines(code, file_path) + notify_client("dataview", list( + title = title, + file = file_path, + source = "object", + type = "R" + )) + } + } + rebind("View", show_dataview, ns = "utils") + + # 2. Browser & Webview Options + make_viewer <- function(method) { + function(url, ...) { + if (!is.character(url)) { + real_url <- NULL + temp_viewer <- function(url, ...) { + real_url <<- url + } + op <- options(viewer = temp_viewer, page_viewer = temp_viewer, browser = temp_viewer) + on.exit(options(op)) + print(url) + if (is.character(real_url)) { + url <- real_url + } else { + stop("Invalid object") + } + } + + url <- sub("^file\\://", "", url) + if (file.exists(url)) { + url <- normalizePath(url, "/", mustWork = TRUE) + } + notify_client(method, list(url = url)) + } + } + + options( + browser = make_viewer("browser"), + viewer = make_viewer("webview"), + page_viewer = make_viewer("page_viewer"), + help_type = "html" + ) + + # 3. Help System Interception + sess_print.help_files_with_topic <- function(x, ...) { + if (length(x) >= 1 && is.character(x)) { + file <- x[1] + pkgname <- basename(dirname(dirname(file))) + requestPath <- paste0("/library/", pkgname, "/html/", basename(file), ".html") + notify_client("help", list( + requestPath = requestPath, + viewer = getOption("sess.helpPanel", "Two") + )) + } else { + utils:::print.help_files_with_topic(x, ...) + } + invisible(x) + } + registerS3method( + "print", "help_files_with_topic", sess_print.help_files_with_topic, + envir = asNamespace("utils") + ) + + sess_print.hsearch <- function(x, ...) { + if (length(x) >= 1) { + requestPath <- paste0("/doc/html/Search?pattern=", tools:::escapeAmpersand(x$pattern)) + notify_client("help", list( + requestPath = requestPath, + viewer = getOption("sess.helpPanel", "Two") + )) + } else { + utils:::print.hsearch(x, ...) + } + invisible(x) + } + + # 4. httpgd or Static Plot Hook + if (use_httpgd && requireNamespace("httpgd", quietly = TRUE)) { + options(device = function(...) { + httpgd::hgd(silent = TRUE) + notify_client("httpgd", list(url = httpgd::hgd_url())) + }) + } else { + # Default to static plot capturing (Re-implementation based on legacy plot handler) + plot_file <- .sess_env$latest_plot_path + file.create(plot_file, showWarnings = FALSE) + + plot_updated <- FALSE + last_plot_record_length <- 0 + + check_null_dev <- function() { + cur <- grDevices::dev.cur() + id <- getOption("sess.null_dev") + !is.null(id) && cur == id + } + + new_plot <- function() { + if (check_null_dev()) { + plot_updated <<- TRUE + } + } + + options(device = function(...) { + grDevices::pdf(NULL, width = 7, height = 7, bg = "white") + options(sess.null_dev = grDevices::dev.cur()) + grDevices::dev.control(displaylist = "enable") + }) + + update_plot <- function(...) { + tryCatch( + { + if (check_null_dev()) { + # Only record if we are reasonably sure there is something to record + # and we are on the null device. + record <- grDevices::recordPlot() + if (length(record[[1L]])) { + curr_length <- length(record[[1L]]) + if (plot_updated || curr_length != last_plot_record_length) { + plot_updated <<- FALSE + last_plot_record_length <<- curr_length + .sess_env$latest_plot_record <- record + notify_client("plot_updated") + } + } + } + }, + error = function(e) { + warning("Error in sess update_plot: ", e$message) + } + ) + TRUE + } + + setHook("plot.new", new_plot, "replace") + setHook("grid.newpage", new_plot, "replace") + + update_plot() + addTaskCallback(update_plot, name = "sess.plot") + } + + # 5. rstudioapi hooks + if (use_rstudioapi) { + setHook(packageEvent("rstudioapi", "onLoad"), function(...) { + patch_rstudioapi() + }, action = "append") + + if ("rstudioapi" %in% loadedNamespaces()) { + patch_rstudioapi() + } + } + + # 6. Workspace Update Callback + # This notifies the client whenever a top-level command is completed, + # suggesting that the Global Environment might have changed. + removeTaskCallback("sess.workspace") + addTaskCallback(function(...) { + notify_client("workspace_updated") + TRUE + }, name = "sess.workspace") + + invisible(NULL) +} diff --git a/sess/R/rstudioapi.R b/sess/R/rstudioapi.R new file mode 100644 index 000000000..5b73e772d --- /dev/null +++ b/sess/R/rstudioapi.R @@ -0,0 +1,457 @@ +getActiveDocumentContext <- function() { + editor_context <- request_client("rstudioapi/active_editor_context", args = list()) + make_rs_document_context(editor_context) +} + +getSourceEditorContext <- getActiveDocumentContext + +verifyAvailable <- function(version_needed = NULL) { + if (is.null(version_needed)) { + return(TRUE) + } + getVersion() >= numeric_version(version_needed) +} + +isAvailable <- function(version_needed = NULL, child_ok = FALSE) { + verifyAvailable(version_needed) +} + +insertText <- function(location, text, id = NULL) { + if (missing(text) && is.character(location) && length(location) == 1) { + return(invisible(request_client( + "rstudioapi/replace_text_in_current_selection", + args = list(text = location, id = id) + ))) + } else if (missing(location)) { + return(invisible(request_client( + "rstudioapi/replace_text_in_current_selection", + args = list(text = text, id = id) + ))) + } else if (is.null(location) && missing(text)) { + return(invisible(NULL)) + } + + normalised_location <- normalise_pos_or_range_arg(location) + normalised_text <- normalise_text_arg(text, length(normalised_location)) + + query <- mapply(function(location, text) { + list( + operation = if (rstudioapi::is.document_range(location)) "modifyRange" else "insertText", + location = serialize_location(location), + text = text + ) + }, normalised_location, normalised_text, SIMPLIFY = FALSE) + + invisible(request_client("rstudioapi/insert_or_modify_text", args = list(query = query, id = id))) +} + +modifyRange <- insertText + +readPreference <- function(name, default) default +readRStudioPreference <- readPreference + +.sess_rstudioapi_env <- environment() + +hasFun <- function(name, version_needed = NULL, ...) { + if (!is.null(version_needed)) { + if (!verifyAvailable(version_needed)) { + return(FALSE) + } + } + obj <- .sess_rstudioapi_env[[name]] + is.function(obj) && !identical(obj, .sess_not_yet_implemented) +} + +findFun <- function(name, version_needed = NULL, ...) { + if (!is.null(version_needed)) { + if (!verifyAvailable(version_needed)) { + stop("the generic IPC client does not support used of 'version_needed' > 0.") + } + } + if (hasFun(name, version_needed = version_needed, ...)) { + .sess_rstudioapi_env[[name]] + } else { + stop("Cannot find function '", name, "'") + } +} + +showDialog <- function(title, message, url = "") { + message <- sprintf("%s: %s \n%s", title, message, url) + invisible(request_client("rstudioapi/show_dialog", args = list(message = message))) +} + +navigateToFile <- function(file, line = 1L, column = 1L) { + invisible(request_client("rstudioapi/navigate_to_file", args = list( + file = normalizePath(file), + line = line, + column = column + ))) +} + +setSelectionRanges <- function(ranges, id = NULL) { + ranges_or_positions <- normalise_pos_or_range_arg(ranges) + ranges <- lapply(ranges_or_positions, function(location) { + if (rstudioapi::is.document_position(location)) { + rstudioapi::document_range(location, location) + } else { + location + } + }) + sess_ranges <- lapply(ranges, serialize_range) + invisible(request_client( + "rstudioapi/set_selection_ranges", + args = list(ranges = sess_ranges, id = id) + )) +} + +setCursorPosition <- setSelectionRanges + +documentSave <- function(id = NULL) { + invisible(request_client("rstudioapi/document_save", args = list(id = id))) +} + +getActiveProject <- function() { + path_object <- request_client("rstudioapi/get_project_path", args = list()) + path_object$path # Should be NULL if no project is open +} + +document_context <- function(id = NULL) { + editor_context <- request_client("rstudioapi/document_context", args = list(id = id)) + make_rs_document_context(editor_context) +} + +documentId <- function(allowConsole = TRUE) document_context()$id +documentPath <- function(id = NULL) document_context(id)$path + +documentSaveAll <- function() { + invisible(request_client("rstudioapi/document_save_all", args = list())) +} + +documentNew <- function(text = "", type = c("r", "rmarkdown", "sql"), + position = rstudioapi::document_position(1, 1), + execute = FALSE) { + if (!rstudioapi::is.document_position((position))) { + stop("DocumentNew requires a document_position object") + } + if (length(text) != 1 || !is.character(text)) { + stop("text for DocumentNew must be a length one character vector.") + } + invisible(request_client("rstudioapi/document_new", args = list( + text = text, + type = match.arg(type), + position = serialize_pos(position) + ))) +} + +setDocumentContents <- function(text, id = NULL) { + whole_document_range <- rstudioapi::document_range( + rstudioapi::document_position(1, 1), + rstudioapi::document_position(Inf, Inf) + ) + insertText(whole_document_range, text, id) +} + +restartSession <- function(command = "", clean = FALSE) { + invisible(notify_client("restart_r", params = list(command = command, clean = clean))) +} + +viewer <- function(url, height = NULL) { + notify_client("webview", list(url = url, title = "Viewer")) +} + +page_viewer <- function(url, title = NULL) { + notify_client( + "page_viewer", + list(url = url, title = if (is.null(title)) "Page Viewer" else title) + ) +} + +getVersion <- function() numeric_version("0") + +versionInfo <- function() { + list( + citation = "", mode = "generic-ipc", version = numeric_version("0"), + release_name = "generic-ipc" + ) +} + +sendToConsole <- function(code, echo = TRUE, execute = TRUE, focus = TRUE, animate = FALSE) { + if (!echo) { + warning("rstudioapi::sendToConsole echo = FALSE is not supported in the generic IPC client.") + } + code_to_run <- paste0(code, collapse = "\n") + invisible(notify_client("rstudioapi/send_to_console", params = list( + code = code_to_run, execute = execute, focus = focus, animate = animate + ))) +} + +documentClose <- function(id = NULL, save = TRUE) { + invisible(request_client("rstudioapi/document_close", args = list(id = id, save = save))) +} + +.sess_not_yet_implemented <- function(...) { + stop("This {rstudioapi} function is not currently implemented for generic IPC.") +} + +# Add missing helpers from rstudioapi_util +make_rs_range <- function(sess_selection) { + if (is.null(sess_selection$start$line) && !is.null(sess_selection[["start.line"]])) { + start_line <- sess_selection[["start.line"]] + start_character <- sess_selection[["start.character"]] + end_line <- sess_selection[["end.line"]] + end_character <- sess_selection[["end.character"]] + } else { + start_line <- sess_selection$start$line + start_character <- sess_selection$start$character + end_line <- sess_selection$end$line + end_character <- sess_selection$end$character + } + rstudioapi::document_range( + start = rstudioapi::document_position(row = start_line, column = start_character), + end = rstudioapi::document_position(row = end_line, column = end_character) + ) +} + +extract_document_ranges <- function(sess_selections) { + if (is.data.frame(sess_selections)) { + lapply(seq_len(nrow(sess_selections)), function(i) { + make_rs_range(as.list(sess_selections[i, , drop = FALSE])) + }) + } else { + lapply(sess_selections, make_rs_range) + } +} + +to_content_lines <- function(contents, ranges) { + content_lines <- strsplit(contents, "\n|\r\n|\r$")[[1]] + + if (length(ranges) == 0) { + return(content_lines) + } + + range_end_row <- unlist(lapply(ranges, function(range) range$end["row"])) + last_row <- max(range_end_row, na.rm = TRUE) + if (is.finite(last_row) && last_row == length(content_lines) + 1) { + content_lines <- c(content_lines, "") + } + + content_lines +} + +extract_range_text <- function(range, content_lines) { + if (!range_has_text(range)) { + return("") + } + start_row <- range$start["row"] + end_row <- range$end["row"] + if (start_row > length(content_lines)) { + return("") + } + + content_rows <- content_lines[start_row:min(end_row, length(content_lines))] + + # Adjust end + if (end_row <= length(content_lines)) { + content_rows[length(content_rows)] <- substring( + content_rows[length(content_rows)], 1, range$end["column"] - 1 + ) + } + + # Adjust start + content_rows[1] <- substring(content_rows[1], range$start["column"]) + + paste0(content_rows, collapse = "\n") +} + +range_has_text <- function(range) { + (range$end["row"] - range$start["row"]) + + (range$end["column"] - range$start["column"]) > 0 +} + +make_rs_document_selection <- function(ranges, range_texts) { + structure( + mapply( + function(range, text) { + list(range = range, text = text) + }, ranges, range_texts, + SIMPLIFY = FALSE + ), + class = "document_selection" + ) +} + +make_rs_document_context <- function(editor_context) { + document_ranges <- extract_document_ranges(editor_context$selection) + content_lines <- to_content_lines(editor_context$contents, document_ranges) + document_range_texts <- lapply(document_ranges, extract_range_text, content_lines) + document_selection <- make_rs_document_selection(document_ranges, document_range_texts) + structure(list( + id = editor_context$id$external, + path = editor_context$path, + contents = content_lines, + selection = document_selection + ), class = "document_context") +} + +is_positionable <- function(p) is.numeric(p) && length(p) == 2 +is_rangable <- function(r) is.numeric(r) && length(r) == 4 + +normalise_pos_or_range_arg <- function(location) { + if (rstudioapi::is.document_position(location)) { + list(location) + } else if (is_positionable(location)) { + list(rstudioapi::as.document_position(location)) + } else if (rstudioapi::is.document_range(location)) { + list(location) + } else if (is_rangable(location)) { + list(rstudioapi::as.document_range(location)) + } else if (is.list(location)) { + lapply(location, function(a_location) { + is_pos <- rstudioapi::is.document_position(a_location) + is_range <- rstudioapi::is.document_range(a_location) + if (is_pos || is_range) { + a_location + } else if (is_positionable(a_location)) { + rstudioapi::as.document_position(a_location) + } else if (is_rangable((a_location))) { + rstudioapi::as.document_range(a_location) + } else { + stop("object in location list was not a document_position or document_range") + } + }) + } else { + stop("location object was not a document_position or document_range") + } +} + +normalise_text_arg <- function(text, location_length) { + if (length(text) == location_length) { + text + } else if (length(text) == 1 && location_length > 1) { + rep(text, location_length) + } else { + stop("text vector needs to be of length 1 or the same length as location list") + } +} + +serialize_pos <- function(pos) { + as.numeric(c(pos[["row"]], pos[["column"]])) +} + +serialize_range <- function(range) { + list(start = serialize_pos(range$start), end = serialize_pos(range$end)) +} + +serialize_location <- function(location) { + if (rstudioapi::is.document_position(location)) { + serialize_pos(location) + } else if (rstudioapi::is.document_range(location)) { + serialize_range(location) + } else { + location + } +} + +namespace_has <- function(obj, namespace) { + attempt <- try(getFromNamespace(obj, namespace), silent = TRUE) + !inherits(attempt, "try-error") +} + +patch_rstudioapi <- function() { + overrides <- list( + getActiveDocumentContext = getActiveDocumentContext, + getSourceEditorContext = getSourceEditorContext, + insertText = insertText, + modifyRange = modifyRange, + showDialog = showDialog, + navigateToFile = navigateToFile, + setSelectionRanges = setSelectionRanges, + setCursorPosition = setCursorPosition, + documentSave = documentSave, + getActiveProject = getActiveProject, + documentId = documentId, + documentPath = documentPath, + documentSaveAll = documentSaveAll, + documentNew = documentNew, + setDocumentContents = setDocumentContents, + restartSession = restartSession, + viewer = viewer, + getVersion = getVersion, + versionInfo = versionInfo, + sendToConsole = sendToConsole, + documentClose = documentClose, + hasFun = hasFun, + findFun = findFun, + isAvailable = isAvailable, + verifyAvailable = verifyAvailable, + readPreference = readPreference, + readRStudioPreference = readRStudioPreference, + getConsoleEditorContext = .sess_not_yet_implemented, + sourceMarkers = .sess_not_yet_implemented, + showPrompt = function(title, message, default = NULL) { + response <- request_client( + "rstudioapi/show_prompt", + args = list(title = title, message = message, default = default) + ) + response$response + }, + askForPassword = function(prompt = "Please enter your password") { + response <- request_client("rstudioapi/ask_for_password", args = list(prompt = prompt)) + response$response + }, + showQuestion = .sess_not_yet_implemented, + updateDialog = .sess_not_yet_implemented, + openProject = .sess_not_yet_implemented, + initializeProject = .sess_not_yet_implemented, + addTheme = .sess_not_yet_implemented, + applyTheme = .sess_not_yet_implemented, + convertTheme = .sess_not_yet_implemented, + getThemeInfo = .sess_not_yet_implemented, + getThemes = .sess_not_yet_implemented, + removeTheme = .sess_not_yet_implemented, + jobAdd = .sess_not_yet_implemented, + jobAddOutput = .sess_not_yet_implemented, + jobAddProgress = .sess_not_yet_implemented, + jobRemove = .sess_not_yet_implemented, + jobRunScript = .sess_not_yet_implemented, + jobSetProgress = .sess_not_yet_implemented, + jobSetState = .sess_not_yet_implemented, + jobSetStatus = .sess_not_yet_implemented, + launcherGetInfo = .sess_not_yet_implemented, + launcherAvailable = .sess_not_yet_implemented, + launcherGetJobs = .sess_not_yet_implemented, + launcherConfig = .sess_not_yet_implemented, + launcherContainer = .sess_not_yet_implemented, + launcherControlJob = .sess_not_yet_implemented, + launcherGetJob = .sess_not_yet_implemented, + launcherHostMount = .sess_not_yet_implemented, + launcherNfsMount = .sess_not_yet_implemented, + launcherPlacementConstraint = .sess_not_yet_implemented, + launcherResourceLimit = .sess_not_yet_implemented, + launcherSubmitJob = .sess_not_yet_implemented, + launcherSubmitR = .sess_not_yet_implemented, + previewRd = .sess_not_yet_implemented, + previewSql = .sess_not_yet_implemented, + writePreference = .sess_not_yet_implemented, + writeRStudioPreference = .sess_not_yet_implemented, + getPersistentValue = .sess_not_yet_implemented, + setPersistentValue = .sess_not_yet_implemented, + savePlotAsImage = .sess_not_yet_implemented, + createProjectTemplate = .sess_not_yet_implemented, + hasColourConsole = .sess_not_yet_implemented, + bugReport = .sess_not_yet_implemented, + buildToolsCheck = .sess_not_yet_implemented, + buildToolsInstall = .sess_not_yet_implemented, + buildToolsExec = .sess_not_yet_implemented, + dictionariesPath = .sess_not_yet_implemented, + userDictionariesPath = .sess_not_yet_implemented, + executeCommand = .sess_not_yet_implemented, + translateLocalUrl = .sess_not_yet_implemented + ) + + for (name in names(overrides)) { + if (exists(name, envir = asNamespace("rstudioapi"), inherits = FALSE)) { + rebind(name, overrides[[name]], "rstudioapi") + } + } +} diff --git a/sess/R/server.R b/sess/R/server.R new file mode 100644 index 000000000..727ee37f0 --- /dev/null +++ b/sess/R/server.R @@ -0,0 +1,175 @@ +#' Start the client R IPC connection +#' +#' @param port Integer. The port of the VS Code WebSocket server. +#' If NULL, it will use SESS_PORT env var. +#' @param token String. The authentication token. If NULL, it will use SESS_TOKEN env var. +#' @param use_rstudioapi Logical. Should the rstudioapi emulation layer +#' be enabled? Defaults to TRUE. +#' @param use_httpgd Logical. Should httpgd be used for plotting if available? Defaults to TRUE +#' @export +sess_app <- function(port = NULL, token = NULL, use_rstudioapi = TRUE, use_httpgd = TRUE) { + # Initialize state + .sess_env$server <- NULL + .sess_env$ws <- NULL + .sess_env$pending_responses <- list() + + # Specific tempdir for vscode-R + .sess_env$tempdir <- file.path(tempdir(), "sess") + dir.create(.sess_env$tempdir, showWarnings = FALSE, recursive = TRUE) + + # Temporary file for static plot serving + .sess_env$latest_plot_path <- file.path(.sess_env$tempdir, "sess_plot.png") + + is_manual <- (!is.null(port) && !is.na(port) && nzchar(port)) || + (!is.null(token) && !is.na(token) && nzchar(token)) + + if (is.null(port) || is.na(port)) { + port <- Sys.getenv("SESS_PORT") + } + if (is.null(token) || is.na(token) || !nzchar(token)) { + token <- Sys.getenv("SESS_TOKEN") + } + + # Derive file path for fallback/reconnection + pid <- Sys.getpid() + home <- path.expand("~") + file_path <- file.path(home, ".vscode-R", "sessions", sprintf("%d.json", pid)) + + if (!nzchar(port) || !nzchar(token)) { + if (file.exists(file_path)) { + tryCatch({ + config <- jsonlite::fromJSON(readLines(file_path, warn = FALSE)) + port <- config$port + token <- config$token + }, error = function(e) NULL) + } + } + + if (!nzchar(port) || !nzchar(token)) { + warning("[sess] Connection info not available. Cannot connect to VS Code.") + return(invisible(NULL)) + } + + print_async_msg <- function(msg) { + prompt <- if (interactive()) getOption("prompt") else "" + cat(sprintf("\r%s\n\n%s", msg, prompt)) + } + + connect <- function() { + url <- sprintf("ws://127.0.0.1:%s/?token=%s", port, token) + ws <- websocket::WebSocket$new( + url, + autoConnect = FALSE, + accessLogChannels = "none", + errorLogChannels = "none" + ) + + ws$onOpen(function(event) { + .sess_env$ws <- ws + print_async_msg("[sess] Connected to VS Code") + + # Send the attach handshake immediately upon connection + notify_client("attach", list( + version = sprintf("%s.%s", R.version$major, R.version$minor), + pid = Sys.getpid(), + tempdir = .sess_env$tempdir, + wd = getwd(), + info = list( + command = commandArgs()[[1L]], + version = R.version.string, + start_time = format(Sys.time()) + ) + )) + }) + + ws$onMessage(function(event) { + # Handle JSON-RPC 2.0 messages COMING FROM the client + payload <- tryCatch(jsonlite::fromJSON(event$data), error = function(e) NULL) + + if (!is.null(payload) && !is.null(payload$id)) { + if (!is.null(payload$method)) { + # It's a Request from the Client (e.g., 'workspace', 'plot_latest') + handlers <- list( + "workspace" = function(p) get_workspace_data(), + "hover" = function(p) handle_hover(p$expr), + "completion" = function(p) handle_complete(p$expr, p$trigger), + "plot_latest" = function(p) handle_plot_latest(p) + ) + + if (payload$method %in% names(handlers)) { + res <- tryCatch( + { + handlers[[payload$method]](payload$params) + }, + error = function(e) { + warning(sprintf( + "[sess] Error in handler for '%s': %s", + payload$method, e$message + )) + NULL + } + ) + + succ_resp <- list( + jsonrpc = "2.0", + id = payload$id, + result = res + ) + ws$send(jsonlite::toJSON(succ_resp, auto_unbox = TRUE, null = "null", force = TRUE)) + } else { + err_resp <- list( + jsonrpc = "2.0", + id = payload$id, + error = list(code = -32601, message = "Method not found") + ) + ws$send(jsonlite::toJSON(err_resp, auto_unbox = TRUE, null = "null", force = TRUE)) + } + } else { + # It's a Response (to our RStudio API request) + if (!is.null(payload$result)) { + .sess_env$pending_responses[[as.character(payload$id)]] <- + payload$result + } else if (!is.null(payload$error)) { + .sess_env$pending_responses[[as.character(payload$id)]] <- + structure(payload$error, class = "json_rpc_error") + } + } + } + }) + + ws$onClose(function(event) { + .sess_env$ws <- NULL + if (is_manual) { + print_async_msg("[sess] Disconnected from VS Code.") + return() + } + print_async_msg("[sess] Disconnected from VS Code. Retrying in 5 seconds...") + later::later(function() { + if (file.exists(file_path)) { + tryCatch({ + config <- jsonlite::fromJSON(readLines(file_path, warn = FALSE)) + port <<- config$port + token <<- config$token + }, error = function(e) NULL) + } + connect() + }, 5) + }) + + ws$onError(function(event) { + print_async_msg(sprintf("[sess] WebSocket error: %s", event$message)) + }) + + ws$connect() + } + + # Connect to VS Code + connect() + + # Register runtime hooks + if (is.na(use_rstudioapi)) use_rstudioapi <- TRUE + if (is.na(use_httpgd)) use_httpgd <- TRUE + register_hooks(use_rstudioapi = use_rstudioapi, use_httpgd = use_httpgd) + + invisible(NULL) +} diff --git a/sess/R/utils.R b/sess/R/utils.R new file mode 100644 index 000000000..bbe7b6f20 --- /dev/null +++ b/sess/R/utils.R @@ -0,0 +1,48 @@ +# Helper to format JSON-RPC 2.0 Responses +json_rpc_response <- function(id, result) { + list( + status = 200L, + headers = list("Content-Type" = "application/json"), + body = jsonlite::toJSON(list( + jsonrpc = "2.0", + id = id, + result = result + ), auto_unbox = TRUE, null = "null", force = TRUE) + ) +} + +# Helper to format JSON-RPC 2.0 Errors +json_rpc_error <- function(id, code, message, data = NULL) { + list( + status = 200L, # JSON-RPC typically returns 200 even for errors + headers = list("Content-Type" = "application/json"), + body = jsonlite::toJSON(list( + jsonrpc = "2.0", + id = id, + error = list( + code = code, + message = message, + data = data + ) + ), auto_unbox = TRUE, null = "null", force = TRUE) + ) +} + +# Helper to safely hijack and override R internal functions +rebind <- function(sym, value, ns) { + if (is.character(ns)) { + Recall(sym, value, getNamespace(ns)) + pkg <- paste0("package:", ns) + if (pkg %in% search()) { + Recall(sym, value, as.environment(pkg)) + } + } else if (is.environment(ns)) { + if (bindingIsLocked(sym, ns)) { + unlockBinding(sym, ns) + on.exit(lockBinding(sym, ns)) + } + assign(sym, value, ns) + } else { + stop("ns must be a string or environment") + } +} diff --git a/sess/R/zzz.R b/sess/R/zzz.R new file mode 100644 index 000000000..9e70877a2 --- /dev/null +++ b/sess/R/zzz.R @@ -0,0 +1 @@ +.sess_env <- new.env(parent = emptyenv()) diff --git a/sess/README.md b/sess/README.md new file mode 100644 index 000000000..145ebaf0b --- /dev/null +++ b/sess/README.md @@ -0,0 +1,188 @@ +# `sess`: Modern R IPC Server Protocol + +The `sess` package provides a high-performance, token-authenticated IPC (Inter-Process Communication) mechanism between R and a client (such as an IDE or editor extension). It uses a pure **WebSocket** architecture to replace legacy file-based watchers. + +## 1. Connection Handshake + +### Starting the Server + +The server can be started by calling `sess::sess_app()`: + +```r +sess::sess_app( + port = NULL, # Integer: Server port (random if NULL) + token = NULL, # String: Authentication token (random if NULL) + use_rstudioapi = TRUE, # Logical: Enable RStudio API emulation + use_httpgd = TRUE # Logical: Use httpgd for plotting if available +) +``` + +It prints a connection string to the R console: + +```text +[sess] Server address: ws://127.0.0.1:PORT?token=TOKEN +``` + +## 2. Communication Channels + +`sess` uses a pure WebSocket architecture following the **JSON-RPC 2.0** specification for all structured data exchange. The WebSocket connection serves three main purposes: pushing instantaneous events from R to the client via notifications, allowing R to synchronously call client-side methods, and allowing the client to query R state via asynchronous requests. + +### 1. Client Notifications (`notify_client`) + +The WebSocket is used for instantaneous events pushed from R to the client as **JSON-RPC Notifications** (no `id`). + +**Notification Format:** + +```json +{ + "jsonrpc": "2.0", + "method": "method_name", + "params": { ... } +} +``` + +The following methods are sent as notifications from R to the client: + +- **`attach`**: Sent immediately upon connection. Includes PID, R version, and session metadata. +- **`detach`**: Sent when the R session is shutting down (params: `pid`). +- **`dataview`**: Triggered by `View()`. Params include a temporary JSON file path containing the data. +- **`plot_updated`**: Notifies that a new static plot is available. The client should request the `plot_latest` method. +- **`httpgd`**: Provides a URL for an `httpgd` live plot server (params: `url`). +- **`help`**: Requests the client to display an R help page (params: `requestPath`). +- **`browser`**: Requests the client to open a URL (params: `url`, `title`, `viewer`). +- **`webview`**: Requests the client to open a local HTML file or URL in a webview (params: `file`, `title`, `viewer`). +- **`restart_r`**: Requests the client to restart the R session (params: `command`, `clean`). +- **`send_to_console`**: Sends code to the console for execution without blocking the R session (params: `code`, `execute`, `focus`, `animate`). + +### 2. Synchronous Client Requests (`request_client`) + +The `request_client()` function allows R to call client-side functions synchronously by sending a **JSON-RPC Request** (with an `id`) over the WebSocket. This is primarily used to emulate the RStudio API. + +**Coordinate Handling**: The `sess` protocol uses **1-indexed** coordinates for all rows (lines) and columns (characters) on the wire. This aligns with R's internal representation. The client (e.g., VS Code extension) is responsible for converting these to its internal 0-indexed representation if necessary. + +**Serialization Format**: + +- **Position**: A numeric array `[row, column]`. +- **Range**: An object `{ "start": [row, column], "end": [row, column] }`. + +Below are the JSON-RPC methods sent from R to the client to emulate RStudio API functionality: + +- **`rstudioapi/active_editor_context`**: Requests the current context of the active editor. +- **`rstudioapi/replace_text_in_current_selection`**: Replaces text in the current selection (params: `text`, `id`). +- **`rstudioapi/insert_or_modify_text`**: Inserts or modifies text at specific locations (params: `query`, `id`). +- **`rstudioapi/show_dialog`**: Displays a message dialog to the user (params: `message`). +- **`rstudioapi/navigate_to_file`**: Opens and navigates to a specific file, line, and column (params: `file`, `line`, `column`). +- **`rstudioapi/set_selection_ranges`**: Sets the cursor or selection ranges in the editor (params: `ranges`, `id`). +- **`rstudioapi/document_save`**: Saves the specified document (params: `id`). +- **`rstudioapi/get_project_path`**: Retrieves the current project path. +- **`rstudioapi/document_context`**: Retrieves the context of a specific document (params: `id`). +- **`rstudioapi/document_save_all`**: Saves all open documents. +- **`rstudioapi/document_new`**: Creates a new document with specified text and type (params: `text`, `type`, `position`). +- **`rstudioapi/document_close`**: Closes the specified document (params: `id`, `save`). + +### 3. Server Requests (Pull API) + +The Pull API allows the client to query state using **JSON-RPC Requests** sent over the active WebSocket. + +#### JSON-RPC Request Format + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "method_name", + "params": { ... } +} +``` + +#### Available Methods + +**`workspace`** +Returns object metadata from the Global Environment. + +- **Request Params**: None. +- **Response Result**: + + ```json + { + "globalenv": { + "my_df": { "class": "data.frame", "type": "list", "length": 5, "str": "data.frame: 32 obs. of 11 variables:" } + }, + "search": ["package:stats", "package:graphics"], + "loaded_namespaces": ["sess", "httpuv"] + } + ``` + +**`plot_latest`** +Returns the most recent static plot captured by the R session. + +- **Request Params**: None. +- **Response Result**: `{"data": "iVBORw0KGgoAAAANSUhEUgA..."}` (base64 encoded PNG string). Returns `{"data": null}` if no plot exists. + +**`hover`** + +- **Request Params**: `{"expr": "head(mtcars)"}` +- **Response Result**: `{"str": " 'data.frame': 6 obs. of 11 variables: ..."}` + +**`completion`** + +- **Request Params**: `{"expr": "mtcars", "trigger": "$"}` +- **Response Result**: + + ```json + [ + { "name": "mpg", "type": "double", "str": "numeric" }, + { "name": "cyl", "type": "double", "str": "numeric" } + ] + ``` + +--- + +## 3. Hook Registration & Options + +By default, the package does not inject hooks into the R session on load. Calling `sess::sess_app()` will start the server and automatically call `sess::register_hooks()` to enable features like automatic `View()` interception or plot redirection. + +### Intercepted Functions + +- **`utils::View()`**: Redirects data to the client's data viewer. Supports `data.frame`, `matrix`, `list`, and `ArrowTabular` objects. +- **`browser()`**, **`viewer()`**, **`page_viewer()`**: Redirects URLs and HTML files to the client's browser or webview. +- **Help System**: Intercepts help topic printing to route HTML help to the client. + +### Global Options + +- **`sess.row_limit`**: Limits the number of rows sent to the data viewer (default: 100). Set to 0 for no limit. +- **`sess.dataview`**: Target viewer column for data (default: `"Two"`). +- **`sess.browser`**: Target viewer for browser (default: `"Active"`). +- **`sess.webview`**: Target viewer for webview (default: `"Two"`). +- **`sess.helpPanel`**: Target viewer for help (default: `"Two"`). + +## 4. Comparison with Legacy IPC + +The `sess` package replaces the legacy file-based IPC mechanism with a modern, in-memory WebSocket architecture using **JSON-RPC 2.0**. + +| Feature | Legacy IPC (File-based) | Modern IPC (`sess`) | +| :--- | :--- | :--- | +| **Command Dispatch** | `request.log` + `request.lock` | **WS Notification** (JSON-RPC) | +| **Workspace State** | `workspace.json` + `workspace.lock` | **WS Request `workspace`** (On-demand) | +| **Static Plots** | `plot.png` + `plot.lock` | **WS Notification** + **WS Request `plot_latest`** | +| **RStudio API (Sync)**| `request.log` + `response.lock` | **WS Request** (JSON-RPC) | +| **Client Queries** | Internal HTTP Server (`httpuv`) | **WS Request** (JSON-RPC 2.0) | +| **Transport Reliability**| OS-level File System Watchers | **WebSocket** | +| **Protocol Standard** | Ad-hoc JSON formats | **JSON-RPC 2.0** | + +### Architectural Shifts + +1. **Elimination of File Watchers**: Replaces unreliable OS-level file system watchers with persistent WebSocket connections for instantaneous event pushing. +2. **On-Demand Evaluation**: Evaluations of the Global Environment are now performed only when requested by the client, reducing R's background workload. +3. **Unified Standard**: Unifies all structured communication under the **JSON-RPC 2.0** standard across a single WebSocket connection. + +### Connection Discovery & Reconnection + +While `sess` primarily uses WebSockets for low-latency communication, it uses a file-based +discovery mechanism to handle VS Code window reloads and support manual connections: + +1. **Initial Connection**: VS Code passes `SESS_PORT` and `SESS_TOKEN` environment variables when launching R. R connects directly. +2. **Discovery File**: VS Code writes the current connection info to `~/.vscode-R/sessions/{PID}.json`. +3. **Reconnection**: If disconnected (e.g., after a window reload), R retries connecting by + reading the file to find the new port and token. Automatic reconnection is skipped + for manual connections (where port/token were passed as arguments). diff --git a/sess/man/notify_client.Rd b/sess/man/notify_client.Rd new file mode 100644 index 000000000..1545127ed --- /dev/null +++ b/sess/man/notify_client.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dispatch.R +\name{notify_client} +\alias{notify_client} +\title{Notify the client via WebSocket (JSON-RPC 2.0 Notification)} +\usage{ +notify_client(method, params = list()) +} +\arguments{ +\item{method}{A string representing the action (e.g., "dataview", "plot_updated")} + +\item{params}{A list containing the arguments for the command} +} +\description{ +Pushes an event instantly to the client extension via the active WebSocket connection. +} diff --git a/sess/man/register_hooks.Rd b/sess/man/register_hooks.Rd new file mode 100644 index 000000000..3736802b4 --- /dev/null +++ b/sess/man/register_hooks.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/hooks.R +\name{register_hooks} +\alias{register_hooks} +\title{Register hooks for the client IPC} +\usage{ +register_hooks(use_rstudioapi = TRUE, use_httpgd = TRUE) +} +\arguments{ +\item{use_rstudioapi}{Logical. Enable rstudioapi emulation.} + +\item{use_httpgd}{Logical. Enable httpgd plot device if available.} +} +\description{ +Register hooks for the client IPC +} diff --git a/sess/man/request_client.Rd b/sess/man/request_client.Rd new file mode 100644 index 000000000..e311b9e57 --- /dev/null +++ b/sess/man/request_client.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dispatch.R +\name{request_client} +\alias{request_client} +\title{Emulate rstudioapi (or any client action) synchronously but without blocking the R Event Loop} +\usage{ +request_client(action, args = list()) +} +\arguments{ +\item{action}{String of the action name} + +\item{args}{List of arguments} +} +\description{ +Emulate rstudioapi (or any client action) synchronously but without blocking the R Event Loop +} diff --git a/sess/man/rpc_send.Rd b/sess/man/rpc_send.Rd new file mode 100644 index 000000000..7404dcfd1 --- /dev/null +++ b/sess/man/rpc_send.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dispatch.R +\name{rpc_send} +\alias{rpc_send} +\title{Send a message to the client via WebSocket (JSON-RPC 2.0)} +\usage{ +rpc_send(method, params = list(), request = FALSE) +} +\arguments{ +\item{method}{String. The JSON-RPC method.} + +\item{params}{List. The parameters for the method.} + +\item{request}{Logical. If TRUE, sends a Request and waits for a Response.} +} +\value{ +The result of the request if request=TRUE, otherwise TRUE if sent. +} +\description{ +This is the internal workhorse for both Notifications and Requests. +} +\keyword{internal} diff --git a/sess/man/sess_app.Rd b/sess/man/sess_app.Rd new file mode 100644 index 000000000..59967e1cc --- /dev/null +++ b/sess/man/sess_app.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/server.R +\name{sess_app} +\alias{sess_app} +\title{Start the client R IPC connection} +\usage{ +sess_app(port = NULL, token = NULL, use_rstudioapi = TRUE, use_httpgd = TRUE) +} +\arguments{ +\item{port}{Integer. The port of the VS Code WebSocket server. If NULL, it will use SESS_PORT env var.} + +\item{token}{String. The authentication token. If NULL, it will use SESS_TOKEN env var.} + +\item{use_rstudioapi}{Logical. Should the rstudioapi emulation layer be enabled? Defaults to TRUE.} + +\item{use_httpgd}{Logical. Should httpgd be used for plotting if available? Defaults to TRUE} +} +\description{ +Start the client R IPC connection +} diff --git a/src/completions.ts b/src/completions.ts index 17546f42e..1af7882b1 100644 --- a/src/completions.ts +++ b/src/completions.ts @@ -1,10 +1,4 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ - - +'use strict'; import * as vscode from 'vscode'; import * as session from './session'; @@ -52,9 +46,9 @@ export class HoverProvider implements vscode.HoverProvider { hoverRange = document.getWordRangeAtPosition(position, exprRegex)?.with({ end: hoverRange?.end }); const expr = document.getText(hoverRange); const response = await session.sessionRequest(session.server, { - type: 'hover', - expr: expr - }); + method: 'hover', + params: { expr: expr } + }) as { str: string }; if (response) { hoverText = response.str; @@ -62,7 +56,7 @@ export class HoverProvider implements vscode.HoverProvider { } else { const symbol = document.getText(hoverRange); - const str = session.workspaceData.globalenv[symbol]?.str; + const str = session.activeSession?.workspaceData.globalenv[symbol]?.str; if (str) { hoverText = str; @@ -101,7 +95,7 @@ export class HelpLinkHoverProvider implements vscode.HoverProvider { const encodedArgs = encodeURIComponent(JSON.stringify(args)); const cmd = 'command:r.helpPanel.openForPath'; const cmdUri = vscode.Uri.parse(`${cmd}?${encodedArgs}`); - return `[\`${cmdText}\`](${cmdUri})`; + return `[\`${cmdText}\`](${cmdUri.toString()})`; }); const md = new vscode.MarkdownString(mds.join(' \n')); md.isTrusted = true; @@ -137,7 +131,8 @@ export class LiveCompletionItemProvider implements vscode.CompletionItemProvider completionContext: vscode.CompletionContext ): Promise { const items: vscode.CompletionItem[] = []; - if (token.isCancellationRequested || !session.workspaceData?.globalenv) { + const activeSession = session.activeSession; + if (token.isCancellationRequested || !activeSession?.workspaceData?.globalenv) { return items; } @@ -152,8 +147,9 @@ export class LiveCompletionItemProvider implements vscode.CompletionItemProvider const trigger = completionContext.triggerCharacter; if (trigger === undefined) { - Object.keys(session.workspaceData.globalenv).forEach((key) => { - const obj = session.workspaceData.globalenv[key]; + const globalenv: session.GlobalEnv = activeSession.workspaceData.globalenv; + Object.keys(globalenv).forEach((key) => { + const obj = globalenv[key]; const item = new vscode.CompletionItem( key, obj.type === 'closure' || obj.type === 'builtin' @@ -170,11 +166,10 @@ export class LiveCompletionItemProvider implements vscode.CompletionItemProvider const re = /([a-zA-Z0-9._$@ ])+(? document.lineAt(x).text, document.lineCount); let symbol: string | undefined = undefined; @@ -322,7 +317,7 @@ function getPipelineCompletionItems(document: vscode.TextDocument, position: vsc } if (!token.isCancellationRequested && symbol !== undefined) { - const obj = session.workspaceData.globalenv[symbol]; + const obj = activeSession.workspaceData.globalenv[symbol]; if (obj !== undefined && obj.names !== undefined) { const doc = new vscode.MarkdownString('Element of `' + symbol + '`'); items.push(...getCompletionItems(obj.names, vscode.CompletionItemKind.Variable, '[session]', doc)); diff --git a/src/extension.ts b/src/extension.ts index 67a07ea07..1c3de2013 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,4 +1,3 @@ - 'use strict'; // interfaces, functions, etc. provided by vscode @@ -20,8 +19,8 @@ import * as workspaceViewer from './workspaceViewer'; import * as apiImplementation from './apiImplementation'; import * as rHelp from './helpViewer'; import * as completions from './completions'; -import * as rShare from './liveShare'; -import * as httpgdViewer from './plotViewer'; +import * as plotViewer from './plotViewer'; +import { PlotManager } from './plotViewer/types'; import * as languageService from './languageService'; import { RTaskProvider } from './tasks'; @@ -33,7 +32,7 @@ export let rWorkspace: workspaceViewer.WorkspaceDataProvider | undefined = undef export let globalRHelp: rHelp.RHelp | undefined = undefined; export let extensionContext: vscode.ExtensionContext; export let enableSessionWatcher: boolean | undefined = undefined; -export let globalHttpgdManager: httpgdViewer.HttpgdManager | undefined = undefined; +export let globalPlotManager: PlotManager | undefined = undefined; export let rmdPreviewManager: rmarkdown.RMarkdownPreviewManager | undefined = undefined; export let rmdKnitManager: rmarkdown.RMarkdownKnitManager | undefined = undefined; export let sessionStatusBarItem: vscode.StatusBarItem | undefined = undefined; @@ -133,7 +132,8 @@ export async function activate(context: vscode.ExtensionContext): Promise('lsp.enabled')) { @@ -199,8 +200,8 @@ export async function activate(context: vscode.ExtensionContext): Promise + + + + + <%= packageTitle %> + + + + + + +
+

+ <%= packageTitle %> +

+
+

Documentation for package ‘<%= packageName %> ’ version <%= packageVersion %> +

+ + + +

Help Pages

+ + + <% topics.forEach((topic)=> { %> + + + + + <% }) %> +
<%= topic.name %><%= topic.title %>
+
+ + + \ No newline at end of file diff --git a/src/helpViewer/webview/index.ts b/src/helpViewer/webview/index.ts new file mode 100644 index 000000000..1e2ab9e41 --- /dev/null +++ b/src/helpViewer/webview/index.ts @@ -0,0 +1,97 @@ +import { acquireVsCodeApi, VsCode } from '../webviewMessages'; + +const vscode: VsCode = acquireVsCodeApi(); + +// notify vscode when mouse buttons are clicked +// used to implement back/forward on mouse buttons 3/4 +window.onmousedown = (ev) => { + vscode.postMessage({ + message: 'mouseClick', + button: Number(ev.button), + scrollY: window.scrollY + }); +}; + + +// handle requests from vscode ui +window.addEventListener('message', (ev: MessageEvent) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const message = ev.data; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if(message.command === 'getScrollY'){ + vscode.postMessage({ + message: 'getScrollY', + scrollY: window.scrollY + }); + } +}); + + +// do everything after loading the body +window.document.body.onload = () => { + + // make relative path for hyperlinks + const relPath = (document.body.getAttribute('relPath') || ''); + + // notify vscode, used to restore help panels between sessions + vscode.setState(relPath); + + const loc = document.location; + const url0 = new URL(loc.protocol + '//' + loc.host); + const url1 = new URL(relPath, url0); + + // scroll to desired position: + const scrollYTo = Number(document.body.getAttribute('scrollYTo') ?? -1); + if(scrollYTo >= 0){ + window.scrollTo(0,scrollYTo); + } else if(url1.hash){ + document.location.hash = url1.hash; + } + + // notify vscode when links are clicked: + const hyperLinks = document.getElementsByTagName('a'); + + for(let i=0; i { + document.location.hash = hrefRel; + }; + } else if(hrefAbs && hrefAbs.startsWith('vscode-webview://')){ + hyperLinks[i].onclick = () => { + + const url2 = new URL(hrefRel, url1); + const finalHref = url2.toString(); + + vscode.postMessage({ + message: 'linkClicked', + href: finalHref, + scrollY: window.scrollY + }); + }; + } + } + + // notify vscode when code is clicked: + if(document.body.classList.contains('preClickable')){ + const codeElements = document.getElementsByTagName('pre'); + for(let i=0; i { + vscode.postMessage({ + message: 'codeClicked', + code: el.textContent || '', + modifiers: { + altKey: me.altKey, + ctrlKey: me.ctrlKey, + shiftKey: me.shiftKey, + metaKey: me.metaKey, + } + }); + }; + } + } +}; + diff --git a/src/helpViewer/webview/theme.css b/src/helpViewer/webview/theme.css new file mode 100644 index 000000000..8876556a5 --- /dev/null +++ b/src/helpViewer/webview/theme.css @@ -0,0 +1,120 @@ + +/* General styling */ +body { + font-size: var(--vscode-editor-font-size); +} + +body table:nth-child(1)[width="100%"] td:nth-child(2){ + display: none; +} + +body table:nth-child(1)[width="100%"] td:nth-child(1){ + text-align: right; +} + +h1, h2 { + text-align: center; + margin-block-end: 0; +} + +img { + display: none; +} + +a ~ div.header { + display: none; +} + +/* Styling for preview info box */ +.previewInfo { + position: relative; + left: -20px; + width: calc(100% + 40px); + background-color: var(--vscode-list-inactiveSelectionBackground); + box-sizing: border-box; + text-align: center; + + padding-top: 0.5em; + padding-bottom: 0.5em; + padding-left: calc(0.5em + 20px); + padding-right: calc(0.5em + 20px); + /* margin-top: 1.5em; */ + margin-bottom: 1em; + + font-family: var(--vscode-editor-font-family); + font-size: var(--vscode-editor-font-size); +} + +/* Styling for clickable code sections */ +pre, code { + font-family: var(--vscode-editor-font-family); + font-size: var(--vscode-editor-font-size); +} + +.preClickable pre { + margin: 0px; + padding-top: 0.5em; + padding-bottom: 0.5em; +} + +.preHoverPointer .preDiv:hover { + cursor: pointer; +} +.preClickable .preCodeExample:hover { + background-color: var(--vscode-list-hoverBackground); +} + +/* Syntax highlighting in code sections */ +.hljs-link { + color: var(--vscode-textLink-foreground) +} + +.vscode-light { + --rhelp-number: #cb4b16; + --rhelp-string: #647400; + --rhelp-symbol: #cb4b16; + --rhelp-keyword: #2074b1; + --rhelp-comment: #727e7e; + --rhelp-function: #2074b1; +} + +.vscode-dark, +.vscode-high-contrast { + --rhelp-number: #b5cea8; + --rhelp-string: #CE9178; + --rhelp-symbol: #4EC9B0; + --rhelp-keyword: #569cd6; + --rhelp-comment: #6A9955; + --rhelp-function: #DCDCAA; +} + +.hljs-number { + color: var(--rhelp-number) +} + +.hljs-regexp, +.hljs-bullet, +.hljs-string { + color: var(--rhelp-string) +} + +.hljs-symbol, +.hljs-class { + color: var(--rhelp-symbol) +} + +.hljs-literal, +.hljs-keyword { + color: var(--rhelp-keyword) +} + +.hljs-built_in, +.hljs-function { + color: var(--rhelp-function) +} + +.hljs-quote, +.hljs-comment { + color: var(--rhelp-comment) +} + diff --git a/src/helpViewer/webviewMessages.ts b/src/helpViewer/webviewMessages.ts new file mode 100644 index 000000000..4fc1c8176 --- /dev/null +++ b/src/helpViewer/webviewMessages.ts @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export interface VsCode { + postMessage: (msg: OutMessage) => void; + setState: (state: string) => void; +} +/** + * Function declared by VS Code in Webview + */ +export const acquireVsCodeApi: () => VsCode = (globalThis as { acquireVsCodeApi?: () => VsCode }).acquireVsCodeApi || (() => ({} as VsCode)); + +export interface IMessage { + message: string; +} + +export interface LogMessage extends IMessage { + message: 'log', + body: any +} +export interface MouseClickMessage extends IMessage { + message: 'mouseClick', + button: number, + scrollY: number +} +export interface LinkClickedMessage extends IMessage { + message: 'linkClicked', + href: string, + scrollY: number +} +export interface CodeClickedMessage extends IMessage { + message: 'codeClicked', + code: string, + modifiers: { + altKey: boolean, + ctrlKey: boolean, + shiftKey: boolean, + metaKey: boolean, + } +} +export interface GetScrollYMessage extends IMessage { + message: 'getScrollY', + scrollY: number +} + +export type OutMessage = LogMessage | MouseClickMessage | LinkClickedMessage | CodeClickedMessage | GetScrollYMessage; diff --git a/src/languageService.ts b/src/languageService.ts index 73a9bec62..18a99b6e2 100644 --- a/src/languageService.ts +++ b/src/languageService.ts @@ -1,3 +1,5 @@ +'use strict'; + import * as os from 'os'; import { dirname } from 'path'; import * as net from 'net'; @@ -46,7 +48,9 @@ export class LanguageService implements Disposable { client.outputChannel.show(); } } - void client.stop(); + if (client.needsStop()) { + void client.stop(); + } }); return childProcess; } diff --git a/src/liveShare/index.ts b/src/liveShare/index.ts deleted file mode 100644 index e18305693..000000000 --- a/src/liveShare/index.ts +++ /dev/null @@ -1,354 +0,0 @@ -// re-exported variables -export * from './shareCommands'; -export * from './shareSession'; -export * from './shareTree'; -export * from './virtualDocs'; - -import * as vscode from 'vscode'; -import * as vsls from 'vsls'; -import * as fs from 'fs-extra'; - -import { enableSessionWatcher, extensionContext } from '../extension'; -import { attachActiveGuest, browserDisposables, initGuest } from './shareSession'; -import { initTreeView, rLiveShareProvider, shareWorkspace, ToggleNode } from './shareTree'; -import { Commands, Callback, liveShareOnRequest, liveShareRequest } from './shareCommands'; - -import { HelpFile } from '../helpViewer'; -import { WorkspaceData, workspaceData } from '../session'; -import { config } from '../util'; - -/// LiveShare -export let rHostService: HostService | undefined = undefined; -export let rGuestService: GuestService | undefined = undefined; -export let liveSession: vsls.LiveShare; -export let isGuestSession: boolean; -export let _sessionStatusBarItem: vscode.StatusBarItem; - -// service vars -export const ShareProviderName = 'vscode-r'; -export let service: vsls.SharedServiceProxy | vsls.SharedService | null = null; - -// random number to fake a UUID for differentiating between -// host calls and guest calls (specifically for the workspace -// viewer 'View' function) -export const UUID = Math.floor(Math.random() * Date.now()); - -/// state-tracking bools -// Bool to check if live share is loaded and active -export function isLiveShare(): boolean { - const shareStarted = liveSession?.session?.id; - // If there is a hosted session*, return true - // else return false - // * using vsls.getApi() instead of vsls.getApi().session.id - // * will always return true, even if a session is not active - // * (a session id will only exist if a session is active) - return !!shareStarted; -} - -export function isGuest(): boolean { - if (isLiveShare()) { - return liveSession.session.role === vsls.Role.Guest; - } else { - return false; - } -} - -export function isHost(): boolean { - if (isLiveShare()) { - return liveSession.session.role === vsls.Role.Host; - } else { - return false; - } -} - -// Initialises the Liveshare functionality for host & guest -// * session watcher is required * -export async function initLiveShare(context: vscode.ExtensionContext): Promise { - if (enableSessionWatcher) { - await LiveSessionListener(); - isGuestSession = isGuest(); - if (!isGuestSession) { - // Construct tree view for host - initTreeView(); - } else { - // Construct guest session watcher - initGuest(context); - } - - // Set context value for hiding buttons for guests - void vscode.commands.executeCommand('setContext', 'r.liveShare:isGuest', isGuestSession); - - // push commands - if (!isGuestSession) { - context.subscriptions.push( - vscode.commands.registerCommand( - 'r.liveShare.toggle', (node: ToggleNode) => node.toggle(rLiveShareProvider) - ), - vscode.commands.registerCommand( - 'r.liveShare.retry', async () => { - await LiveSessionListener(); - rLiveShareProvider.refresh(); - } - ) - ); - } else { - context.subscriptions.push( - vscode.commands.registerCommand('r.attachActiveGuest', () => attachActiveGuest()) - ); - } - } -} - -// Listens for the activation of a LiveShare session -export async function LiveSessionListener(): Promise { - rHostService = new HostService; - rGuestService = new GuestService; - - // catch errors in case of issues with the - // LiveShare extension/API (see #671) - async function tryAPI(): Promise { - try { - return await Promise.race([ - vsls.getApi(), - new Promise((res) => setTimeout(() => res(null), config().get('liveShare.timeout'))) - ]); - } catch(e: unknown) { - console.log('[LiveSessionListener] an error occured when attempting to access the Live Share API.', e); - return null; - } - } - - // Return out when the vsls extension isn't - // installed/available - const liveSessionStatus = await tryAPI(); - - void vscode.commands.executeCommand('setContext', 'r.liveShare:aborted', !liveSessionStatus); - - if (!liveSessionStatus) { - console.log('[LiveSessionListener] aborted'); - return; - } - - liveSession = liveSessionStatus as vsls.LiveShare; - console.log('[LiveSessionListener] started'); - - // When the session state changes, attempt to - // start a liveSession service, which is responsible - // for providing session-watcher functionality - // to guest sessions - liveSession.onDidChangeSession(async (e: vsls.SessionChangeEvent) => { - switch (e.session.role) { - case vsls.Role.None: - console.log('[LiveSessionListener] end event'); - await sessionCleanup(); - break; - case vsls.Role.Guest: - console.log('[LiveSessionListener] guest event'); - await rGuestService?.startService(); - break; - case vsls.Role.Host: - console.log('[LiveSessionListener] host event'); - await rHostService?.startService(); - rLiveShareProvider.refresh(); - break; - default: - console.log('[LiveSessionListener] default case'); - break; - } - }, null, extensionContext.subscriptions); - - // onDidChangeSession seems to only activate when the host joins/leaves, - // or roles are changed somehow - may be a regression in API, - // this is a workaround for the time being - switch (liveSession.session.role) { - case vsls.Role.None: - break; - case vsls.Role.Guest: - console.log('[LiveSessionListener] guest event'); - await rGuestService.startService(); - break; - default: - console.log('[LiveSessionListener] host event'); - await rHostService.startService(); - break; - } -} - -// Communication between the HostService and the GuestService -// typically falls under 2 communication paths (there are exceptions): -// -// 1. a function on the HostService is called, which pushes -// an event (notify), which is picked up by a callback (onNotify) -// e.g. rHostService.notifyRequest -// -// 2. a function on the GuestService is called, which pushes a -// request to the HostService, which is picked up the HostService -// callback and * returned * to the GuestService -// e.g. rGuestService.requestFileContent -// -// Note: If you are wanting the guest/host to run code, you must either ensure that -// the code is accessible from the guest/host, or the guest/host is notified of the -// method by the other role. Calling, for instance, a GuestService method from -// a method only accessible to the host will NOT call the method for the guest. -export class HostService { - private _isStarted: boolean = false; - // Service state getter - public isStarted(): boolean { - return this._isStarted; - } - public async startService(): Promise { - // Provides core liveshare functionality - // The shared service is used as a RPC service - // to pass messages between the host and guests - service = await liveSession.shareService(ShareProviderName); - if (service) { - this._isStarted = true; - for (const command in Commands.host) { - void liveShareOnRequest(command, Commands.host[command], service); - console.log(`[HostService] added ${command} callback`); - } - } else { - console.error('[HostService] service activation failed'); - } - } - public async stopService(): Promise { - await liveSession.unshareService(ShareProviderName); - service = null; - this._isStarted = false; - } - /// Session Syncing /// - // These are called from the host in order to tell the guest session - // to update the env/request/plot - // This way, we don't have to re-create a guest version of the session - // watcher, and can rely on the host to tell when something needs to be - // updated - public notifyWorkspace(hostWorkspace: WorkspaceData): void { - if (this._isStarted && shareWorkspace) { - void liveShareRequest(Callback.NotifyWorkspaceUpdate, hostWorkspace); - } - } - public notifyRequest(file: string, force: boolean = false): void { - if (this._isStarted && shareWorkspace) { - void liveShareRequest(Callback.NotifyRequestUpdate, file, force); - void this.notifyWorkspace(workspaceData); - } - } - public notifyPlot(file: string): void { - if (this._isStarted && shareWorkspace) { - void liveShareRequest(Callback.NotifyPlotUpdate, file); - } - } - public notifyGuestPlotManager(url: string): void { - if (this._isStarted) { - void liveShareRequest(Callback.NotifyGuestPlotManager, url); - } - } - public orderGuestDetach(): void { - if (this._isStarted) { - void liveShareRequest(Callback.OrderDetach); - } - } -} - -export class GuestService { - private _isStarted: boolean = false; - public isStarted(): boolean { - return this._isStarted; - } - public async startService(): Promise { - service = await liveSession.getSharedService(ShareProviderName); - if (service) { - this._isStarted = true; - this.requestAttach(); - for (const command in Commands.guest) { - void liveShareOnRequest(command, Commands.guest[command], service); - console.log(`[GuestService] added ${command} callback`); - } - } else { - console.error('[GuestService] service request failed'); - } - } - public setStatusBarItem(sessionStatusBarItem: vscode.StatusBarItem): void { - _sessionStatusBarItem = sessionStatusBarItem; - } - // The guest requests the host returns the attach specifications to the guest - // This ensures that guests without read/write access can still view the - // R workspace - public requestAttach(): void { - if (this._isStarted) { - void liveShareRequest(Callback.RequestAttachGuest); - // focus guest term if it exists - const rTermNameOptions = ['R [Shared]', 'R Interactive [Shared]']; - const activeTerminalName = vscode.window.activeTerminal?.name; - if (activeTerminalName && !rTermNameOptions.includes(activeTerminalName)) { - for (const [i] of vscode.window.terminals.entries()) { - const terminal = vscode.window.terminals[i]; - const terminalName = terminal.name; - if (rTermNameOptions.includes(terminalName)) { - terminal.show(true); - } - } - } - } - } - // Used to ensure that the guest can run workspace viewer commands - // e.g.view, remove, clean - // * Permissions are handled host-side - public requestRunTextInTerm(text: string): void { - if (this._isStarted) { - void liveShareRequest(Callback.RequestRunTextInTerm, text); - } - } - // The session watcher relies on files for providing many functions to vscode-R. - // As LiveShare does not allow for exposing files outside a given workspace, - // the guest must rely on the host sending the content of a given file, in place - // of having their own /tmp/ files - public async requestFileContent(file: fs.PathLike | number): Promise; - public async requestFileContent(file: fs.PathLike | number, encoding: string): Promise; - public async requestFileContent(file: fs.PathLike | number, encoding?: string): Promise { - if (this._isStarted) { - if (encoding !== undefined) { - const content: string | unknown = await liveShareRequest(Callback.GetFileContent, file, encoding); - if (typeof content === 'string') { - return content; - } else { - console.error('[GuestService] failed to retrieve file content (not of type "string")'); - } - } else { - const content: Buffer | unknown = await liveShareRequest(Callback.GetFileContent, file); - if (content) { - return content as Buffer; - } else { - console.error('[GuestService] failed to retrieve file content (not of type "Buffer")'); - } - } - } - } - - public async requestHelpContent(file: string): Promise { - const content: string | null | unknown = await liveShareRequest(Callback.GetHelpFileContent, file); - if (content) { - return content as HelpFile; - } else { - console.error('[GuestService] failed to retrieve help content from host'); - } - } - -} - -// Clear up any listeners & disposables, so that vscode-R -// isn't slowed down if liveshare is ended -// This is used instead of relying on context disposables, -// as an R session can continue even when liveshare is ended -async function sessionCleanup(): Promise { - if (rHostService?.isStarted()) { - console.log('[HostService] stopping service'); - await rHostService.stopService(); - for (const [key, item] of browserDisposables.entries()) { - console.log(`[HostService] disposing of browser ${item.url}`); - item.Disposable.dispose(); - browserDisposables.splice(key); - } - rLiveShareProvider.refresh(); - } -} diff --git a/src/liveShare/shareCommands.ts b/src/liveShare/shareCommands.ts deleted file mode 100644 index 7745d2afd..000000000 --- a/src/liveShare/shareCommands.ts +++ /dev/null @@ -1,161 +0,0 @@ -import * as vsls from 'vsls'; -import * as vscode from 'vscode'; -import * as fs from 'fs-extra'; - -import { rHostService, isGuest, service } from '.'; -import { updateGuestRequest, updateGuestWorkspace, updateGuestPlot, detachGuest } from './shareSession'; -import { forwardCommands, shareWorkspace } from './shareTree'; - -import { runTextInTerm } from '../rTerminal'; -import { requestFile, WorkspaceData } from '../session'; -import { HelpFile } from '../helpViewer'; -import { globalHttpgdManager, globalRHelp } from '../extension'; - -// used in sending messages to the guest service, -// distinguishes the type of vscode message to show -const enum MessageType { - information = 'information', - error = 'error', - warning = 'warning' -} - -interface ICommands { - host: { - [name: string]: unknown - }, - guest: { - [name: string]: unknown - } -} - -// used for notify & request events -// (mainly to prevent typos) -export const enum Callback { - NotifyWorkspaceUpdate = 'NotifyWorkspaceUpdate', - NotifyPlotUpdate = 'NotifyPlotUpdate', - NotifyRequestUpdate = 'NotifyRequestUpdate', - NotifyMessage = 'NotifyMessage', - RequestAttachGuest = 'RequestAttachGuest', - RequestRunTextInTerm = 'RequestRunTextInTerm', - GetFileContent = 'GetFileContent', - OrderDetach = 'OrderDetach', - GetHelpFileContent = 'GetHelpFileContent', - NotifyGuestPlotManager = 'NotifyGuestPlotManager' -} - -// To contribute a request between the host and guest, -// add the method that will be triggered with the callback. -// method arguments should be defined as an array of 'args' -// -// A response should have the this typical structure: -// [Callback.name]: (args:[]): returnType => { -// method -// } -// -// A request, by comparison, may look something like this: -// method(args) { -// await request(Callback.name, args) -// } -export const Commands: ICommands = { - 'host': { - /// Terminal commands /// - // Command arguments are sent from the guest to the host, - // and then the host sends the arguments to the console - [Callback.RequestAttachGuest]: (): void => { - if (shareWorkspace && rHostService) { - void rHostService.notifyRequest(requestFile, true); - } else { - void liveShareRequest(Callback.NotifyMessage, 'The host has not enabled guest attach.', MessageType.warning); - } - }, - [Callback.RequestRunTextInTerm]: (args: [text: string]): void => { - if (forwardCommands) { - void runTextInTerm(`${args[0]}`); - } else { - void liveShareRequest(Callback.NotifyMessage, 'The host has not enabled command forwarding. Command was not sent.', MessageType.warning); - } - - }, - [Callback.GetHelpFileContent]: (args: [text: string]): Promise | undefined => { - return globalRHelp?.getHelpFileForPath(args[0]); - }, - /// File Handling /// - // Host reads content from file, then passes the content - // to the guest session. - [Callback.GetFileContent]: async (args: [text: string, encoding?: string]): Promise => { - return args[1] !== undefined ? - await fs.readFile(args[0], args[1]) : - await fs.readFile(args[0]); - } - }, - 'guest': { - [Callback.NotifyRequestUpdate]: (args: [file: string, force: boolean]): void => { - void updateGuestRequest(args[0], args[1]); - }, - [Callback.NotifyWorkspaceUpdate]: (args: [hostWorkspace: WorkspaceData]): void => { - void updateGuestWorkspace(args[0]); - }, - [Callback.NotifyPlotUpdate]: (args: [file: string]): void => { - void updateGuestPlot(args[0]); - }, - [Callback.NotifyGuestPlotManager]: (args: [url: string]): void => { - void globalHttpgdManager?.showViewer(args[0]); - }, - [Callback.OrderDetach]: (): void => { - void detachGuest(); - }, - /// vscode Messages /// - // The host sends messages to the guest, which are displayed as a vscode window message - // E.g., teling the guest a terminal is not attached to the current session - // This way, we don't have to do much error checking on the guests side, which is more secure - // and less prone to error - [Callback.NotifyMessage]: (args: [text: string, messageType: MessageType]): void => { - switch (args[1]) { - case MessageType.error: - return void vscode.window.showErrorMessage(args[0]); - case MessageType.information: - return void vscode.window.showInformationMessage(args[0]); - case MessageType.warning: - return void vscode.window.showWarningMessage(args[0]); - case undefined: - return void vscode.window.showInformationMessage(args[0]); - } - } - } -}; - - -// The following onRequest and request methods are wrappers -// around the vsls RPC API. These are intended to simplify -// the API, so that the learning curve is minimal for contributing -// future callbacks. -// -// You can see that the onNotify and notify methods have been -// aggregated under these two methods. This is because the host service -// has no request methods, and for *most* purposes, there is little functional -// difference between request and notify. -export function liveShareOnRequest(name: string, command: unknown, service: vsls.SharedService | vsls.SharedServiceProxy | null): void { - if (isGuest()) { - // is guest service - (service as vsls.SharedServiceProxy).onNotify(name, command as vsls.NotifyHandler); - } else { - // is host service - (service as vsls.SharedService).onRequest(name, command as vsls.RequestHandler); - } -} - -export function liveShareRequest(name: string, ...rest: unknown[]): unknown { - if (isGuest()) { - if (rest !== undefined) { - return (service as vsls.SharedServiceProxy).request(name, rest); - } else { - return (service as vsls.SharedServiceProxy).request(name, []); - } - } else { - if (rest !== undefined) { - return (service as vsls.SharedService).notify(name, { ...rest }); - } else { - return (service as vsls.SharedService).notify(name, {}); - } - } -} diff --git a/src/liveShare/shareSession.ts b/src/liveShare/shareSession.ts deleted file mode 100644 index d11b930e0..000000000 --- a/src/liveShare/shareSession.ts +++ /dev/null @@ -1,276 +0,0 @@ -import path = require('path'); -import * as vscode from 'vscode'; - -import { extensionContext, globalHttpgdManager, globalRHelp, rWorkspace } from '../extension'; -import { asViewColumn, config, readContent } from '../util'; -import { showBrowser, showDataView, showWebView, WorkspaceData } from '../session'; -import { liveSession, UUID, rGuestService, _sessionStatusBarItem as sessionStatusBarItem } from '.'; -import { autoShareBrowser } from './shareTree'; -import { docProvider, docScheme } from './virtualDocs'; - -// Workspace Vars -let guestPid: string; -export let guestWorkspace: WorkspaceData | undefined; -export let guestResDir: string; -let rVer: string; -let info: IRequest['info']; - -// Browser Vars -// Used to keep track of shared browsers -export const browserDisposables: { Disposable: vscode.Disposable, url: string, name: string }[] = []; - -export interface IRequest { - command: string; - time?: string; - pid?: string; - wd?: string; - source?: string; - type?: string; - title?: string; - file?: string; - viewer?: string; - plot?: string; - action?: string; - args?: unknown; - sd?: string; - url?: string; - requestPath?: string; - uuid?: number; - tempdir?: string; - version?: string; - info?: { - version: string, - command: string, - start_time: string - }; -} - -export function initGuest(context: vscode.ExtensionContext): void { - // create status bar item that contains info about the *guest* session watcher - console.info('Create guestSessionStatusBarItem'); - const sessionStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 1000); - sessionStatusBarItem.command = 'r.attachActiveGuest'; - sessionStatusBarItem.text = 'Guest R: (not attached)'; - sessionStatusBarItem.tooltip = 'Click to attach to host terminal'; - sessionStatusBarItem.show(); - context.subscriptions.push( - sessionStatusBarItem, - vscode.workspace.registerTextDocumentContentProvider(docScheme, docProvider) - ); - rGuestService?.setStatusBarItem(sessionStatusBarItem); - guestResDir = path.join(context.extensionPath, 'dist', 'resources'); -} - -export function detachGuest(): void { - console.info('[Guest Service] detach guest from workspace'); - sessionStatusBarItem.text = 'Guest R: (not attached)'; - sessionStatusBarItem.tooltip = 'Click to attach to host terminal'; - guestWorkspace = undefined; - rWorkspace?.refresh(); -} - -export function attachActiveGuest(): void { - if (config().get('sessionWatcher', true)) { - console.info('[attachActiveGuest]'); - void rGuestService?.requestAttach(); - } else { - void vscode.window.showInformationMessage('This command requires that r.sessionWatcher be enabled.'); - } -} - -// Guest version of session.ts updateRequest(), no need to check for changes in files -// as this is handled by the session.ts variant -// the force parameter is used for ensuring that the 'attach' case is appropriately called on guest join -export async function updateGuestRequest(file: string, force: boolean = false): Promise { - const requestContent: string | undefined = await readContent(file, 'utf8'); - if (!requestContent) { - return; - } - console.info(`[updateGuestRequest] request: ${requestContent}`); - if (typeof (requestContent) !== 'string') { - return; - } - - const request: IRequest = JSON.parse(requestContent) as IRequest; - if (!request) { - return; - } - - if (force) { - // The last request is not necessarily an attach request. - guestPid = String(request.pid); - console.info(`[updateGuestRequest] attach PID: ${guestPid}`); - sessionStatusBarItem.text = `Guest R: ${guestPid}`; - sessionStatusBarItem.tooltip = 'Click to attach to host terminal.'; - sessionStatusBarItem.show(); - } - - if (request.uuid === null || request.uuid === undefined || request.uuid === UUID) { - switch (request.command) { - case 'help': { - if (globalRHelp) { - console.log(request.requestPath); - if (request.requestPath) { - await globalRHelp.showHelpForPath(request.requestPath, request.viewer); - } - } - break; - } - case 'httpgd': { - if (request.url) { - await globalHttpgdManager?.showViewer(request.url); - } - break; - } - case 'attach': { - guestPid = String(request.pid); - rVer = String(request.version); - info = request.info; - console.info(`[updateGuestRequest] attach PID: ${guestPid}`); - sessionStatusBarItem.text = `Guest R ${rVer}: ${guestPid}`; - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - sessionStatusBarItem.tooltip = `${info?.version || 'unknown version'}\nProcess ID: ${guestPid}\nCommand: ${info?.command}\nStart time: ${info?.start_time}\nClick to attach to host terminal.`; - sessionStatusBarItem.show(); - break; - } - case 'browser': { - if (request.url && request.title && request.viewer !== undefined) { - await showBrowser(request.url, request.title, request.viewer); - } - break; - } - case 'webview': { - if (request.file && request.title && request.viewer !== undefined) { - await showWebView(request.file, request.title, request.viewer); - } - break; - } - case 'dataview': { - if (request.source && request.type && request.title && request.file - && request.viewer !== undefined) { - await showDataView(request.source, - request.type, request.title, request.file, request.viewer); - } - break; - } - case 'rstudioapi': { - console.error(`[GuestService] ${request.command} not supported`); - break; - } - default: - console.error(`[updateRequest] Unsupported command: ${request.command}`); - } - - } -} - -// Call from host, pass parsed workspace file -export function updateGuestWorkspace(hostWorkspace: WorkspaceData): void { - if (hostWorkspace) { - guestWorkspace = hostWorkspace; - void rWorkspace?.refresh(); - console.info('[updateGuestWorkspace] Done'); - } -} - -// Instead of creating a file, we pass the base64 of the plot image -// to the guest, and read that into an html page -let panel: vscode.WebviewPanel | undefined = undefined; -export async function updateGuestPlot(file: string): Promise { - const plotContent = await readContent(file, 'base64'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - - const guestPlotView: vscode.ViewColumn = asViewColumn(config().get('session.viewers.viewColumn.plot'), vscode.ViewColumn.Two); - if (plotContent) { - if (panel) { - panel.webview.html = getGuestImageHtml(plotContent); - panel.reveal(guestPlotView, true); - } else { - panel = vscode.window.createWebviewPanel('dataview', 'R Guest Plot', - { - preserveFocus: true, - viewColumn: guestPlotView, - }, - { - enableScripts: true, - enableFindWidget: true, - retainContextWhenHidden: true, - localResourceRoots: [vscode.Uri.file(guestResDir)], - }); - const content = getGuestImageHtml(plotContent); - panel.webview.html = content; - panel.onDidDispose( - () => { - panel = undefined; - }, - undefined, - extensionContext.subscriptions - ); - } - } -} - - -// Purely used in order to decode a base64 string into -// an image format, bypassing saving a file onto the guest's system -function getGuestImageHtml(content: string) { - return ` - - - - - - - - - - - -`; -} - -export async function shareServer(url: URL, name: string): Promise { - return liveSession.shareServer({ - port: parseInt(url.port), - displayName: `${name} (${url.host})`, - browseUrl: url.toString() - }); -} - -// Share and close browser are called from the -// host session -// Automates sharing browser sessions through the -// shareServer method -export async function shareBrowser(url: string, name: string, force: boolean = false): Promise { - if (autoShareBrowser || force) { - const _url = new URL(url); - const disposable = await shareServer(_url, name); - console.log(`[HostService] shared ${name} at ${url}`); - browserDisposables.push({ Disposable: disposable, url, name }); - } -} - -export function closeBrowser(url: string): void { - browserDisposables.find( - e => e.url === url - )?.Disposable.dispose(); - - for (const [key, item] of browserDisposables.entries()) { - if (item.url === url) { - browserDisposables.splice(key, 1); - } - } -} diff --git a/src/liveShare/shareTree.ts b/src/liveShare/shareTree.ts deleted file mode 100644 index be0941f99..000000000 --- a/src/liveShare/shareTree.ts +++ /dev/null @@ -1,158 +0,0 @@ -import * as vscode from 'vscode'; -import { requestFile } from '../session'; - -import { config } from '../util'; -import { isLiveShare, rHostService } from '.'; - -export let forwardCommands: boolean; -export let shareWorkspace: boolean; -export let autoShareBrowser: boolean; -export let rLiveShareProvider: LiveShareTreeProvider; - -export function initTreeView(): void { - // get default bool values from settings - shareWorkspace = config().get('liveShare.defaults.shareWorkspace', true); - forwardCommands = config().get('liveShare.defaults.commandForward', false); - autoShareBrowser = config().get('liveShare.defaults.shareBrowser', false); - - // create tree view for host controls - rLiveShareProvider = new LiveShareTreeProvider(); - void vscode.window.registerTreeDataProvider( - 'rLiveShare', - rLiveShareProvider - ); -} - -export class LiveShareTreeProvider implements vscode.TreeDataProvider { - private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); - readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - - refresh(): void { - this._onDidChangeTreeData.fire(); - } - - getTreeItem(element: Node): vscode.TreeItem { - return element; - } - - // If a node needs to be collapsible, - // change the element condition & return value - getChildren(element?: Node): Node[] | undefined { - if (element) { - return; - } else { - return this.getNodes(); - } - } - - // To add a tree item to the LiveShare R view, - // write a class object that extends Node and - // add it to the list of nodes here - private getNodes(): Node[] | undefined { - let items: Node[] | undefined = undefined; - if (isLiveShare()) { - items = [ - new ShareNode(), - new CommandNode(), - new PortNode() - ]; - } - - return items; - } -} - -// Base class for adding to -abstract class Node extends vscode.TreeItem { - declare public label?: string; - declare public tooltip?: string; - declare public contextValue?: string; - declare public description?: string; - declare public iconPath?: vscode.ThemeIcon; - declare public collapsibleState?: vscode.TreeItemCollapsibleState; - - constructor() { - super(''); - } -} - -// Class for any tree item that should have a toggleable state -// To implement a ToggleNode, in the super, provide a boolean -// that is used for tracking state. -// If a toggle is not required, extend a different Node type. -export abstract class ToggleNode extends Node { - public toggle(treeProvider: LiveShareTreeProvider): void { treeProvider.refresh(); } - declare public label?: string; - declare public tooltip?: string; - declare public contextValue?: string; - declare public description?: string; - declare public iconPath?: vscode.ThemeIcon; - declare public collapsibleState?: vscode.TreeItemCollapsibleState; - - constructor(bool: boolean) { - super(); - this.description = bool === true ? 'Enabled' : 'Disabled'; - } - -} - -/// Nodes for changing R LiveShare variables -class ShareNode extends ToggleNode { - toggle(treeProvider: LiveShareTreeProvider): void { - shareWorkspace = !shareWorkspace; - this.description = shareWorkspace === true ? 'Enabled' : 'Disabled'; - if (shareWorkspace) { - void rHostService?.notifyRequest(requestFile, true); - } else { - void rHostService?.orderGuestDetach(); - } - treeProvider.refresh(); - } - - public label: string = 'Share R Workspace'; - public tooltip: string = 'Whether guests can access the current R session and its workspace'; - public contextValue: string = 'shareNode'; - declare public description?: string; - public iconPath: vscode.ThemeIcon = new vscode.ThemeIcon('broadcast'); - public collapsibleState: vscode.TreeItemCollapsibleState = vscode.TreeItemCollapsibleState.None; - - constructor() { - super(shareWorkspace); - } -} - -class CommandNode extends ToggleNode { - toggle(treeProvider: LiveShareTreeProvider): void { - forwardCommands = !forwardCommands; - this.description = forwardCommands === true ? 'Enabled' : 'Disabled'; - treeProvider.refresh(); - } - - public label: string = 'Guest interaction with host R extension'; - public tooltip: string = 'Whether commands to interact with the R extension should be forwarded from the guest to the host (bypasses permissions); shared R terminal (command line) permissions can be toggled in the Live Share extension'; - public contextValue: string = 'commandNode'; - public iconPath: vscode.ThemeIcon = new vscode.ThemeIcon('debug-step-over'); - public collapsibleState: vscode.TreeItemCollapsibleState = vscode.TreeItemCollapsibleState.None; - - constructor() { - super(forwardCommands); - } -} - -class PortNode extends ToggleNode { - toggle(treeProvider: LiveShareTreeProvider): void { - autoShareBrowser = !autoShareBrowser; - this.description = autoShareBrowser === true ? 'Enabled' : 'Disabled'; - treeProvider.refresh(); - } - - public label: string = 'Auto share ports'; - public tooltip: string = 'Whether opened R browsers should be shared with guests'; - public contextValue: string = 'portNode'; - public iconPath: vscode.ThemeIcon = new vscode.ThemeIcon('plug'); - public collapsibleState: vscode.TreeItemCollapsibleState = vscode.TreeItemCollapsibleState.None; - - constructor() { - super(autoShareBrowser); - } -} diff --git a/src/liveShare/virtualDocs.ts b/src/liveShare/virtualDocs.ts deleted file mode 100644 index 6d12d67dd..000000000 --- a/src/liveShare/virtualDocs.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as vscode from 'vscode'; - -export const docScheme = 'vscode-r'; -export const docProvider = new class implements vscode.TextDocumentContentProvider { - // class can be expanded if needed - provideTextDocumentContent(uri: vscode.Uri): string | Thenable { - return uri.query; - } -}; - -export async function openVirtualDoc(file: string, content: string, preserveFocus: boolean, preview: boolean, viewColumn: number): Promise { - if (content) { - const uri = vscode.Uri.parse(`${docScheme}:${file}?${content}`); - const doc = await vscode.workspace.openTextDocument(uri); - await vscode.window.showTextDocument(doc, { - preserveFocus: preserveFocus, - preview: preview, - viewColumn: viewColumn - }); - } -} \ No newline at end of file diff --git a/src/plotViewer/httpgdViewer.ts b/src/plotViewer/httpgdViewer.ts new file mode 100644 index 000000000..1a9d27a86 --- /dev/null +++ b/src/plotViewer/httpgdViewer.ts @@ -0,0 +1,628 @@ + +import * as vscode from 'vscode'; +import { Httpgd } from 'httpgd'; +import { HttpgdPlot, IHttpgdViewer, HttpgdViewerOptions } from './httpgdTypes'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as ejs from 'ejs'; + +import { asViewColumn, config, setContext, UriIcon, makeWebviewCommandUriString } from '../util'; +import { extensionContext } from '../extension'; +import { FocusPlotMessage, InMessage, OutMessage, ToggleStyleMessage, UpdatePlotMessage, HidePlotMessage, AddPlotMessage, PreviewPlotLayout, PreviewPlotLayoutMessage, ToggleFullWindowMessage } from './webviewMessages'; +import { HttpgdIdResponse, HttpgdPlotId, HttpgdRendererId } from 'httpgd/lib/types'; +import { PlotViewer } from './types'; + +export class HttpgdManager { + viewers: HttpgdViewer[] = []; + viewerOptions: HttpgdViewerOptions; + recentlyActiveViewers: HttpgdViewer[] = []; + + constructor() { + const htmlRoot = extensionContext.asAbsolutePath('dist/webviews/httpgd'); + this.viewerOptions = { + parent: this, + htmlRoot: htmlRoot, + preserveFocus: true + }; + } + + public async showViewer(urlString: string): Promise { + await Promise.resolve(); + const url = new URL(urlString); + const host = url.host; + const token = url.searchParams.get('token') || undefined; + const ind = this.viewers.findIndex( + (viewer) => viewer.host === host + ); + if (ind >= 0) { + const viewer = this.viewers.splice(ind, 1)[0]; + this.viewers.unshift(viewer); + viewer.show(); + } else { + const conf = config(); + const colorTheme = conf.get('plot.defaults.colorTheme', 'vscode'); + this.viewerOptions.stripStyles = (colorTheme === 'vscode'); + this.viewerOptions.previewPlotLayout = conf.get('plot.defaults.plotPreviewLayout', 'multirow'); + this.viewerOptions.refreshTimeoutLength = conf.get('plot.timing.refreshInterval', 10); + this.viewerOptions.resizeTimeoutLength = conf.get('plot.timing.resizeInterval', 100); + this.viewerOptions.fullWindow = conf.get('plot.defaults.fullWindowMode', false); + this.viewerOptions.token = token; + const viewer = new HttpgdViewer(host, this.viewerOptions); + this.viewers.unshift(viewer); + } + } + + public registerActiveViewer(viewer: HttpgdViewer): void { + const ind = this.recentlyActiveViewers.indexOf(viewer); + if (ind >= 0) { + this.recentlyActiveViewers.splice(ind, 1); + } + this.recentlyActiveViewers.unshift(viewer); + } + + public getRecentViewer(): HttpgdViewer | undefined { + return this.recentlyActiveViewers.find((viewer) => !!viewer.webviewPanel); + } + + public getNewestViewer(): HttpgdViewer | undefined { + return this.viewers[0]; + } + + public async openUrl(): Promise { + const clipText = await vscode.env.clipboard.readText(); + const val0 = clipText.trim().split(/[\n ]/)[0]; + const options: vscode.InputBoxOptions = { + value: val0, + prompt: 'Please enter the httpgd url' + }; + const urlString = await vscode.window.showInputBox(options); + if (urlString) { + await this.showViewer(urlString); + } + } +} + +interface EjsData { + overwriteStyles: boolean; + previewPlotLayout: PreviewPlotLayout; + activePlot?: HttpgdPlotId; + plots: HttpgdPlot[]; + largePlot: HttpgdPlot; + host: string; + asLocalPath: (relPath: string) => string; + asWebViewPath: (localPath: string) => string; + makeCommandUri: (command: string, ...args: unknown[]) => string; + overwriteCssPath: string; + plot?: HttpgdPlot; +} + +interface ShowOptions { + viewColumn: vscode.ViewColumn, + preserveFocus?: boolean +} + +export class HttpgdViewer implements IHttpgdViewer, PlotViewer { + readonly id: string; + readonly parent: HttpgdManager; + readonly host: string; + readonly token?: string; + webviewPanel?: vscode.WebviewPanel; + readonly api: Httpgd; + plots: HttpgdPlot[] = []; + activePlot?: HttpgdPlotId; + hiddenPlots: HttpgdPlotId[] = []; + readonly defaultStripStyles: boolean = true; + stripStyles: boolean; + readonly defaultPreviewPlotLayout: PreviewPlotLayout = 'multirow'; + previewPlotLayout: PreviewPlotLayout; + readonly defaultFullWindow: boolean = false; + fullWindow: boolean; + customOverwriteCssPath?: string; + viewHeight: number = 600; + viewWidth: number = 800; + plotHeight: number = 600; + plotWidth: number = 800; + readonly zoom0: number = 1; + zoom: number = this.zoom0; + protected resizeTimeout?: NodeJS.Timeout; + readonly resizeTimeoutLength: number = 1300; + protected refreshTimeout?: NodeJS.Timeout; + readonly refreshTimeoutLength: number = 10; + private lastExportUri?: vscode.Uri; + readonly htmlTemplate: string; + readonly smallPlotTemplate: string; + readonly htmlRoot: string; + readonly showOptions: ShowOptions; + readonly webviewOptions: vscode.WebviewPanelOptions & vscode.WebviewOptions; + + protected get activeIndex(): number { + if(!this.activePlot){ + return -1; + } + return this.getIndex(this.activePlot); + } + protected set activeIndex(ind: number) { + if (this.plots.length === 0) { + this.activePlot = undefined; + } else { + ind = Math.max(ind, 0); + ind = Math.min(ind, this.plots.length - 1); + this.activePlot = this.plots[ind].id; + } + } + + constructor(host: string, options: HttpgdViewerOptions) { + this.host = host; + this.id = host; + this.token = options.token; + this.parent = options.parent; + + this.api = new Httpgd(this.host, this.token, true); + this.api.onPlotsChanged((newState) => { + void this.refreshPlotsDelayed(newState.plots); + }); + const conf = config(); + this.customOverwriteCssPath = conf.get('plot.customStyleOverwrites', ''); + const localResourceRoots = ( + this.customOverwriteCssPath ? + [extensionContext.extensionUri, vscode.Uri.file(path.dirname(this.customOverwriteCssPath))] : + undefined + ); + this.htmlRoot = options.htmlRoot; + this.htmlTemplate = fs.readFileSync(path.join(this.htmlRoot, 'index.ejs'), 'utf-8'); + this.smallPlotTemplate = fs.readFileSync(path.join(this.htmlRoot, 'smallPlot.ejs'), 'utf-8'); + this.showOptions = { + viewColumn: options.viewColumn ?? asViewColumn(conf.get('session.viewers.viewColumn.plot'), vscode.ViewColumn.Two), + preserveFocus: !!options.preserveFocus + }; + this.webviewOptions = { + enableCommandUris: true, + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: localResourceRoots + }; + this.stripStyles = options.stripStyles ?? this.defaultStripStyles; + this.previewPlotLayout = options.previewPlotLayout ?? this.defaultPreviewPlotLayout; + this.fullWindow = options.fullWindow ?? this.defaultFullWindow; + this.resizeTimeoutLength = options.refreshTimeoutLength ?? this.resizeTimeoutLength; + this.refreshTimeoutLength = options.refreshTimeoutLength ?? this.refreshTimeoutLength; + void this.api.connect(); + } + + public handleCommand(command: string, ...args: unknown[]): void | Promise { + const stringArg = findItemOfType(args, 'string'); + const boolArg = findItemOfType(args, 'boolean'); + + switch (command) { + case 'showIndex': return this.focusPlot(stringArg); + case 'nextPlot': return this.nextPlot(boolArg); + case 'prevPlot': return this.prevPlot(boolArg); + case 'lastPlot': return this.nextPlot(true); + case 'firstPlot': return this.prevPlot(true); + case 'resetPlots': return this.resetPlots(); + case 'toggleStyle': return this.toggleStyle(boolArg); + case 'togglePreviewPlots': return this.togglePreviewPlots(stringArg as PreviewPlotLayout); + case 'closePlot': return this.closePlot(stringArg); + case 'hidePlot': return this.hidePlot(stringArg); + case 'exportPlot': return this.exportPlot(stringArg); + case 'zoomIn': return this.zoomIn(); + case 'zoomOut': return this.zoomOut(); + case 'openExternal': return this.openExternal(); + case 'toggleFullWindow': return this.toggleFullWindow(); + } + } + + public show(preserveFocus?: boolean): void { + preserveFocus ??= this.showOptions.preserveFocus; + if (!this.webviewPanel) { + const showOptions = { + ...this.showOptions, + preserveFocus: preserveFocus + }; + this.webviewPanel = this.makeNewWebview(showOptions); + this.refreshHtml(); + } else { + this.webviewPanel.reveal(undefined, preserveFocus); + } + this.parent.registerActiveViewer(this); + } + + public openExternal(): void { + let urlString = `http://${this.host}/live`; + if (this.token) { + urlString += `?token=${this.token}`; + } + const uri = vscode.Uri.parse(urlString); + void vscode.env.openExternal(uri); + } + + public async focusPlot(id?: HttpgdPlotId): Promise { + this.activePlot = id || this.activePlot; + const plt = this.plots[this.activeIndex]; + if (plt && (plt.height !== this.viewHeight || plt.width !== this.viewHeight || plt.zoom !== this.zoom)) { + await this.refreshPlots(this.api.getPlots()); + } else { + this._focusPlot(); + } + } + protected _focusPlot(plotId?: HttpgdPlotId): void { + plotId ??= this.activePlot; + if(!plotId){ + return; + } + const msg: FocusPlotMessage = { + message: 'focusPlot', + plotId: plotId + }; + this.postWebviewMessage(msg); + void this.setContextValues(); + } + + public async nextPlot(last?: boolean): Promise { + this.activeIndex = last ? this.plots.length - 1 : this.activeIndex + 1; + await this.focusPlot(); + } + public async prevPlot(first?: boolean): Promise { + this.activeIndex = first ? 0 : this.activeIndex - 1; + await this.focusPlot(); + } + + public resetPlots(): void { + this.hiddenPlots = []; + this.zoom = this.zoom0; + void this.refreshPlots(this.api.getPlots(), true, true); + } + + public hidePlot(id?: HttpgdPlotId): void { + id ??= this.activePlot; + if (!id) { return; } + const tmpIndex = this.activeIndex; + this.hiddenPlots.push(id); + this.plots = this.plots.filter((plt) => !this.hiddenPlots.includes(plt.id)); + if (id === this.activePlot) { + this.activeIndex = tmpIndex; + this._focusPlot(); + } + this._hidePlot(id); + } + protected _hidePlot(id: HttpgdPlotId): void { + const msg: HidePlotMessage = { + message: 'hidePlot', + plotId: id + }; + this.postWebviewMessage(msg); + } + + public async closePlot(id?: HttpgdPlotId): Promise { + id ??= this.activePlot; + if (id) { + this.hidePlot(id); + await this.api.removePlot({ id: id }); + } + } + + public toggleStyle(force?: boolean): void { + this.stripStyles = force ?? !this.stripStyles; + const msg: ToggleStyleMessage = { + message: 'toggleStyle', + useOverwrites: this.stripStyles + }; + this.postWebviewMessage(msg); + } + + public toggleFullWindow(force?: boolean): void { + this.fullWindow = force ?? !this.fullWindow; + const msg: ToggleFullWindowMessage = { + message: 'toggleFullWindow', + useFullWindow: this.fullWindow + }; + this.postWebviewMessage(msg); + } + + public togglePreviewPlots(force?: PreviewPlotLayout): void { + if (force) { + this.previewPlotLayout = force; + } else if (this.previewPlotLayout === 'multirow') { + this.previewPlotLayout = 'scroll'; + } else if (this.previewPlotLayout === 'scroll') { + this.previewPlotLayout = 'hidden'; + } else if (this.previewPlotLayout === 'hidden') { + this.previewPlotLayout = 'multirow'; + } + const msg: PreviewPlotLayoutMessage = { + message: 'togglePreviewPlotLayout', + style: this.previewPlotLayout + }; + this.postWebviewMessage(msg); + } + + public zoomOut(): void { + if (this.zoom > 0.1) { + this.zoom -= 0.1; + void this.resizePlot(); + } + } + + public zoomIn(): void { + this.zoom += 0.1; + void this.resizePlot(); + } + + public async setContextValues(mightBeInBackground: boolean = false): Promise { + if (this.webviewPanel?.active) { + this.parent.registerActiveViewer(this); + await setContext('r.plot.active', true); + await setContext('r.plot.canGoBack', this.activeIndex > 0); + await setContext('r.plot.canGoForward', this.activeIndex < this.plots.length - 1); + } else if (!mightBeInBackground) { + await setContext('r.plot.active', false); + } + } + + public getPanelPath(): string | undefined { + if (!this.webviewPanel) { + return undefined; + } + const dummyUri = this.webviewPanel.webview.asWebviewUri(vscode.Uri.file('')); + const m = /^[^.]*/.exec(dummyUri.authority); + const webviewId = m?.[0] || ''; + return `webview-panel/webview-${webviewId}`; + } + + protected getIndex(id: HttpgdPlotId): number { + return this.plots.findIndex((plt: HttpgdPlot) => plt.id === id); + } + + protected handleResize(height: number, width: number, userTriggered: boolean = false): void { + this.viewHeight = height; + this.viewWidth = width; + if (userTriggered || this.resizeTimeoutLength === 0) { + if(this.resizeTimeout){ + clearTimeout(this.resizeTimeout); + } + this.resizeTimeout = undefined; + void this.resizePlot(); + } else if (!this.resizeTimeout) { + this.resizeTimeout = setTimeout(() => { + void this.resizePlot().then(() => + this.resizeTimeout = undefined + ); + }, this.resizeTimeoutLength); + } + } + + protected async resizePlot(id?: HttpgdPlotId): Promise { + id ??= this.activePlot; + if (!id) { return; } + const plt = await this.getPlotContent(id, this.viewWidth, this.viewHeight, this.zoom); + this.plotWidth = plt.width; + this.plotHeight = plt.height; + this.updatePlot(plt); + } + + protected async refreshPlotsDelayed(plotsIdResponse: HttpgdIdResponse[], redraw: boolean = false, force: boolean = false): Promise { + if(this.refreshTimeoutLength === 0){ + await this.refreshPlots(plotsIdResponse, redraw, force); + } else{ + clearTimeout(this.refreshTimeout); + this.refreshTimeout = setTimeout(() => { + void this.refreshPlots(plotsIdResponse, redraw, force).then(() => + this.refreshTimeout = undefined + ); + }, this.refreshTimeoutLength); + } + } + + protected async refreshPlots(plotsIdResponse: HttpgdIdResponse[], redraw: boolean = false, force: boolean = false): Promise { + const nPlots = this.plots.length; + let plotIds = plotsIdResponse.map((x) => x.id); + plotIds = plotIds.filter((id) => !this.hiddenPlots.includes(id)); + const newPlotPromises = plotIds.map(async (id) => { + const plot = this.plots.find((plt) => plt.id === id); + if (force || !plot || id === this.activePlot) { + return await this.getPlotContent(id, this.viewWidth, this.viewHeight, this.zoom); + } else { + return plot; + } + }); + const newPlots = await Promise.all(newPlotPromises); + const oldPlotIds = this.plots.map(plt => plt.id); + this.plots = newPlots; + if (this.plots.length !== nPlots) { + this.activePlot = this.plots[this.plots.length - 1]?.id; + } + if (redraw || !this.webviewPanel) { + this.refreshHtml(); + } else { + for (const plt of this.plots) { + if (oldPlotIds.includes(plt.id)) { + this.updatePlot(plt); + } else { + this.addPlot(plt); + } + } + this._focusPlot(); + } + } + + protected updatePlot(plt: HttpgdPlot): void { + const msg: UpdatePlotMessage = { + message: 'updatePlot', + plotId: plt.id, + svg: plt.data + }; + this.postWebviewMessage(msg); + } + + protected addPlot(plt: HttpgdPlot): void { + const ejsData = this.makeEjsData(); + ejsData.plot = plt; + const html = ejs.render(this.smallPlotTemplate, ejsData); + const msg: AddPlotMessage = { + message: 'addPlot', + html: html + }; + this.postWebviewMessage(msg); + void this.focusPlot(plt.id); + void this.setContextValues(); + } + + protected async getPlotContent(id: HttpgdPlotId, width: number, height: number, zoom: number): Promise> { + const args = { + id: id, + height: height, + width: width, + zoom: zoom, + renderer: 'svgp' + }; + const plotContent = await this.api.getPlot(args); + const svg = await plotContent?.text() || ''; + const plt: HttpgdPlot = { + id: id, + data: svg, + height: height, + width: width, + zoom: zoom, + }; + this.viewHeight = plt.height; + this.viewWidth = plt.width; + return plt; + } + + protected refreshHtml(): void { + this.webviewPanel ??= this.makeNewWebview(); + this.webviewPanel.webview.html = ''; + this.webviewPanel.webview.html = this.makeHtml(); + this.toggleFullWindow(this.fullWindow); + void this.setContextValues(true); + } + + protected makeHtml(): string { + const ejsData = this.makeEjsData(); + return ejs.render(this.htmlTemplate, ejsData); + } + + protected makeEjsData(): EjsData { + const asLocalPath = (relPath: string) => { + if (!this.webviewPanel) { + return relPath; + } + const localUri = vscode.Uri.file(path.join(this.htmlRoot, relPath)); + return localUri.fsPath; + }; + const asWebViewPath = (localPath: string) => { + if (!this.webviewPanel) { + return localPath; + } + const localUri = vscode.Uri.file(path.join(this.htmlRoot, localPath)); + const webViewUri = this.webviewPanel.webview.asWebviewUri(localUri); + return webViewUri.toString(); + }; + let overwriteCssPath = ''; + if (this.customOverwriteCssPath) { + const uri = vscode.Uri.file(this.customOverwriteCssPath); + overwriteCssPath = this.webviewPanel?.webview.asWebviewUri(uri).toString() || ''; + } else { + overwriteCssPath = asWebViewPath('styleOverwrites.css'); + } + return { + overwriteStyles: this.stripStyles, + previewPlotLayout: this.previewPlotLayout, + plots: this.plots, + largePlot: this.plots[this.activeIndex], + activePlot: this.activePlot, + host: this.host, + asLocalPath: asLocalPath, + asWebViewPath: asWebViewPath, + makeCommandUri: makeWebviewCommandUriString, + overwriteCssPath: overwriteCssPath + }; + } + + protected makeNewWebview(showOptions?: ShowOptions): vscode.WebviewPanel { + const webviewPanel = vscode.window.createWebviewPanel( + 'RPlot', + 'R Plot', + showOptions || this.showOptions, + this.webviewOptions + ); + webviewPanel.iconPath = new UriIcon('graph'); + webviewPanel.onDidDispose(() => this.webviewPanel = undefined); + webviewPanel.onDidChangeViewState(() => { + void this.setContextValues(); + }); + webviewPanel.webview.onDidReceiveMessage((e: OutMessage) => { + this.handleWebviewMessage(e); + }); + return webviewPanel; + } + + protected handleWebviewMessage(msg: OutMessage): void { + if (msg.message === 'log') { + console.log(msg.body); + } else if (msg.message === 'resize') { + void this.handleResize(msg.height, msg.width, msg.userTriggered); + } + } + + protected postWebviewMessage(msg: InMessage): void { + void this.webviewPanel?.webview.postMessage(msg); + } + + public async exportPlot(id?: HttpgdPlotId, rendererId?: HttpgdRendererId, outFile?: string): Promise { + id ||= this.activePlot || this.plots[this.plots.length - 1]?.id; + const plot = this.plots.find((plt) => plt.id === id); + if (!plot) { + void vscode.window.showWarningMessage('No plot available for export.'); + return; + } + if (!rendererId) { + const renderers = this.api.getRenderers(); + const qpItems = renderers.map(renderer => ({ + label: renderer.name, + detail: renderer.descr, + id: renderer.id + })); + const qpPick = await vscode.window.showQuickPick(qpItems, { placeHolder: 'Please choose a file format' }); + rendererId = qpPick?.id; + if(!rendererId){ + return; + } + } + if (!outFile) { + const options: vscode.SaveDialogOptions = {}; + const renderer = this.api.getRenderers().find(r => r.id === rendererId); + const ext = renderer?.ext.replace(/^\./, ''); + if(this.lastExportUri){ + const noExtPath = this.lastExportUri.fsPath.replace(/\.[^.]*$/, ''); + options.defaultUri = vscode.Uri.file(noExtPath + (ext ? `.${ext}` : '')); + } else { + const defaultFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if(defaultFolder) {options.defaultUri = vscode.Uri.file(path.join(defaultFolder, 'plot' + (ext ? `.${ext}` : '')));} + } + if(ext && renderer?.name) {options.filters = { [renderer.name]: [ext], ['All']: ['*'] };} + const outUri = await vscode.window.showSaveDialog(options); + if(outUri){ + this.lastExportUri = outUri; + outFile = outUri.fsPath; + } else {return;} + } + const plt = await this.api.getPlot({ id: this.activePlot, renderer: rendererId }) as unknown as { body: NodeJS.ReadableStream }; + const dest = fs.createWriteStream(outFile); + dest.on('error', (err) => void vscode.window.showErrorMessage(`Export failed: ${err.message}`)); + dest.on('close', () => void vscode.window.showInformationMessage(`Export done: ${outFile || ''}`)); + plt.body.pipe(dest); + } + + public dispose(): void { + this.api.disconnect(); + } +} + +function findItemOfType(arr: unknown[], type: 'string'): string | undefined; +function findItemOfType(arr: unknown[], type: 'boolean'): boolean | undefined; +function findItemOfType(arr: unknown[], type: 'number'): number | undefined; +function findItemOfType(arr: unknown[], type: string): T { + const item = arr.find((elm) => typeof elm === type) as T; + return item; +} diff --git a/src/plotViewer/index.ts b/src/plotViewer/index.ts index 20f81d636..0c48c7d13 100644 --- a/src/plotViewer/index.ts +++ b/src/plotViewer/index.ts @@ -1,24 +1,11 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - import * as vscode from 'vscode'; -import { Httpgd } from 'httpgd'; -import { HttpgdPlot, IHttpgdViewer, HttpgdViewerOptions } from './httpgdTypes'; -import * as path from 'path'; -import * as fs from 'fs'; -import * as ejs from 'ejs'; - -import { asViewColumn, config, setContext, UriIcon, makeWebviewCommandUriString } from '../util'; - +import { PlotViewer, PlotManager } from './types'; +import { HttpgdManager, HttpgdViewer } from './httpgdViewer'; +export { HttpgdManager }; +import { StandardPlotViewer } from './standardViewer'; import { extensionContext } from '../extension'; -import { FocusPlotMessage, InMessage, OutMessage, ToggleStyleMessage, UpdatePlotMessage, HidePlotMessage, AddPlotMessage, PreviewPlotLayout, PreviewPlotLayoutMessage, ToggleFullWindowMessage } from './webviewMessages'; -import { HttpgdIdResponse, HttpgdPlotId, HttpgdRendererId } from 'httpgd/lib/types'; -import { Response } from 'node-fetch'; -import { autoShareBrowser, isHost, shareServer } from '../liveShare'; - const commands = [ 'showViewers', 'openUrl', @@ -39,823 +26,76 @@ const commands = [ 'zoomOut' ] as const; -type CommandName = typeof commands[number]; - -export function initializeHttpgd(): HttpgdManager { - const httpgdManager = new HttpgdManager(); - for (const cmd of commands) { - const fullCommand = `r.plot.${cmd}`; - const cb = httpgdManager.getCommandHandler(cmd); - extensionContext.subscriptions.push( - vscode.commands.registerCommand(fullCommand, cb) - ); - } - return httpgdManager; -} - -export class HttpgdManager { - viewers: HttpgdViewer[] = []; - - viewerOptions: HttpgdViewerOptions; - - recentlyActiveViewers: HttpgdViewer[] = []; +export class CommonPlotManager implements PlotManager { + public httpgdManager: HttpgdManager; + public standardPlotViewer: StandardPlotViewer; constructor() { - const htmlRoot = extensionContext.asAbsolutePath('html/httpgd'); - this.viewerOptions = { - parent: this, - htmlRoot: htmlRoot, - preserveFocus: true - }; - } - - public async showViewer(urlString: string): Promise { - const url = new URL(urlString); - const host = url.host; - const token = url.searchParams.get('token') || undefined; - const ind = this.viewers.findIndex( - (viewer) => viewer.host === host - ); - if (ind >= 0) { - const viewer = this.viewers.splice(ind, 1)[0]; - this.viewers.unshift(viewer); - viewer.show(); - } else { - const conf = config(); - const colorTheme = conf.get('plot.defaults.colorTheme', 'vscode'); - this.viewerOptions.stripStyles = (colorTheme === 'vscode'); - this.viewerOptions.previewPlotLayout = conf.get('plot.defaults.plotPreviewLayout', 'multirow'); - this.viewerOptions.refreshTimeoutLength = conf.get('plot.timing.refreshInterval', 10); - this.viewerOptions.resizeTimeoutLength = conf.get('plot.timing.resizeInterval', 100); - this.viewerOptions.fullWindow = conf.get('plot.defaults.fullWindowMode', false); - this.viewerOptions.token = token; - const viewer = new HttpgdViewer(host, this.viewerOptions); - if (isHost() && autoShareBrowser) { - const disposable = await shareServer(url, 'httpgd'); - viewer.webviewPanel?.onDidDispose(() => void disposable.dispose()); - } - this.viewers.unshift(viewer); - } + this.httpgdManager = new HttpgdManager(); + this.standardPlotViewer = new StandardPlotViewer(); } - public registerActiveViewer(viewer: HttpgdViewer): void { - const ind = this.recentlyActiveViewers.indexOf(viewer); - if (ind) { - this.recentlyActiveViewers.splice(ind, 1); - } - this.recentlyActiveViewers.unshift(viewer); + get viewers(): PlotViewer[] { + const viewers: PlotViewer[] = [...this.httpgdManager.viewers]; + viewers.push(this.standardPlotViewer); + return viewers; } - public getRecentViewer(): HttpgdViewer | undefined { - return this.recentlyActiveViewers.find((viewer) => !!viewer.webviewPanel); + get activeViewer(): PlotViewer | undefined { + return this.httpgdManager.getRecentViewer() || this.standardPlotViewer; } - public getNewestViewer(): HttpgdViewer | undefined { - return this.viewers[0]; + public initialize(): void { + for (const cmd of commands) { + const fullCommand = `r.plot.${cmd}`; + extensionContext.subscriptions.push( + vscode.commands.registerCommand(fullCommand, (hostOrWebviewUri?: string | vscode.Uri, ...args: unknown[]) => { + void this.handleCommand(cmd, hostOrWebviewUri, ...args); + }) + ); + } } - public getCommandHandler(command: CommandName): (...args: any[]) => void { - return (...args: any[]) => { - this.handleCommand(command, ...args); - }; + public async showStandardPlot(): Promise { + await this.standardPlotViewer.update(); } - public async openUrl(): Promise { - const clipText = await vscode.env.clipboard.readText(); - const val0 = clipText.trim().split(/[\n ]/)[0]; - const options: vscode.InputBoxOptions = { - value: val0, - prompt: 'Please enter the httpgd url' - }; - const urlString = await vscode.window.showInputBox(options); - if (urlString) { - await this.showViewer(urlString); - } + public async showHttpgdPlot(url: string): Promise { + await this.httpgdManager.showViewer(url); } - // generic command handler - public handleCommand(command: CommandName, hostOrWebviewUri?: string | vscode.Uri, ...args: any[]): void { - // the number and type of arguments given to a command can vary, depending on where it was called from: - // - calling from the title bar menu provides two arguments, the first of which identifies the webview - // - calling from the command palette provides no arguments - // - calling from a command uri provides a flexible number/type of arguments - // below is an attempt to handle these different combinations efficiently and (somewhat) robustly - // - + private async handleCommand(command: string, hostOrWebviewUri?: string | vscode.Uri, ...args: unknown[]): Promise { if (command === 'showViewers') { - this.viewers.forEach(viewer => { + for (const viewer of this.viewers) { viewer.show(true); - }); + } return; - } else if (command === 'openUrl') { - void this.openUrl(); + } + + if (command === 'openUrl') { + await this.httpgdManager.openUrl(); return; } // Identify the correct viewer - let viewer: HttpgdViewer | undefined; + let viewer: PlotViewer | undefined; if (typeof hostOrWebviewUri === 'string') { - const host = hostOrWebviewUri; - viewer = this.viewers.find((viewer) => viewer.host === host); + viewer = this.httpgdManager.viewers.find((v: HttpgdViewer) => v.host === hostOrWebviewUri); } else if (hostOrWebviewUri instanceof vscode.Uri) { - const uri = hostOrWebviewUri; - viewer = this.viewers.find((viewer) => viewer.getPanelPath() === uri.path); + viewer = this.httpgdManager.viewers.find((v: HttpgdViewer) => v.getPanelPath() === hostOrWebviewUri.path); } - // fall back to most recent viewer - viewer ||= this.getRecentViewer(); + // Fallback to active viewer + viewer ||= this.activeViewer; - // Abort if no viewer identified - if (!viewer) { - return; + if (viewer) { + await viewer.handleCommand(command, ...args); } - - // Get possible arguments for commands: - const stringArg = findItemOfType(args, 'string'); - const boolArg = findItemOfType(args, 'boolean'); - - // Call corresponding method, possibly with an argument: - switch (command) { - case 'showIndex': { - void viewer.focusPlot(stringArg); - break; - } case 'nextPlot': { - void viewer.nextPlot(boolArg); - break; - } case 'prevPlot': { - void viewer.prevPlot(boolArg); - break; - } case 'lastPlot': { - void viewer.nextPlot(true); - break; - } case 'firstPlot': { - void viewer.prevPlot(true); - break; - } case 'resetPlots': { - viewer.resetPlots(); - break; - } case 'toggleStyle': { - void viewer.toggleStyle(boolArg); - break; - } case 'togglePreviewPlots': { - void viewer.togglePreviewPlots(stringArg as PreviewPlotLayout); - break; - } case 'closePlot': { - void viewer.closePlot(stringArg); - break; - } case 'hidePlot': { - void viewer.hidePlot(stringArg); - break; - } case 'exportPlot': { - void viewer.exportPlot(stringArg); - break; - } case 'zoomIn': { - void viewer.zoomIn(); - break; - } case 'zoomOut': { - void viewer.zoomOut(); - break; - } case 'openExternal': { - void viewer.openExternal(); - break; - } case 'toggleFullWindow': { - void viewer.toggleFullWindow(); - break; - } default: { - break; - } - } - } -} - - -interface EjsData { - overwriteStyles: boolean; - previewPlotLayout: PreviewPlotLayout; - activePlot?: HttpgdPlotId; - plots: HttpgdPlot[]; - largePlot: HttpgdPlot; - host: string; - asLocalPath: (relPath: string) => string; - asWebViewPath: (localPath: string) => string; - makeCommandUri: (command: string, ...args: any[]) => string; - overwriteCssPath: string; - - // only used to render an individual smallPlot div: - plot?: HttpgdPlot; -} - -interface ShowOptions { - viewColumn: vscode.ViewColumn, - preserveFocus?: boolean -} - -export class HttpgdViewer implements IHttpgdViewer { - - readonly parent: HttpgdManager; - - readonly host: string; - readonly token?: string; - - // Actual webview where the plot viewer is shown - // Will have to be created anew, if the user closes it and the plot changes - webviewPanel?: vscode.WebviewPanel; - - // Api that provides plot contents etc. - readonly api: Httpgd; - - // active plots - plots: HttpgdPlot[] = []; - - // Id of the currently viewed plot - activePlot?: HttpgdPlotId; - - // Ids of plots that are not shown, but not closed inside httpgd - hiddenPlots: HttpgdPlotId[] = []; - - readonly defaultStripStyles: boolean = true; - stripStyles: boolean; - - readonly defaultPreviewPlotLayout: PreviewPlotLayout = 'multirow'; - previewPlotLayout: PreviewPlotLayout; - - readonly defaultFullWindow: boolean = false; - fullWindow: boolean; - - // Custom file to be used instead of `styleOverwrites.css` - customOverwriteCssPath?: string; - - // Size of the view area: - viewHeight: number = 600; - viewWidth: number = 800; - - // Size of the shown plot (as computed): - plotHeight: number = 600; - plotWidth: number = 800; - - readonly zoom0: number = 1; - zoom: number = this.zoom0; - - protected resizeTimeout?: NodeJS.Timeout; - readonly resizeTimeoutLength: number = 1300; - - protected refreshTimeout?: NodeJS.Timeout; - readonly refreshTimeoutLength: number = 10; - - private lastExportUri?: vscode.Uri; - - readonly htmlTemplate: string; - readonly smallPlotTemplate: string; - readonly htmlRoot: string; - - readonly showOptions: ShowOptions; - readonly webviewOptions: vscode.WebviewPanelOptions & vscode.WebviewOptions; - - // Computed properties: - - // Get/set active plot by index instead of id: - protected get activeIndex(): number { - if(!this.activePlot){ - return -1; - } - return this.getIndex(this.activePlot); - } - protected set activeIndex(ind: number) { - if (this.plots.length === 0) { - this.activePlot = undefined; - } else { - ind = Math.max(ind, 0); - ind = Math.min(ind, this.plots.length - 1); - this.activePlot = this.plots[ind].id; - } - } - - // constructor called by the session watcher if a corresponding function was called in R - // creates a new api instance itself - constructor(host: string, options: HttpgdViewerOptions) { - this.host = host; - this.token = options.token; - this.parent = options.parent; - - this.api = new Httpgd(this.host, this.token, true); - this.api.onPlotsChanged((newState) => { - void this.refreshPlotsDelayed(newState.plots); - }); - this.api.onConnectionChanged(() => { - // todo - }); - this.api.onDeviceActiveChanged(() => { - // todo - }); - const conf = config(); - this.customOverwriteCssPath = conf.get('plot.customStyleOverwrites', ''); - const localResourceRoots = ( - this.customOverwriteCssPath ? - [extensionContext.extensionUri, vscode.Uri.file(path.dirname(this.customOverwriteCssPath))] : - undefined - ); - this.htmlRoot = options.htmlRoot; - this.htmlTemplate = fs.readFileSync(path.join(this.htmlRoot, 'index.ejs'), 'utf-8'); - this.smallPlotTemplate = fs.readFileSync(path.join(this.htmlRoot, 'smallPlot.ejs'), 'utf-8'); - this.showOptions = { - viewColumn: options.viewColumn ?? asViewColumn(conf.get('session.viewers.viewColumn.plot'), vscode.ViewColumn.Two), - preserveFocus: !!options.preserveFocus - }; - this.webviewOptions = { - enableCommandUris: true, - enableScripts: true, - retainContextWhenHidden: true, - localResourceRoots: localResourceRoots - }; - this.defaultStripStyles = options.stripStyles ?? this.defaultStripStyles; - this.stripStyles = this.defaultStripStyles; - this.defaultPreviewPlotLayout = options.previewPlotLayout ?? this.defaultPreviewPlotLayout; - this.previewPlotLayout = this.defaultPreviewPlotLayout; - this.defaultFullWindow = options.fullWindow ?? this.defaultFullWindow; - this.fullWindow = this.defaultFullWindow; - this.resizeTimeoutLength = options.refreshTimeoutLength ?? this.resizeTimeoutLength; - this.refreshTimeoutLength = options.refreshTimeoutLength ?? this.refreshTimeoutLength; - void this.api.connect(); - //void this.checkState(); - } - - - // Methods to interact with the webview - // Can e.g. be called by vscode commands + menu items: - - // Called to create a new webview if the user closed the old one: - public show(preserveFocus?: boolean): void { - preserveFocus ??= this.showOptions.preserveFocus; - if (!this.webviewPanel) { - const showOptions = { - ...this.showOptions, - preserveFocus: preserveFocus - }; - this.webviewPanel = this.makeNewWebview(showOptions); - this.refreshHtml(); - } else { - this.webviewPanel.reveal(undefined, preserveFocus); - } - this.parent.registerActiveViewer(this); - } - - public openExternal(): void { - let urlString = `http://${this.host}/live`; - if (this.token) { - urlString += `?token=${this.token}`; - } - const uri = vscode.Uri.parse(urlString); - void vscode.env.openExternal(uri); - } - - // focus a specific plot id - public async focusPlot(id?: HttpgdPlotId): Promise { - this.activePlot = id || this.activePlot; - const plt = this.plots[this.activeIndex]; - if (plt.height !== this.viewHeight || plt.width !== this.viewHeight || plt.zoom !== this.zoom) { - await this.refreshPlots(this.api.getPlots()); - } else { - this._focusPlot(); - } - } - protected _focusPlot(plotId?: HttpgdPlotId): void { - plotId ??= this.activePlot; - if(!plotId){ - return; - } - const msg: FocusPlotMessage = { - message: 'focusPlot', - plotId: plotId - }; - this.postWebviewMessage(msg); - void this.setContextValues(); - } - - // navigate through plots (supply `true` to go to end/beginning of list) - public async nextPlot(last?: boolean): Promise { - this.activeIndex = last ? this.plots.length - 1 : this.activeIndex + 1; - await this.focusPlot(); - } - public async prevPlot(first?: boolean): Promise { - this.activeIndex = first ? 0 : this.activeIndex - 1; - await this.focusPlot(); - } - - // restore closed plots, reset zoom, redraw html - public resetPlots(): void { - this.hiddenPlots = []; - this.zoom = this.zoom0; - void this.refreshPlots(this.api.getPlots(), true, true); - } - - public hidePlot(id?: HttpgdPlotId): void { - id ??= this.activePlot; - if (!id) { return; } - const tmpIndex = this.activeIndex; - this.hiddenPlots.push(id); - this.plots = this.plots.filter((plt) => !this.hiddenPlots.includes(plt.id)); - if (id === this.activePlot) { - this.activeIndex = tmpIndex; - this._focusPlot(); - } - this._hidePlot(id); - } - protected _hidePlot(id: HttpgdPlotId): void { - const msg: HidePlotMessage = { - message: 'hidePlot', - plotId: id - }; - this.postWebviewMessage(msg); - } - - public async closePlot(id?: HttpgdPlotId): Promise { - id ??= this.activePlot; - if (id) { - this.hidePlot(id); - await this.api.removePlot({ id: id }); - } - } - - public toggleStyle(force?: boolean): void { - this.stripStyles = force ?? !this.stripStyles; - const msg: ToggleStyleMessage = { - message: 'toggleStyle', - useOverwrites: this.stripStyles - }; - this.postWebviewMessage(msg); - } - - public toggleFullWindow(force?: boolean): void { - this.fullWindow = force ?? !this.fullWindow; - const msg: ToggleFullWindowMessage = { - message: 'toggleFullWindow', - useFullWindow: this.fullWindow - }; - this.postWebviewMessage(msg); - } - - public togglePreviewPlots(force?: PreviewPlotLayout): void { - if (force) { - this.previewPlotLayout = force; - } else if (this.previewPlotLayout === 'multirow') { - this.previewPlotLayout = 'scroll'; - } else if (this.previewPlotLayout === 'scroll') { - this.previewPlotLayout = 'hidden'; - } else if (this.previewPlotLayout === 'hidden') { - this.previewPlotLayout = 'multirow'; - } - const msg: PreviewPlotLayoutMessage = { - message: 'togglePreviewPlotLayout', - style: this.previewPlotLayout - }; - this.postWebviewMessage(msg); - } - - public zoomOut(): void { - if (this.zoom > 0) { - this.zoom -= 0.1; - void this.resizePlot(); - } - } - - public zoomIn(): void { - this.zoom += 0.1; - void this.resizePlot(); - } - - - public async setContextValues(mightBeInBackground: boolean = false): Promise { - if (this.webviewPanel?.active) { - this.parent.registerActiveViewer(this); - await setContext('r.plot.active', true); - await setContext('r.plot.canGoBack', this.activeIndex > 0); - await setContext('r.plot.canGoForward', this.activeIndex < this.plots.length - 1); - } else if (!mightBeInBackground) { - await setContext('r.plot.active', false); - } - } - - public getPanelPath(): string | undefined { - if (!this.webviewPanel) { - return undefined; - } - const dummyUri = this.webviewPanel.webview.asWebviewUri(vscode.Uri.file('')); - const m = /^[^.]*/.exec(dummyUri.authority); - const webviewId = m?.[0] || ''; - return `webview-panel/webview-${webviewId}`; - } - - protected getIndex(id: HttpgdPlotId): number { - return this.plots.findIndex((plt: HttpgdPlot) => plt.id === id); - } - - protected handleResize(height: number, width: number, userTriggered: boolean = false): void { - this.viewHeight = height; - this.viewWidth = width; - if (userTriggered || this.resizeTimeoutLength === 0) { - if(this.resizeTimeout){ - clearTimeout(this.resizeTimeout); - } - this.resizeTimeout = undefined; - void this.resizePlot(); - } else if (!this.resizeTimeout) { - this.resizeTimeout = setTimeout(() => { - void this.resizePlot().then(() => - this.resizeTimeout = undefined - ); - }, this.resizeTimeoutLength); - } - } - - protected async resizePlot(id?: HttpgdPlotId): Promise { - id ??= this.activePlot; - if (!id) { return; } - const plt = await this.getPlotContent(id, this.viewWidth, this.viewHeight, this.zoom); - this.plotWidth = plt.width; - this.plotHeight = plt.height; - this.updatePlot(plt); - } - - protected async refreshPlotsDelayed(plotsIdResponse: HttpgdIdResponse[], redraw: boolean = false, force: boolean = false): Promise { - if(this.refreshTimeoutLength === 0){ - await this.refreshPlots(plotsIdResponse, redraw, force); - } else{ - clearTimeout(this.refreshTimeout); - this.refreshTimeout = setTimeout(() => { - void this.refreshPlots(plotsIdResponse, redraw, force).then(() => - this.refreshTimeout = undefined - ); - }, this.refreshTimeoutLength); - } - } - - protected async refreshPlots(plotsIdResponse: HttpgdIdResponse[], redraw: boolean = false, force: boolean = false): Promise { - const nPlots = this.plots.length; - let plotIds = plotsIdResponse.map((x) => x.id); - plotIds = plotIds.filter((id) => !this.hiddenPlots.includes(id)); - const newPlotPromises = plotIds.map(async (id) => { - const plot = this.plots.find((plt) => plt.id === id); - if (force || !plot || id === this.activePlot) { - return await this.getPlotContent(id, this.viewWidth, this.viewHeight, this.zoom); - } else { - return plot; - } - }); - const newPlots = await Promise.all(newPlotPromises); - const oldPlotIds = this.plots.map(plt => plt.id); - this.plots = newPlots; - if (this.plots.length !== nPlots) { - this.activePlot = this.plots[this.plots.length - 1]?.id; - } - if (redraw || !this.webviewPanel) { - this.refreshHtml(); - } else { - for (const plt of this.plots) { - if (oldPlotIds.includes(plt.id)) { - this.updatePlot(plt); - } else { - this.addPlot(plt); - } - } - this._focusPlot(); - } - } - - protected updatePlot(plt: HttpgdPlot): void { - const msg: UpdatePlotMessage = { - message: 'updatePlot', - plotId: plt.id, - svg: plt.data - }; - this.postWebviewMessage(msg); - } - - protected addPlot(plt: HttpgdPlot): void { - const ejsData = this.makeEjsData(); - ejsData.plot = plt; - const html = ejs.render(this.smallPlotTemplate, ejsData); - const msg: AddPlotMessage = { - message: 'addPlot', - html: html - }; - this.postWebviewMessage(msg); - void this.focusPlot(plt.id); - void this.setContextValues(); - } - - // get content of a single plot - protected async getPlotContent(id: HttpgdPlotId, width: number, height: number, zoom: number): Promise> { - - const args = { - id: id, - height: height, - width: width, - zoom: zoom, - renderer: 'svgp' - }; - - const plotContent = await this.api.getPlot(args); - const svg = await plotContent?.text() || ''; - - const plt: HttpgdPlot = { - id: id, - data: svg, - height: height, - width: width, - zoom: zoom, - }; - - this.viewHeight ??= plt.height; - this.viewWidth ??= plt.width; - return plt; - } - - - // functions for initial or re-drawing of html: - - protected refreshHtml(): void { - this.webviewPanel ??= this.makeNewWebview(); - this.webviewPanel.webview.html = ''; - this.webviewPanel.webview.html = this.makeHtml(); - // make sure that fullWindow is set correctly: - this.toggleFullWindow(this.fullWindow); - void this.setContextValues(true); - } - - protected makeHtml(): string { - const ejsData = this.makeEjsData(); - const html = ejs.render(this.htmlTemplate, ejsData); - return html; - } - - protected makeEjsData(): EjsData { - const asLocalPath = (relPath: string) => { - if (!this.webviewPanel) { - return relPath; - } - const localUri = vscode.Uri.file(path.join(this.htmlRoot, relPath)); - return localUri.fsPath; - }; - const asWebViewPath = (localPath: string) => { - if (!this.webviewPanel) { - return localPath; - } - const localUri = vscode.Uri.file(path.join(this.htmlRoot, localPath)); - const webViewUri = this.webviewPanel.webview.asWebviewUri(localUri); - return webViewUri.toString(); - }; - let overwriteCssPath = ''; - if (this.customOverwriteCssPath) { - const uri = vscode.Uri.file(this.customOverwriteCssPath); - overwriteCssPath = this.webviewPanel?.webview.asWebviewUri(uri).toString() || ''; - } else { - overwriteCssPath = asWebViewPath('styleOverwrites.css'); - } - const ejsData: EjsData = { - overwriteStyles: this.stripStyles, - previewPlotLayout: this.previewPlotLayout, - plots: this.plots, - largePlot: this.plots[this.activeIndex], - activePlot: this.activePlot, - host: this.host, - asLocalPath: asLocalPath, - asWebViewPath: asWebViewPath, - makeCommandUri: makeWebviewCommandUriString, - overwriteCssPath: overwriteCssPath - }; - return ejsData; - } - - protected makeNewWebview(showOptions?: ShowOptions): vscode.WebviewPanel { - const webviewPanel = vscode.window.createWebviewPanel( - 'RPlot', - 'R Plot', - showOptions || this.showOptions, - this.webviewOptions - ); - webviewPanel.iconPath = new UriIcon('graph'); - webviewPanel.onDidDispose(() => this.webviewPanel = undefined); - webviewPanel.onDidChangeViewState(() => { - void this.setContextValues(); - }); - webviewPanel.webview.onDidReceiveMessage((e: OutMessage) => { - this.handleWebviewMessage(e); - }); - return webviewPanel; - } - - protected handleWebviewMessage(msg: OutMessage): void { - if (msg.message === 'log') { - console.log(msg.body); - } else if (msg.message === 'resize') { - const height = msg.height; - const width = msg.width; - const userTriggered = msg.userTriggered; - void this.handleResize(height, width, userTriggered); - } - } - - protected postWebviewMessage(msg: InMessage): void { - void this.webviewPanel?.webview.postMessage(msg); - } - - - // export plot - // if no format supplied, show a quickpick menu etc. - // if no filename supplied, show selector window - public async exportPlot(id?: HttpgdPlotId, rendererId?: HttpgdRendererId, outFile?: string): Promise { - // make sure id is valid or return: - id ||= this.activePlot || this.plots[this.plots.length - 1]?.id; - const plot = this.plots.find((plt) => plt.id === id); - if (!plot) { - void vscode.window.showWarningMessage('No plot available for export.'); - return; - } - // make sure format is valid or return: - if (!rendererId) { - const renderers = this.api.getRenderers(); - const qpItems = renderers.map(renderer => ({ - label: renderer.name, - detail: renderer.descr, - id: renderer.id - })); - const options: vscode.QuickPickOptions = { - placeHolder: 'Please choose a file format' - }; - // format = await vscode.window.showQuickPick(formats, options); - const qpPick = await vscode.window.showQuickPick(qpItems, options); - rendererId = qpPick?.id; - if(!rendererId){ - return; - } - } - // make sure outFile is valid or return: - if (!outFile) { - const options: vscode.SaveDialogOptions = {}; - - // Suggest a file extension: - const renderer = this.api.getRenderers().find(r => r.id === rendererId); - const ext = renderer?.ext.replace(/^\./, ''); - - // try to set default URI: - if(this.lastExportUri){ - const noExtPath = this.lastExportUri.fsPath.replace(/\.[^.]*$/, ''); - const defaultPath = noExtPath + (ext ? `.${ext}` : ''); - options.defaultUri = vscode.Uri.file(defaultPath); - } else { - // construct default Uri - const defaultFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; - if(defaultFolder){ - const defaultName = 'plot' + (ext ? `.${ext}` : ''); - options.defaultUri = vscode.Uri.file(path.join(defaultFolder, defaultName)); - } - } - // set file extension filter - if(ext && renderer?.name){ - options.filters = { - [renderer.name]: [ext], - ['All']: ['*'], - }; - } - - const outUri = await vscode.window.showSaveDialog(options); - if(outUri){ - this.lastExportUri = outUri; - outFile = outUri.fsPath; - } else { - return; - } - } - // get plot: - const plt = await this.api.getPlot({ - id: this.activePlot, - renderer: rendererId - }) as unknown as Response; // I am not sure why eslint thinks this is the - // browser Response object and not the node-fetch one. - // cross-fetch problem or config problem in vscode-r? - - const dest = fs.createWriteStream(outFile); - dest.on('error', (err) => void vscode.window.showErrorMessage( - `Export failed: ${err.message}` - )); - dest.on('close', () => void vscode.window.showInformationMessage( - `Export done: ${outFile || ''}` - )); - void plt.body.pipe(dest); - } - - // Dispose-function to clean up when vscode closes - // E.g. to close connections etc., notify R, ... - public dispose(): void { - this.api.disconnect(); } } -// helper function to handle argument lists that might contain (useless) extra arguments -function findItemOfType(arr: any[], type: 'string'): string | undefined; -function findItemOfType(arr: any[], type: 'boolean'): boolean | undefined; -function findItemOfType(arr: any[], type: 'number'): number | undefined; -function findItemOfType(arr: any[], type: string): T { - const item = arr.find((elm) => typeof elm === type) as T; - return item; +export function initializePlotManager(): PlotManager { + const manager = new CommonPlotManager(); + manager.initialize(); + return manager; } diff --git a/src/plotViewer/standardViewer.ts b/src/plotViewer/standardViewer.ts new file mode 100644 index 000000000..622b8c77e --- /dev/null +++ b/src/plotViewer/standardViewer.ts @@ -0,0 +1,179 @@ + +import * as vscode from 'vscode'; +import { asViewColumn, config, UriIcon } from '../util'; +import { sessionRequest, server } from '../session'; +import { PlotViewer } from './types'; + +interface PlotResponse { + data: string; + format: string; +} + +export class StandardPlotViewer implements PlotViewer { + readonly id: string = 'standard'; + private panel: vscode.WebviewPanel | undefined; + private viewWidth: number = 800; + private viewHeight: number = 600; + private plotData: string | undefined; + private plotFormat: string | undefined; + + public async update(): Promise { + const viewColumn = asViewColumn(config().get('session.viewers.viewColumn.plot'), vscode.ViewColumn.Two); + if (!this.panel) { + this.createPanel(viewColumn); + } else { + this.panel.reveal(viewColumn, true); + await this.requestPlot(); + } + } + + public show(preserveFocus?: boolean): void { + if (this.panel) { + this.panel.reveal(undefined, preserveFocus); + } + } + + public handleCommand(command: string): void { + if (command === 'showViewers') { + this.show(); + } + // Other commands are not supported by the standard viewer + } + + public dispose(): void { + this.panel?.dispose(); + } + + private createPanel(viewColumn: vscode.ViewColumn) { + this.panel = vscode.window.createWebviewPanel( + 'r.standardPlot', + 'R Plot', + { + viewColumn, + preserveFocus: true + }, + { + enableScripts: true, + retainContextWhenHidden: true + } + ); + + this.panel.iconPath = new UriIcon('graph'); + this.panel.webview.html = this.getHtml(); + + this.panel.webview.onDidReceiveMessage(async (msg: { type: string, width?: number, height?: number }) => { + if (msg.type === 'resize') { + this.viewWidth = msg.width || this.viewWidth; + this.viewHeight = msg.height || this.viewHeight; + await this.requestPlot(); + } + }); + + this.panel.onDidDispose(() => { + this.panel = undefined; + }); + } + + private async requestPlot() { + if (!server || !this.panel) { + return; + } + + const format = config().get('plot.format', 'svglite'); + const devArgs = config().get>('plot.devArgs'); + const response = await sessionRequest(server, { + method: 'plot_latest', + params: { + width: this.viewWidth, + height: this.viewHeight, + format: format, + devArgs: devArgs + } + }) as PlotResponse | undefined; + + if (response?.data) { + this.plotData = response.data; + this.plotFormat = response.format || format; + void this.panel.webview.postMessage({ + type: 'update', + data: this.plotData, + format: this.plotFormat + }); + } + } + + private getHtml() { + return ` + + + + + + + + +
+ + + + `; + } +} diff --git a/src/plotViewer/types.ts b/src/plotViewer/types.ts new file mode 100644 index 000000000..478c2d291 --- /dev/null +++ b/src/plotViewer/types.ts @@ -0,0 +1,15 @@ + +export interface PlotViewer { + readonly id: string; + show(preserveFocus?: boolean): void; + dispose(): void; + handleCommand(command: string, ...args: unknown[]): void | Promise; +} + +export interface PlotManager { + viewers: PlotViewer[]; + activeViewer: PlotViewer | undefined; + initialize(): void; + showStandardPlot(): Promise; + showHttpgdPlot(url: string): Promise; +} diff --git a/src/plotViewer/webview/index.ejs b/src/plotViewer/webview/index.ejs new file mode 100644 index 000000000..99036c41c --- /dev/null +++ b/src/plotViewer/webview/index.ejs @@ -0,0 +1,25 @@ + + + + + + > + + + +
> + <%- largePlot?.data %> +
+
+
+ <% plots.forEach((plot)=> { %> +
+ <%- include(asLocalPath('./smallPlot.ejs'), {plot: plot}) %> +
+ <% }) %> +
+
+ + + + diff --git a/src/plotViewer/webview/index.ts b/src/plotViewer/webview/index.ts new file mode 100644 index 000000000..117fb3fbb --- /dev/null +++ b/src/plotViewer/webview/index.ts @@ -0,0 +1,238 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + + +interface Plot { + // unique ID for this plot (w.r.t. this connection/device) + id: string; + + // svg of the plot + svg: string; + + height?: number; + width?: number; +} + +import { acquireVsCodeApi, ResizeMessage, InMessage, PreviewPlotLayout } from '../webviewMessages'; +const vscode = acquireVsCodeApi() as { postMessage: (msg: any) => void }; + +// globals +let oldHeight = -1; +let oldWidth = -1; + + +const handler = document.querySelector('#handler') as HTMLDivElement; +const largePlotDiv = document.querySelector('#largePlot') as HTMLDivElement; +const largeSvg = largePlotDiv.querySelector('svg') as SVGElement; +const cssLink = document.querySelector('link.overwrites') as HTMLLinkElement; +const smallPlotDiv = document.querySelector('#smallPlots') as HTMLDivElement; + + +function getSmallPlots(): HTMLAnchorElement[] { + const smallPlots: HTMLAnchorElement[] = []; + document.querySelectorAll('a.focusPlot').forEach(elm => { + smallPlots.push(elm as HTMLAnchorElement); + }); + return smallPlots; +} + +let isHandlerDragging = false; + + +let isFullWindow = false; + +function postResizeMessage(userTriggered: boolean = false){ + let newHeight = largePlotDiv.clientHeight; + let newWidth = largePlotDiv.clientWidth; + if(isFullWindow){ + newHeight = window.innerHeight; + newWidth = window.innerWidth; + } + if(userTriggered || newHeight !== oldHeight || newWidth !== oldWidth){ + const msg: ResizeMessage = { + message: 'resize', + height: newHeight, + width: newWidth, + userTriggered: userTriggered + }; + vscode.postMessage(msg); + oldHeight = newHeight; + oldWidth = newWidth; + } +} + +window.addEventListener('message', (ev: MessageEvent) => { + const msg = ev.data; + if(msg.message === 'updatePlot'){ + updatePlot({ + id: String(msg.plotId), + svg: msg.svg + }); + } else if(msg.message === 'focusPlot'){ + focusPlot(String(msg.plotId)); + } else if(msg.message === 'toggleStyle'){ + toggleStyle(msg.useOverwrites); + } else if(msg.message === 'hidePlot'){ + hidePlot(msg.plotId); + } else if(msg.message === 'addPlot'){ + addPlot(msg.html); + } else if(msg.message === 'togglePreviewPlotLayout'){ + togglePreviewPlotLayout(msg.style); + } else if(msg.message === 'toggleFullWindow'){ + toggleFullWindowMode(msg.useFullWindow); + } +}); + +function addPlot(html: string){ + const wrapper = document.createElement('div'); + wrapper.classList.add('wrapper'); + wrapper.innerHTML = html; + smallPlotDiv.appendChild(wrapper); +} + +function focusPlot(plotId: string): void { + + const smallPlots = getSmallPlots(); + + const ind = findIndex(plotId, smallPlots); + if(ind < 0){ + return; + } + + for(const elm of smallPlots){ + elm.classList.remove('active'); + } + + const smallPlot = smallPlots[ind]; + + smallPlot.classList.add('active'); + + largePlotDiv.innerHTML = smallPlot.innerHTML; +} + +function updatePlot(plt: Plot): void { + + const smallPlots = getSmallPlots(); + + const ind = findIndex(plt.id, smallPlots); + if(ind<0){ + return; + } + + smallPlots[ind].innerHTML = plt.svg; + + if(smallPlots[ind].classList.contains('active')){ + largePlotDiv.innerHTML = plt.svg; + } +} + +function hidePlot(plotId: string): void { + const smallPlots = getSmallPlots(); + + const ind = findIndex(plotId, smallPlots); + if(ind<0){ + return; + } + + if(smallPlots[ind].classList.contains('active')){ + largePlotDiv.innerHTML = ''; + } + + smallPlots[ind].parentElement?.remove(); +} + +function findIndex(plotId: string, smallPlots?: Element[]): number { + smallPlots ||= getSmallPlots(); + const ind = smallPlots.findIndex(elm => elm.getAttribute('plotId') === String(plotId)); + if(ind<0){ + console.warn(`plotId not found: ${plotId}`); + } + return ind; +} + +function toggleStyle(useOverwrites: boolean): void { + cssLink.disabled = !useOverwrites; +} + +function togglePreviewPlotLayout(newStyle: PreviewPlotLayout): void { + smallPlotDiv.classList.remove('multirow', 'scroll', 'hidden'); + smallPlotDiv.classList.add(newStyle); +} + +function toggleFullWindowMode(useFullWindow: boolean): void { + isFullWindow = useFullWindow; + if(useFullWindow){ + document.body.classList.add('fullWindow'); + window.scrollTo(0, 0); + } else { + document.body.classList.remove('fullWindow'); + } + postResizeMessage(true); +} + +//// +// On window load +//// + +window.onload = () => { + largePlotDiv.style.height = `${largeSvg.clientHeight}px`; + postResizeMessage(true); +}; + + +//// +// Resize bar +//// + + +document.addEventListener('mousedown', (e) => { + // If mousedown event is fired from .handler, toggle flag to true + if (!isFullWindow && e.target === handler) { + isHandlerDragging = true; + handler.classList.add('dragging'); + document.body.style.cursor = 'ns-resize'; + } +}); + +document.addEventListener('mousemove', (e) => { + // Don't do anything if dragging flag is false + if (isFullWindow || !isHandlerDragging) { + return false; + } + + // postLogMessage('mousemove'); + + // Get offset + const containerOffsetTop = document.body.offsetTop; + + // Get x-coordinate of pointer relative to container + const pointerRelativeYpos = e.clientY - containerOffsetTop + window.scrollY; + + // Arbitrary minimum width set on box A, otherwise its inner content will collapse to width of 0 + const largePlotMinHeight = 60; + + // Resize large plot + const newHeight = Math.max(largePlotMinHeight, pointerRelativeYpos - 5); // <- why 5? + const newHeightString = `${newHeight}px`; + + if(largePlotDiv.style.height !== newHeightString){ + largePlotDiv.style.height = newHeightString; + postResizeMessage(); + } +}); + +window.onresize = () => postResizeMessage(); + +document.addEventListener('mouseup', () => { + // Turn off dragging flag when user mouse is up + if(isHandlerDragging){ + postResizeMessage(true); + document.body.style.cursor = ''; + } + handler.classList.remove('dragging'); + isHandlerDragging = false; +}); + diff --git a/src/plotViewer/webview/smallPlot.ejs b/src/plotViewer/webview/smallPlot.ejs new file mode 100644 index 000000000..7d8337661 --- /dev/null +++ b/src/plotViewer/webview/smallPlot.ejs @@ -0,0 +1,15 @@ + + <%- plot.data %> + + + ✖ + diff --git a/src/plotViewer/webview/style.css b/src/plotViewer/webview/style.css new file mode 100644 index 000000000..c7d30c9b4 --- /dev/null +++ b/src/plotViewer/webview/style.css @@ -0,0 +1,136 @@ + +/* Use box-sizing: border-box everywhere: */ +html { + box-sizing: border-box; +} +*, *::before, *::after { + box-sizing: inherit; +} + +/* General styling: */ +body { + padding-left: 1px; + padding-right: 1px; +} +body.fullWindow { + overflow-x: hidden; + overflow-y: hidden; +} + +svg { + user-select: none; +} + +/* Stretch large plot during resizing, */ +/* Keep small plots the same size: */ +.httpgd { + width: 100%; + height: 100%; +} + +/* Main plot area: */ +#largePlot { + overflow-x: auto; + overflow-y: hidden; + padding: 10px; + width: 100%; + height: 100%; +} +body.fullWindow #largePlot { + overflow-x: hidden; + width: 100vw !important; + height: 100vh !important; +} + +/* Dragbar to resize main plot: */ +#handler { + background-color: var(--vscode-textSeparator-foreground); + height: 4px; + cursor: ns-resize; +} +#handler:hover, #handler.dragging { + background-color: var(--vscode-focusBorder); + transition: background-color .1s ease-out; + transition-delay: .2s; +} +body.fullWindow #handler { + display: none; +} + +#placeholder { + height: 95vh; +} +body.fullWindow #placeHolder { + display: none; +} + +/* Plot history: */ + +#smallPlots { + display: flex; + /* flex-direction: row; */ + position: relative; + overflow-x: auto; + flex-direction: row; + overflow-y: hidden; + padding: 10px; +} +body.fullWindow #smallPlots { + display: none; +} + +#smallPlots.multirow { + overflow-x: hidden; + flex-wrap: wrap; +} + +#smallPlots.hidden { + display: none; +} + +/* Each small plot: */ + +#smallPlots .wrapper { + position: relative; + height: 15vw; + width: 19vw; + flex: none; + padding: 3px; + padding-bottom: 12px; +} + +a.focusPlot { + height: 100%; + width: 100%; +} + +a.hidePlot { + display: none; + position: absolute; + top: 0; + right: 5px; + text-decoration: none; + font-size: 2em; + user-select: none; + color: var(--vscode-foreground); +} + +.plotContent { + height: 100%; + width: 100%; +} + +.smallPlot:not(.active):hover { + background-color: var(--vscode-list-hoverBackground); + background-clip: content-box; +} + +/* Hide plot button: */ +#smallPlots .wrapper:hover a.hidePlot { + display: block; +} + +#smallPlots .wrapper a.hidePlot:hover { + color: var(--vscode-errorForeground); +} + diff --git a/src/plotViewer/webview/styleOverwrites.css b/src/plotViewer/webview/styleOverwrites.css new file mode 100644 index 000000000..953ac5611 --- /dev/null +++ b/src/plotViewer/webview/styleOverwrites.css @@ -0,0 +1,15 @@ + + +.httpgd rect { + stroke: none !important; + fill: none !important; +} + +svg text { + font-family: var(--vscode-editor-font-family) !important; + fill: var(--vscode-foreground) !important; +} + +.httpgd line, .httpgd polyline, .httpgd polygon, .httpgd path, .httpgd circle, .httpgd rect:not(:first-of-type) { + stroke: var(--vscode-foreground) !important; +} diff --git a/src/plotViewer/webview/vars.css b/src/plotViewer/webview/vars.css new file mode 100644 index 000000000..d20aad6b3 --- /dev/null +++ b/src/plotViewer/webview/vars.css @@ -0,0 +1,470 @@ +/* This file contains a list of variables available in a vscode webview and their values from the Dark+ theme */ + +.fromvscode { + --vscode-font-family: "Segoe WPC", "Segoe UI", sans-serif; + --vscode-font-weight: normal; + --vscode-font-size: 13px; + --vscode-editor-font-family: Consolas, "Courier New", monospace; + --vscode-editor-font-weight: normal; + --vscode-editor-font-size: 14px; + --vscode-foreground: #cccccc; + --vscode-errorForeground: #f48771; + --vscode-descriptionForeground: rgba(204, 204, 204, 0.7); + --vscode-icon-foreground: #c5c5c5; + --vscode-focusBorder: #007fd4; + --vscode-textSeparator-foreground: rgba(255, 255, 255, 0.18); + --vscode-textLink-foreground: #3794ff; + --vscode-textLink-activeForeground: #3794ff; + --vscode-textPreformat-foreground: #d7ba7d; + --vscode-textBlockQuote-background: rgba(127, 127, 127, 0.1); + --vscode-textBlockQuote-border: rgba(0, 122, 204, 0.5); + --vscode-textCodeBlock-background: rgba(10, 10, 10, 0.4); + --vscode-widget-shadow: rgba(0, 0, 0, 0.36); + --vscode-input-background: #3c3c3c; + --vscode-input-foreground: #cccccc; + --vscode-inputOption-activeBorder: rgba(0, 122, 204, 0); + --vscode-inputOption-activeBackground: rgba(0, 127, 212, 0.4); + --vscode-inputOption-activeForeground: #ffffff; + --vscode-input-placeholderForeground: #a6a6a6; + --vscode-inputValidation-infoBackground: #063b49; + --vscode-inputValidation-infoBorder: #007acc; + --vscode-inputValidation-warningBackground: #352a05; + --vscode-inputValidation-warningBorder: #b89500; + --vscode-inputValidation-errorBackground: #5a1d1d; + --vscode-inputValidation-errorBorder: #be1100; + --vscode-dropdown-background: #3c3c3c; + --vscode-dropdown-foreground: #f0f0f0; + --vscode-dropdown-border: #3c3c3c; + --vscode-checkbox-background: #3c3c3c; + --vscode-checkbox-foreground: #f0f0f0; + --vscode-checkbox-border: #3c3c3c; + --vscode-button-foreground: #ffffff; + --vscode-button-background: #0e639c; + --vscode-button-hoverBackground: #1177bb; + --vscode-button-secondaryForeground: #ffffff; + --vscode-button-secondaryBackground: #3a3d41; + --vscode-button-secondaryHoverBackground: #45494e; + --vscode-badge-background: #4d4d4d; + --vscode-badge-foreground: #ffffff; + --vscode-scrollbar-shadow: #000000; + --vscode-scrollbarSlider-background: rgba(121, 121, 121, 0.4); + --vscode-scrollbarSlider-hoverBackground: rgba(100, 100, 100, 0.7); + --vscode-scrollbarSlider-activeBackground: rgba(191, 191, 191, 0.4); + --vscode-progressBar-background: #0e70c0; + --vscode-editorError-foreground: #f48771; + --vscode-editorWarning-foreground: #cca700; + --vscode-editorInfo-foreground: #75beff; + --vscode-editorHint-foreground: rgba(238, 238, 238, 0.7); + --vscode-sash-hoverBorder: #007fd4; + --vscode-editor-background: #1e1e1e; + --vscode-editor-foreground: #d4d4d4; + --vscode-editorWidget-background: #252526; + --vscode-editorWidget-foreground: #cccccc; + --vscode-editorWidget-border: #454545; + --vscode-quickInput-background: #252526; + --vscode-quickInput-foreground: #cccccc; + --vscode-quickInputTitle-background: rgba(255, 255, 255, 0.1); + --vscode-pickerGroup-foreground: #3794ff; + --vscode-pickerGroup-border: #3f3f46; + --vscode-editor-selectionBackground: #264f78; + --vscode-editor-inactiveSelectionBackground: #3a3d41; + --vscode-editor-selectionHighlightBackground: rgba(173, 214, 255, 0.15); + --vscode-editor-findMatchBackground: #515c6a; + --vscode-editor-findMatchHighlightBackground: rgba(234, 92, 0, 0.33); + --vscode-editor-findRangeHighlightBackground: rgba(58, 61, 65, 0.4); + --vscode-searchEditor-findMatchBackground: rgba(234, 92, 0, 0.22); + --vscode-editor-hoverHighlightBackground: rgba(38, 79, 120, 0.25); + --vscode-editorHoverWidget-background: #252526; + --vscode-editorHoverWidget-foreground: #cccccc; + --vscode-editorHoverWidget-border: #454545; + --vscode-editorHoverWidget-statusBarBackground: #2c2c2d; + --vscode-editorLink-activeForeground: #4e94ce; + --vscode-editorInlineHint-foreground: #252526; + --vscode-editorInlineHint-background: #cccccc; + --vscode-editorLightBulb-foreground: #ffcc00; + --vscode-editorLightBulbAutoFix-foreground: #75beff; + --vscode-diffEditor-insertedTextBackground: rgba(155, 185, 85, 0.2); + --vscode-diffEditor-removedTextBackground: rgba(255, 0, 0, 0.2); + --vscode-diffEditor-diagonalFill: rgba(204, 204, 204, 0.2); + --vscode-list-focusOutline: #007fd4; + --vscode-list-activeSelectionBackground: #094771; + --vscode-list-activeSelectionForeground: #ffffff; + --vscode-list-inactiveSelectionBackground: #37373d; + --vscode-list-hoverBackground: #2a2d2e; + --vscode-list-dropBackground: #383b3d; + --vscode-list-highlightForeground: #0097fb; + --vscode-list-invalidItemForeground: #b89500; + --vscode-list-errorForeground: #f88070; + --vscode-list-warningForeground: #cca700; + --vscode-listFilterWidget-background: #653723; + --vscode-listFilterWidget-outline: rgba(0, 0, 0, 0); + --vscode-listFilterWidget-noMatchesOutline: #be1100; + --vscode-list-filterMatchBackground: rgba(234, 92, 0, 0.33); + --vscode-tree-indentGuidesStroke: #585858; + --vscode-tree-tableColumnsBorder: rgba(204, 204, 204, 0.13); + --vscode-list-deemphasizedForeground: #8c8c8c; + --vscode-quickInputList-focusBackground: #062f4a; + --vscode-menu-foreground: #cccccc; + --vscode-menu-background: #252526; + --vscode-menu-selectionForeground: #ffffff; + --vscode-menu-selectionBackground: #094771; + --vscode-menu-separatorBackground: #bbbbbb; + --vscode-toolbar-hoverBackground: rgba(90, 93, 94, 0.31); + --vscode-toolbar-activeBackground: rgba(99, 102, 103, 0.31); + --vscode-editor-snippetTabstopHighlightBackground: rgba(124, 124, 124, 0.3); + --vscode-editor-snippetFinalTabstopHighlightBorder: #525252; + --vscode-breadcrumb-foreground: rgba(204, 204, 204, 0.8); + --vscode-breadcrumb-background: #1e1e1e; + --vscode-breadcrumb-focusForeground: #e0e0e0; + --vscode-breadcrumb-activeSelectionForeground: #e0e0e0; + --vscode-breadcrumbPicker-background: #252526; + --vscode-merge-currentHeaderBackground: rgba(64, 200, 174, 0.5); + --vscode-merge-currentContentBackground: rgba(64, 200, 174, 0.2); + --vscode-merge-incomingHeaderBackground: rgba(64, 166, 255, 0.5); + --vscode-merge-incomingContentBackground: rgba(64, 166, 255, 0.2); + --vscode-merge-commonHeaderBackground: rgba(96, 96, 96, 0.4); + --vscode-merge-commonContentBackground: rgba(96, 96, 96, 0.16); + --vscode-editorOverviewRuler-currentContentForeground: rgba(64, 200, 174, 0.5); + --vscode-editorOverviewRuler-incomingContentForeground: rgba(64, 166, 255, 0.5); + --vscode-editorOverviewRuler-commonContentForeground: rgba(96, 96, 96, 0.4); + --vscode-editorOverviewRuler-findMatchForeground: rgba(209, 134, 22, 0.49); + --vscode-editorOverviewRuler-selectionHighlightForeground: rgba(160, 160, 160, 0.8); + --vscode-minimap-findMatchHighlight: #d18616; + --vscode-minimap-selectionHighlight: #264f78; + --vscode-minimap-errorHighlight: rgba(255, 18, 18, 0.7); + --vscode-minimap-warningHighlight: #cca700; + --vscode-minimapSlider-background: rgba(121, 121, 121, 0.2); + --vscode-minimapSlider-hoverBackground: rgba(100, 100, 100, 0.35); + --vscode-minimapSlider-activeBackground: rgba(191, 191, 191, 0.2); + --vscode-problemsErrorIcon-foreground: #f48771; + --vscode-problemsWarningIcon-foreground: #cca700; + --vscode-problemsInfoIcon-foreground: #75beff; + --vscode-charts-foreground: #cccccc; + --vscode-charts-lines: rgba(204, 204, 204, 0.5); + --vscode-charts-red: #f48771; + --vscode-charts-blue: #75beff; + --vscode-charts-yellow: #cca700; + --vscode-charts-orange: #d18616; + --vscode-charts-green: #89d185; + --vscode-charts-purple: #b180d7; + --vscode-editor-lineHighlightBorder: #282828; + --vscode-editor-rangeHighlightBackground: rgba(255, 255, 255, 0.04); + --vscode-editor-symbolHighlightBackground: rgba(234, 92, 0, 0.33); + --vscode-editorCursor-foreground: #aeafad; + --vscode-editorWhitespace-foreground: rgba(227, 228, 226, 0.16); + --vscode-editorIndentGuide-background: #404040; + --vscode-editorIndentGuide-activeBackground: #707070; + --vscode-editorLineNumber-foreground: #858585; + --vscode-editorActiveLineNumber-foreground: #c6c6c6; + --vscode-editorLineNumber-activeForeground: #c6c6c6; + --vscode-editorRuler-foreground: #5a5a5a; + --vscode-editorCodeLens-foreground: #999999; + --vscode-editorBracketMatch-background: rgba(0, 100, 0, 0.1); + --vscode-editorBracketMatch-border: #888888; + --vscode-editorOverviewRuler-border: rgba(127, 127, 127, 0.3); + --vscode-editorGutter-background: #1e1e1e; + --vscode-editorUnnecessaryCode-opacity: rgba(0, 0, 0, 0.67); + --vscode-editorOverviewRuler-rangeHighlightForeground: rgba(0, 122, 204, 0.6); + --vscode-editorOverviewRuler-errorForeground: rgba(255, 18, 18, 0.7); + --vscode-editorOverviewRuler-warningForeground: #cca700; + --vscode-editorOverviewRuler-infoForeground: #75beff; + --vscode-symbolIcon-arrayForeground: #cccccc; + --vscode-symbolIcon-booleanForeground: #cccccc; + --vscode-symbolIcon-classForeground: #ee9d28; + --vscode-symbolIcon-colorForeground: #cccccc; + --vscode-symbolIcon-constantForeground: #cccccc; + --vscode-symbolIcon-constructorForeground: #b180d7; + --vscode-symbolIcon-enumeratorForeground: #ee9d28; + --vscode-symbolIcon-enumeratorMemberForeground: #75beff; + --vscode-symbolIcon-eventForeground: #ee9d28; + --vscode-symbolIcon-fieldForeground: #75beff; + --vscode-symbolIcon-fileForeground: #cccccc; + --vscode-symbolIcon-folderForeground: #cccccc; + --vscode-symbolIcon-functionForeground: #b180d7; + --vscode-symbolIcon-interfaceForeground: #75beff; + --vscode-symbolIcon-keyForeground: #cccccc; + --vscode-symbolIcon-keywordForeground: #cccccc; + --vscode-symbolIcon-methodForeground: #b180d7; + --vscode-symbolIcon-moduleForeground: #cccccc; + --vscode-symbolIcon-namespaceForeground: #cccccc; + --vscode-symbolIcon-nullForeground: #cccccc; + --vscode-symbolIcon-numberForeground: #cccccc; + --vscode-symbolIcon-objectForeground: #cccccc; + --vscode-symbolIcon-operatorForeground: #cccccc; + --vscode-symbolIcon-packageForeground: #cccccc; + --vscode-symbolIcon-propertyForeground: #cccccc; + --vscode-symbolIcon-referenceForeground: #cccccc; + --vscode-symbolIcon-snippetForeground: #cccccc; + --vscode-symbolIcon-stringForeground: #cccccc; + --vscode-symbolIcon-structForeground: #cccccc; + --vscode-symbolIcon-textForeground: #cccccc; + --vscode-symbolIcon-typeParameterForeground: #cccccc; + --vscode-symbolIcon-unitForeground: #cccccc; + --vscode-symbolIcon-variableForeground: #75beff; + --vscode-editorOverviewRuler-bracketMatchForeground: #a0a0a0; + --vscode-editor-linkedEditingBackground: rgba(255, 0, 0, 0.3); + --vscode-editor-wordHighlightBackground: rgba(87, 87, 87, 0.72); + --vscode-editor-wordHighlightStrongBackground: rgba(0, 73, 114, 0.72); + --vscode-editorOverviewRuler-wordHighlightForeground: rgba(160, 160, 160, 0.8); + --vscode-editorOverviewRuler-wordHighlightStrongForeground: rgba(192, 160, 192, 0.8); + --vscode-editor-foldBackground: rgba(38, 79, 120, 0.3); + --vscode-editorGutter-foldingControlForeground: #c5c5c5; + --vscode-peekViewTitle-background: #1e1e1e; + --vscode-peekViewTitleLabel-foreground: #ffffff; + --vscode-peekViewTitleDescription-foreground: rgba(204, 204, 204, 0.7); + --vscode-peekView-border: #007acc; + --vscode-peekViewResult-background: #252526; + --vscode-peekViewResult-lineForeground: #bbbbbb; + --vscode-peekViewResult-fileForeground: #ffffff; + --vscode-peekViewResult-selectionBackground: rgba(51, 153, 255, 0.2); + --vscode-peekViewResult-selectionForeground: #ffffff; + --vscode-peekViewEditor-background: #001f33; + --vscode-peekViewEditorGutter-background: #001f33; + --vscode-peekViewResult-matchHighlightBackground: rgba(234, 92, 0, 0.3); + --vscode-peekViewEditor-matchHighlightBackground: rgba(255, 143, 0, 0.6); + --vscode-editorMarkerNavigationError-background: #f48771; + --vscode-editorMarkerNavigationWarning-background: #cca700; + --vscode-editorMarkerNavigationInfo-background: #75beff; + --vscode-editorMarkerNavigation-background: #2d2d30; + --vscode-editorSuggestWidget-background: #252526; + --vscode-editorSuggestWidget-border: #454545; + --vscode-editorSuggestWidget-foreground: #d4d4d4; + --vscode-editorSuggestWidget-selectedBackground: #062f4a; + --vscode-editorSuggestWidget-highlightForeground: #0097fb; + --vscode-tab-activeBackground: #1e1e1e; + --vscode-tab-unfocusedActiveBackground: #1e1e1e; + --vscode-tab-inactiveBackground: #2d2d2d; + --vscode-tab-unfocusedInactiveBackground: #2d2d2d; + --vscode-tab-activeForeground: #ffffff; + --vscode-tab-inactiveForeground: rgba(255, 255, 255, 0.5); + --vscode-tab-unfocusedActiveForeground: rgba(255, 255, 255, 0.5); + --vscode-tab-unfocusedInactiveForeground: rgba(255, 255, 255, 0.25); + --vscode-tab-border: #252526; + --vscode-tab-lastPinnedBorder: rgba(204, 204, 204, 0.2); + --vscode-tab-activeModifiedBorder: #3399cc; + --vscode-tab-inactiveModifiedBorder: rgba(51, 153, 204, 0.5); + --vscode-tab-unfocusedActiveModifiedBorder: rgba(51, 153, 204, 0.5); + --vscode-tab-unfocusedInactiveModifiedBorder: rgba(51, 153, 204, 0.25); + --vscode-editorPane-background: #1e1e1e; + --vscode-editorGroupHeader-tabsBackground: #252526; + --vscode-editorGroupHeader-noTabsBackground: #1e1e1e; + --vscode-editorGroup-border: #444444; + --vscode-editorGroup-dropBackground: rgba(83, 89, 93, 0.5); + --vscode-imagePreview-border: rgba(128, 128, 128, 0.35); + --vscode-panel-background: #1e1e1e; + --vscode-panel-border: rgba(128, 128, 128, 0.35); + --vscode-panelTitle-activeForeground: #e7e7e7; + --vscode-panelTitle-inactiveForeground: rgba(231, 231, 231, 0.6); + --vscode-panelTitle-activeBorder: #e7e7e7; + --vscode-panel-dropBorder: #e7e7e7; + --vscode-panelSection-dropBackground: rgba(83, 89, 93, 0.5); + --vscode-panelSectionHeader-background: rgba(128, 128, 128, 0.2); + --vscode-panelSection-border: rgba(128, 128, 128, 0.35); + --vscode-statusBar-foreground: #ffffff; + --vscode-statusBar-noFolderForeground: #ffffff; + --vscode-statusBar-background: #007acc; + --vscode-statusBar-noFolderBackground: #68217a; + --vscode-statusBarItem-activeBackground: rgba(255, 255, 255, 0.18); + --vscode-statusBarItem-hoverBackground: rgba(255, 255, 255, 0.12); + --vscode-statusBarItem-prominentForeground: #ffffff; + --vscode-statusBarItem-prominentBackground: rgba(0, 0, 0, 0.5); + --vscode-statusBarItem-prominentHoverBackground: rgba(0, 0, 0, 0.3); + --vscode-statusBarItem-errorBackground: #c72e0f; + --vscode-statusBarItem-errorForeground: #ffffff; + --vscode-activityBar-background: #333333; + --vscode-activityBar-foreground: #ffffff; + --vscode-activityBar-inactiveForeground: rgba(255, 255, 255, 0.4); + --vscode-activityBar-activeBorder: #ffffff; + --vscode-activityBar-dropBorder: #ffffff; + --vscode-activityBarBadge-background: #007acc; + --vscode-activityBarBadge-foreground: #ffffff; + --vscode-statusBarItem-remoteBackground: #16825d; + --vscode-statusBarItem-remoteForeground: #ffffff; + --vscode-extensionBadge-remoteBackground: #007acc; + --vscode-extensionBadge-remoteForeground: #ffffff; + --vscode-sideBar-background: #252526; + --vscode-sideBarTitle-foreground: #bbbbbb; + --vscode-sideBar-dropBackground: rgba(83, 89, 93, 0.5); + --vscode-sideBarSectionHeader-background: rgba(0, 0, 0, 0); + --vscode-sideBarSectionHeader-border: rgba(204, 204, 204, 0.2); + --vscode-titleBar-activeForeground: #cccccc; + --vscode-titleBar-inactiveForeground: rgba(204, 204, 204, 0.6); + --vscode-titleBar-activeBackground: #3c3c3c; + --vscode-titleBar-inactiveBackground: rgba(60, 60, 60, 0.6); + --vscode-menubar-selectionForeground: #cccccc; + --vscode-menubar-selectionBackground: rgba(255, 255, 255, 0.1); + --vscode-notifications-foreground: #cccccc; + --vscode-notifications-background: #252526; + --vscode-notificationLink-foreground: #3794ff; + --vscode-notificationCenterHeader-background: #303031; + --vscode-notifications-border: #303031; + --vscode-notificationsErrorIcon-foreground: #f48771; + --vscode-notificationsWarningIcon-foreground: #cca700; + --vscode-notificationsInfoIcon-foreground: #75beff; + --vscode-editorGutter-commentRangeForeground: #c5c5c5; + --vscode-debugToolBar-background: #333333; + --vscode-debugIcon-startForeground: #89d185; + --vscode-settings-headerForeground: #e7e7e7; + --vscode-settings-modifiedItemIndicator: #0c7d9d; + --vscode-settings-dropdownBackground: #3c3c3c; + --vscode-settings-dropdownForeground: #f0f0f0; + --vscode-settings-dropdownBorder: #3c3c3c; + --vscode-settings-dropdownListBorder: #454545; + --vscode-settings-checkboxBackground: #3c3c3c; + --vscode-settings-checkboxForeground: #f0f0f0; + --vscode-settings-checkboxBorder: #3c3c3c; + --vscode-settings-textInputBackground: #3c3c3c; + --vscode-settings-textInputForeground: #cccccc; + --vscode-settings-numberInputBackground: #3c3c3c; + --vscode-settings-numberInputForeground: #cccccc; + --vscode-settings-focusedRowBackground: rgba(128, 128, 128, 0.14); + --vscode-notebook-rowHoverBackground: rgba(128, 128, 128, 0.07); + --vscode-notebook-focusedRowBorder: rgba(255, 255, 255, 0.12); + --vscode-terminal-foreground: #cccccc; + --vscode-terminal-selectionBackground: rgba(255, 255, 255, 0.25); + --vscode-terminal-border: rgba(128, 128, 128, 0.35); + --vscode-testing-iconFailed: #f14c4c; + --vscode-testing-iconErrored: #f14c4c; + --vscode-testing-iconPassed: #73c991; + --vscode-testing-runAction: #73c991; + --vscode-testing-iconQueued: #cca700; + --vscode-testing-iconUnset: #848484; + --vscode-testing-iconSkipped: #848484; + --vscode-testing-peekBorder: #f48771; + --vscode-testing-message\.error\.decorationForeground: #f48771; + --vscode-testing-message\.error\.lineBackground: rgba(255, 0, 0, 0.2); + --vscode-testing-message\.warning\.decorationForeground: #cca700; + --vscode-testing-message\.warning\.lineBackground: rgba(255, 208, 0, 0.2); + --vscode-testing-message\.info\.decorationForeground: #75beff; + --vscode-testing-message\.info\.lineBackground: rgba(0, 127, 255, 0.2); + --vscode-testing-message\.hint\.decorationForeground: rgba(238, 238, 238, 0.7); + --vscode-welcomePage-tileBackground: #252526; + --vscode-welcomePage-tileHoverBackground: #2c2c2d; + --vscode-welcomePage-tileShadow\.: rgba(0, 0, 0, 0.36); + --vscode-welcomePage-progress\.background: #3c3c3c; + --vscode-welcomePage-progress\.foreground: #3794ff; + --vscode-workspaceTrust-trustedForegound: #89d185; + --vscode-workspaceTrust-untrustedForeground: #f48771; + --vscode-workspaceTrust-tileBackground: #252526; + --vscode-statusBar-debuggingBackground: #cc6633; + --vscode-statusBar-debuggingForeground: #ffffff; + --vscode-debugExceptionWidget-border: #a31515; + --vscode-debugExceptionWidget-background: #420b0d; + --vscode-editorGutter-modifiedBackground: #0c7d9d; + --vscode-editorGutter-addedBackground: #587c0c; + --vscode-editorGutter-deletedBackground: #94151b; + --vscode-minimapGutter-modifiedBackground: #0c7d9d; + --vscode-minimapGutter-addedBackground: #587c0c; + --vscode-minimapGutter-deletedBackground: #94151b; + --vscode-editorOverviewRuler-modifiedForeground: rgba(12, 125, 157, 0.6); + --vscode-editorOverviewRuler-addedForeground: rgba(88, 124, 12, 0.6); + --vscode-editorOverviewRuler-deletedForeground: rgba(148, 21, 27, 0.6); + --vscode-notebook-cellBorderColor: #37373d; + --vscode-notebook-focusedEditorBorder: #007fd4; + --vscode-notebookStatusSuccessIcon-foreground: #89d185; + --vscode-notebookStatusErrorIcon-foreground: #f48771; + --vscode-notebookStatusRunningIcon-foreground: #cccccc; + --vscode-notebook-outputContainerBackgroundColor: #37373d; + --vscode-notebook-cellToolbarSeparator: rgba(128, 128, 128, 0.35); + --vscode-notebook-selectedCellBackground: #37373d; + --vscode-notebook-selectedCellBorder: #37373d; + --vscode-notebook-focusedCellBorder: #007fd4; + --vscode-notebook-inactiveFocusedCellBorder: #37373d; + --vscode-notebook-cellStatusBarItemHoverBackground: rgba(255, 255, 255, 0.15); + --vscode-notebook-cellInsertionIndicator: #007fd4; + --vscode-notebookScrollbarSlider-background: rgba(121, 121, 121, 0.4); + --vscode-notebookScrollbarSlider-hoverBackground: rgba(100, 100, 100, 0.7); + --vscode-notebookScrollbarSlider-activeBackground: rgba(191, 191, 191, 0.4); + --vscode-notebook-symbolHighlightBackground: rgba(255, 255, 255, 0.04); + --vscode-editor-stackFrameHighlightBackground: rgba(255, 255, 0, 0.2); + --vscode-editor-focusedStackFrameHighlightBackground: rgba(122, 189, 122, 0.3); + --vscode-debugIcon-breakpointForeground: #e51400; + --vscode-debugIcon-breakpointDisabledForeground: #848484; + --vscode-debugIcon-breakpointUnverifiedForeground: #848484; + --vscode-debugIcon-breakpointCurrentStackframeForeground: #ffcc00; + --vscode-debugIcon-breakpointStackframeForeground: #89d185; + --vscode-scm-providerBorder: #454545; + --vscode-extensionButton-prominentBackground: #0e639c; + --vscode-extensionButton-prominentForeground: #ffffff; + --vscode-extensionButton-prominentHoverBackground: #1177bb; + --vscode-extensionIcon-starForeground: #ff8e00; + --vscode-terminal-ansiBlack: #000000; + --vscode-terminal-ansiRed: #cd3131; + --vscode-terminal-ansiGreen: #0dbc79; + --vscode-terminal-ansiYellow: #e5e510; + --vscode-terminal-ansiBlue: #2472c8; + --vscode-terminal-ansiMagenta: #bc3fbc; + --vscode-terminal-ansiCyan: #11a8cd; + --vscode-terminal-ansiWhite: #e5e5e5; + --vscode-terminal-ansiBrightBlack: #666666; + --vscode-terminal-ansiBrightRed: #f14c4c; + --vscode-terminal-ansiBrightGreen: #23d18b; + --vscode-terminal-ansiBrightYellow: #f5f543; + --vscode-terminal-ansiBrightBlue: #3b8eea; + --vscode-terminal-ansiBrightMagenta: #d670d6; + --vscode-terminal-ansiBrightCyan: #29b8db; + --vscode-terminal-ansiBrightWhite: #e5e5e5; + --vscode-debugTokenExpression-name: #c586c0; + --vscode-debugTokenExpression-value: rgba(204, 204, 204, 0.6); + --vscode-debugTokenExpression-string: #ce9178; + --vscode-debugTokenExpression-boolean: #4e94ce; + --vscode-debugTokenExpression-number: #b5cea8; + --vscode-debugTokenExpression-error: #f48771; + --vscode-debugView-exceptionLabelForeground: #cccccc; + --vscode-debugView-exceptionLabelBackground: #6c2022; + --vscode-debugView-stateLabelForeground: #cccccc; + --vscode-debugView-stateLabelBackground: rgba(136, 136, 136, 0.27); + --vscode-debugView-valueChangedHighlight: #569cd6; + --vscode-debugConsole-infoForeground: #75beff; + --vscode-debugConsole-warningForeground: #cca700; + --vscode-debugConsole-errorForeground: #f48771; + --vscode-debugConsole-sourceForeground: #cccccc; + --vscode-debugConsoleInputIcon-foreground: #cccccc; + --vscode-debugIcon-pauseForeground: #75beff; + --vscode-debugIcon-stopForeground: #f48771; + --vscode-debugIcon-disconnectForeground: #f48771; + --vscode-debugIcon-restartForeground: #89d185; + --vscode-debugIcon-stepOverForeground: #75beff; + --vscode-debugIcon-stepIntoForeground: #75beff; + --vscode-debugIcon-stepOutForeground: #75beff; + --vscode-debugIcon-continueForeground: #75beff; + --vscode-debugIcon-stepBackForeground: #75beff; + --vscode-gitDecoration-addedResourceForeground: #81b88b; + --vscode-gitDecoration-modifiedResourceForeground: #e2c08d; + --vscode-gitDecoration-deletedResourceForeground: #c74e39; + --vscode-gitDecoration-renamedResourceForeground: #73c991; + --vscode-gitDecoration-untrackedResourceForeground: #73c991; + --vscode-gitDecoration-ignoredResourceForeground: #8c8c8c; + --vscode-gitDecoration-stageModifiedResourceForeground: #e2c08d; + --vscode-gitDecoration-stageDeletedResourceForeground: #c74e39; + --vscode-gitDecoration-conflictingResourceForeground: #e4676b; + --vscode-gitDecoration-submoduleResourceForeground: #8db9e2; + --vscode-bookmarks-lineBackground: rgba(0, 0, 0, 0); + --vscode-bookmarks-lineBorder: rgba(0, 0, 0, 0); + --vscode-bookmarks-overviewRuler: rgba(21, 126, 251, 0.53); + --vscode-gitlens-gutterBackgroundColor: rgba(255, 255, 255, 0.07); + --vscode-gitlens-gutterForegroundColor: #bebebe; + --vscode-gitlens-gutterUncommittedForegroundColor: rgba(0, 188, 242, 0.6); + --vscode-gitlens-trailingLineBackgroundColor: rgba(0, 0, 0, 0); + --vscode-gitlens-trailingLineForegroundColor: rgba(153, 153, 153, 0.35); + --vscode-gitlens-lineHighlightBackgroundColor: rgba(0, 188, 242, 0.2); + --vscode-gitlens-lineHighlightOverviewRulerColor: rgba(0, 188, 242, 0.6); + --vscode-gitlens-closedPullRequestIconColor: #f85149; + --vscode-gitlens-openPullRequestIconColor: #56d364; + --vscode-gitlens-mergedPullRequestIconColor: #995dff; + --vscode-gitlens-unpushlishedChangesIconColor: #35b15e; + --vscode-gitlens-unpublishedCommitIconColor: #35b15e; + --vscode-gitlens-unpulledChangesIconColor: #b15e35; + --vscode-gitlens-decorations\.addedForegroundColor: #81b88b; + --vscode-gitlens-decorations\.copiedForegroundColor: #73c991; + --vscode-gitlens-decorations\.deletedForegroundColor: #c74e39; + --vscode-gitlens-decorations\.ignoredForegroundColor: #8c8c8c; + --vscode-gitlens-decorations\.modifiedForegroundColor: #e2c08d; + --vscode-gitlens-decorations\.untrackedForegroundColor: #73c991; + --vscode-gitlens-decorations\.renamedForegroundColor: #73c991; + --vscode-gitlens-decorations\.branchAheadForegroundColor: #35b15e; + --vscode-gitlens-decorations\.branchBehindForegroundColor: #b15e35; + --vscode-gitlens-decorations\.branchDivergedForegroundColor: #d8af1b; + --vscode-gitlens-decorations\.branchUnpublishedForegroundColor: #35b15e; + --vscode-gitlens-decorations\.branchMissingUpstreamForegroundColor: #c74e39; +} diff --git a/src/plotViewer/webviewMessages.ts b/src/plotViewer/webviewMessages.ts new file mode 100644 index 000000000..2cdf8a6ba --- /dev/null +++ b/src/plotViewer/webviewMessages.ts @@ -0,0 +1,66 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export interface VsCode { + postMessage: (msg: OutMessage) => void; + setState: (state: string) => void; +} +/** + * Function declared by VS Code in Webview + */ +export const acquireVsCodeApi: () => VsCode = (globalThis as { acquireVsCodeApi?: () => VsCode }).acquireVsCodeApi || (() => ({} as VsCode)); + +export interface IMessage { + message: string; +} + +export interface ResizeMessage extends IMessage { + message: 'resize', + height: number, + width: number, + userTriggered: boolean +} +export interface LogMessage extends IMessage { + message: 'log', + body: any +} + +export type OutMessage = ResizeMessage | LogMessage; + +export interface UpdatePlotMessage extends IMessage { + message: 'updatePlot', + svg: string, + plotId: string +} + +export interface FocusPlotMessage extends IMessage { + message: 'focusPlot', + plotId: string +} + +export interface ToggleStyleMessage extends IMessage { + message: 'toggleStyle', + useOverwrites: boolean +} + +export interface ToggleFullWindowMessage extends IMessage { + message: 'toggleFullWindow', + useFullWindow: boolean +} + +export type PreviewPlotLayout = 'multirow' | 'scroll' | 'hidden'; +export interface PreviewPlotLayoutMessage extends IMessage { + message: 'togglePreviewPlotLayout', + style: PreviewPlotLayout +} + +export interface HidePlotMessage extends IMessage { + message: 'hidePlot', + plotId: string +} + +export interface AddPlotMessage extends IMessage { + message: 'addPlot', + html: string +} + +export type InMessage = UpdatePlotMessage | FocusPlotMessage | ToggleStyleMessage | HidePlotMessage | AddPlotMessage | PreviewPlotLayoutMessage | ToggleFullWindowMessage; diff --git a/src/rTerminal.ts b/src/rTerminal.ts index a71ee7e53..13085616e 100644 --- a/src/rTerminal.ts +++ b/src/rTerminal.ts @@ -5,16 +5,81 @@ import { isDeepStrictEqual } from 'util'; import * as vscode from 'vscode'; -import { extensionContext, homeExtDir } from './extension'; +import { extensionContext } from './extension'; import * as util from './util'; import * as selection from './selection'; import { getSelection } from './selection'; import { cleanupSession } from './session'; import { config, delay, getRterm, getCurrentWorkspaceFolder } from './util'; -import { rGuestService, isGuestSession } from './liveShare'; import * as fs from 'fs'; +import * as yaml from 'js-yaml'; + export let rTerm: vscode.Terminal | undefined = undefined; +let lastParamsRmdPath: string | undefined; +let lastParamsRmdVersion: number | undefined; + +const rExprType = new yaml.Type('!r', { + kind: 'scalar', + construct: (data: string) => ({ __rExpr: data }), +}); +const RMARKDOWN_SCHEMA = yaml.DEFAULT_SCHEMA.extend([rExprType]); + +function valueToR(val: unknown): string { + if (val === null || val === undefined) { + return 'NULL'; + } + if (typeof val === 'boolean') { + return val ? 'TRUE' : 'FALSE'; + } + if (typeof val === 'number') { + return String(val); + } + if (typeof val === 'string') { + return `"${val.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; + } + if (typeof val === 'object' && val !== null && '__rExpr' in (val as Record)) { + return (val as { __rExpr: string }).__rExpr; + } + if (Array.isArray(val)) { + return `c(${val.map(valueToR).join(', ')})`; + } + const obj = val as Record; + if ('value' in obj) { + return valueToR(obj['value']); + } + const entries = Object.entries(obj).map(([k, v]) => `${k} = ${valueToR(v)}`); + return `list(${entries.join(', ')})`; +} + +export function getRmdParamsCommand(document: vscode.TextDocument): string | undefined { + if (document.languageId !== 'rmd') { + return undefined; + } + const text = document.getText(); + const match = text.match(/^---\s*\n([\s\S]*?)\n---/); + if (!match || !/^\s*params\s*:/m.test(match[1])) { + return undefined; + } + const filePath = document.uri.fsPath; + if (filePath === lastParamsRmdPath && document.version === lastParamsRmdVersion) { + return undefined; + } + lastParamsRmdPath = filePath; + lastParamsRmdVersion = document.version; + try { + const frontmatter = yaml.load(match[1], { schema: RMARKDOWN_SCHEMA }) as Record; + const params = frontmatter?.['params'] as Record | undefined; + if (!params || typeof params !== 'object') { + return undefined; + } + const entries = Object.entries(params).map(([k, v]) => `${k} = ${valueToR(v)}`); + return `params <- list(${entries.join(', ')})`; + } catch { + return undefined; + } +} + export async function runSource(echo: boolean): Promise { const wad = vscode.window.activeTextEditor?.document; if (!wad) { @@ -114,6 +179,8 @@ export async function runFromLineToEnd(): Promise { await runTextInTerm(text); } +import { getGlobalSessionServer, writeSessionFile } from './session'; + export async function makeTerminalOptions(): Promise { const workspaceFolderPath = getCurrentWorkspaceFolder()?.uri.fsPath; const termPath = await getRterm(); @@ -124,14 +191,16 @@ export async function makeTerminalOptions(): Promise { shellArgs: shellArgs, cwd: workspaceFolderPath, }; - const newRprofile = extensionContext.asAbsolutePath(path.join('R', 'session', 'profile.R')); - const initR = extensionContext.asAbsolutePath(path.join('R', 'session','init.R')); + const newRprofile = extensionContext.asAbsolutePath(path.join('R', 'profile.R')); if (config().get('sessionWatcher')) { + const { port, token } = await getGlobalSessionServer(); termOptions.env = { R_PROFILE_USER_OLD: process.env.R_PROFILE_USER, R_PROFILE_USER: newRprofile, - VSCODE_INIT_R: initR, - VSCODE_WATCHER_DIR: homeExtDir() + SESS_PORT: port.toString(), + SESS_TOKEN: token, + SESS_RSTUDIOAPI: config().get('session.emulateRStudioAPI') ? 'TRUE' : 'FALSE', + SESS_USE_HTTPGD: config().get('plot.useHttpgd') ? 'TRUE' : 'FALSE' }; } return termOptions; @@ -139,6 +208,7 @@ export async function makeTerminalOptions(): Promise { export async function createRTerm(preserveshow?: boolean): Promise { const termOptions = await makeTerminalOptions(); + void util.promptToInstallSessPackage(termOptions.cwd); const termPath = termOptions.shellPath; if(!termPath){ void vscode.window.showErrorMessage('Could not find R path. Please check r.rterm and r.rpath setting.'); @@ -149,6 +219,14 @@ export async function createRTerm(preserveshow?: boolean): Promise { } rTerm = vscode.window.createTerminal(termOptions); rTerm.show(preserveshow); + + void rTerm.processId.then(async (pid) => { + if (pid) { + const { port, token } = await getGlobalSessionServer(); + await writeSessionFile(pid.toString(), port, token); + } + }); + return true; } @@ -228,8 +306,8 @@ export async function runSelectionInTerm(moveCursor: boolean, useRepl = true): P if (!selection) { return; } + const textEditor = vscode.window.activeTextEditor; if (moveCursor && selection.linesDownToMoveCursor > 0) { - const textEditor = vscode.window.activeTextEditor; if (!textEditor) { return; } @@ -244,7 +322,8 @@ export async function runSelectionInTerm(moveCursor: boolean, useRepl = true): P if(useRepl && vscode.debug.activeDebugSession?.type === 'R-Debugger'){ await sendRangeToRepl(selection.range); } else{ - await runTextInTerm(selection.selectedText); + const paramsCmd = textEditor ? getRmdParamsCommand(textEditor.document) : undefined; + await runTextInTerm(paramsCmd ? `${paramsCmd}\n${selection.selectedText}` : selection.selectedText); } } @@ -253,48 +332,45 @@ export async function runChunksInTerm(chunks: vscode.Range[]): Promise { if (!textEditor) { return; } + const paramsCmd = getRmdParamsCommand(textEditor.document); const text = chunks .map((chunk) => textEditor.document.getText(chunk).trim()) .filter((chunk) => chunk.length > 0) .join('\n'); if (text.length > 0) { - return runTextInTerm(text); + return runTextInTerm(paramsCmd ? `${paramsCmd}\n${text}` : text); } } export async function runTextInTerm(text: string, execute: boolean = true): Promise { - if (isGuestSession) { - rGuestService?.requestRunTextInTerm(text); + const term = await chooseTerminal(); + if (term === undefined) { + return; + } + if (config().get('bracketedPaste')) { + // Surround with ANSI control characters for bracketed paste mode + text = `\x1b[200~${text}\x1b[201~`; + term.sendText(text, execute); } else { - const term = await chooseTerminal(); - if (term === undefined) { - return; - } - if (config().get('bracketedPaste')) { - // Surround with ANSI control characters for bracketed paste mode - text = `\x1b[200~${text}\x1b[201~`; - term.sendText(text, execute); - } else { - const rtermSendDelay: number = config().get('rtermSendDelay') || 8; - const split = text.split('\n'); - const last_split = split.length - 1; - for (const [count, line] of split.entries()) { - if (count > 0) { - await delay(rtermSendDelay); // Increase delay if RTerm can't handle speed. - } + const rtermSendDelay: number = config().get('rtermSendDelay') || 8; + const split = text.split('\n'); + const last_split = split.length - 1; + for (const [count, line] of split.entries()) { + if (count > 0) { + await delay(rtermSendDelay); // Increase delay if RTerm can't handle speed. + } - // Avoid sending newline on last line - if (count === last_split && !execute) { - term.sendText(line, false); - } else { - term.sendText(line); - } + // Avoid sending newline on last line + if (count === last_split && !execute) { + term.sendText(line, false); + } else { + term.sendText(line); } } - setFocus(term); - // Scroll console to see latest output - await vscode.commands.executeCommand('workbench.action.terminal.scrollToBottom'); } + setFocus(term); + // Scroll console to see latest output + await vscode.commands.executeCommand('workbench.action.terminal.scrollToBottom'); } function setFocus(term: vscode.Terminal) { diff --git a/src/rstudioapi.ts b/src/rstudioapi.ts index ac4d738e5..5ce1cbf60 100644 --- a/src/rstudioapi.ts +++ b/src/rstudioapi.ts @@ -1,11 +1,5 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-plus-operands */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +'use strict'; + import { window, TextEditor, TextDocument, Uri, workspace, WorkspaceEdit, Position, Range, Selection, @@ -13,110 +7,96 @@ import { } from 'vscode'; import { readJSON } from 'fs-extra'; import * as path from 'path'; -import { sessionDir, sessionDirectoryExists, writeResponse, writeSuccessResponse } from './session'; -import { runTextInTerm, restartRTerminal, chooseTerminal } from './rTerminal'; +import { sessionDir, sessionDirectoryExists } from './session'; +import { runTextInTerm, chooseTerminal } from './rTerminal'; import { config } from './util'; let lastActiveTextEditor: TextEditor; -export async function dispatchRStudioAPICall(action: string, args: any, sd: string): Promise { +// Types for rstudioapi +export type RSCoord = number | 'Inf' | '-Inf'; - switch (action) { - case 'active_editor_context': { - await writeResponse(activeEditorContext(), sd); - break; - } - case 'insert_or_modify_text': { - await insertOrModifyText(args.query, args.id); - await writeSuccessResponse(sd); - break; - } - case 'replace_text_in_current_selection': { - await replaceTextInCurrentSelection(args.text, args.id); - await writeSuccessResponse(sd); - break; - } - case 'show_dialog': { - showDialog(args.message); - await writeSuccessResponse(sd); - break; - } - case 'navigate_to_file': { - await navigateToFile(args.file, args.line, args.column); - await writeSuccessResponse(sd); - break; - } - case 'set_selection_ranges': { - await setSelections(args.ranges, args.id); - await writeSuccessResponse(sd); - break; - } - case 'document_save': { - await documentSave(args.id); - await writeSuccessResponse(sd); - break; - } - case 'document_save_all': { - await documentSaveAll(); - await writeSuccessResponse(sd); - break; - } - case 'get_project_path': { - await writeResponse(projectPath(), sd); - break; - } - case 'document_context': { - await writeResponse(await documentContext(args.id), sd); - break; - } - case 'document_new': { - await documentNew(args.text, args.type, args.position); - await writeSuccessResponse(sd); - break; - } - case 'restart_r': { - await restartRTerminal(); - await writeSuccessResponse(sd); - break; - } - case 'send_to_console': { - await sendCodeToRTerminal(args.code, args.execute, args.focus); - await writeSuccessResponse(sd); - break; - } - default: - console.error(`[dispatchRStudioAPICall] Unsupported action: ${action}`); - } +export interface RSPosition { + [index: number]: RSCoord; + length: number; +} +export interface RSRange { + start: RSPosition; + end: RSPosition; } +export interface RSEditOperation { + operation: 'insertText' | 'modifyRange'; + text: string; + location: RSPosition | RSRange; +} + +interface RSSelection { + start: { line: number; character: number }; + end: { line: number; character: number }; +} + +interface RSDocumentContext { + id: { external: string }; + contents: string; + path: string; + selection: RSSelection[]; +} + +// dispatchRStudioAPICall removed + //rstudioapi -export function activeEditorContext() { +export function activeEditorContext(): RSDocumentContext { // info returned from RStudio: // list with: // id // path // contents // selection - a list of selections - const currentDocument = getLastActiveTextEditor().document; + const currentEditor = getLastActiveTextEditor(); + const currentDocument = currentEditor.document; return { - id: currentDocument.uri, + id: { external: currentDocument.uri.toString() }, contents: currentDocument.getText(), path: currentDocument.fileName, - selection: getLastActiveTextEditor().selections + selection: currentEditor.selections.map(s => ({ + start: { line: s.start.line + 1, character: s.start.character + 1 }, + end: { line: s.end.line + 1, character: s.end.character + 1 } + })) }; } -export async function documentContext(id: string) { +export async function documentContext(id: string | null): Promise { const target = findTargetUri(id); const targetDocument = await workspace.openTextDocument(target); console.info(`[documentContext] getting context for: ${target.path}`); + + let selections: RSSelection[] = []; + const knownEditors = [getLastActiveTextEditor(), ...window.visibleTextEditors]; + const editor = knownEditors.find(e => e?.document?.uri.toString() === targetDocument.uri.toString()); + + if (editor) { + selections = editor.selections.map(s => ({ + start: { line: s.start.line + 1, character: s.start.character + 1 }, + end: { line: s.end.line + 1, character: s.end.character + 1 } + })); + } else { + selections = [{ + start: { line: 1, character: 1 }, + end: { line: 1, character: 1 } + }]; + } + return { - id: targetDocument.uri + id: { external: targetDocument.uri.toString() }, + contents: targetDocument.getText(), + path: targetDocument.fileName, + selection: selections }; } -export async function insertOrModifyText(query: any[], id: string | null = null) { +export async function insertOrModifyText(query: RSEditOperation[], id: string | null = null): Promise { const target = findTargetUri(id); @@ -127,16 +107,16 @@ export async function insertOrModifyText(query: any[], id: string | null = null) query.forEach((op) => { assertSupportedEditOperation(op.operation); - let editLocation: any; + let editLocation: Position | Range; const editText = normaliseEditText(op.text, op.location, op.operation, targetDocument); if (op.operation === 'insertText') { - editLocation = parsePosition(op.location, targetDocument); + editLocation = parsePosition(op.location as RSPosition, targetDocument); console.info(`[insertTextAtPosition] inserting at: ${JSON.stringify(editLocation)}`); console.info(`[insertTextAtPosition] inserting text: ${editText}`); edit.insert(target, editLocation, editText); } else { - editLocation = parseRange(op.location, targetDocument); + editLocation = parseRange(op.location as RSRange, targetDocument); console.info(`[insertTextAtPosition] replacing at: ${JSON.stringify(editLocation)}`); console.info(`[insertTextAtPosition] replacing with text: ${editText}`); edit.replace(target, editLocation, editText); @@ -146,7 +126,7 @@ export async function insertOrModifyText(query: any[], id: string | null = null) void workspace.applyEdit(edit); } -export async function replaceTextInCurrentSelection(text: string, id: string): Promise { +export async function replaceTextInCurrentSelection(text: string, id: string | null): Promise { const target = findTargetUri(id); console.info(`[replaceTextInCurrentSelection] inserting: ${text} into ${target.path}`); const edit = new WorkspaceEdit(); @@ -164,6 +144,27 @@ export function showDialog(message: string): void { } +export async function showPrompt( + title: string, message: string, defaultValue?: string +): Promise<{ response: string | null }> { + const result = await window.showInputBox({ + title: title, + prompt: message, + value: defaultValue ?? '', + }); + return { response: result ?? null }; +} + +export async function askForPassword( + prompt: string +): Promise<{ response: string | null }> { + const result = await window.showInputBox({ + prompt: prompt, + password: true, + }); + return { response: result ?? null }; +} + export async function navigateToFile(file: string, line: number, column: number): Promise{ const targetDocument = await workspace.openTextDocument(Uri.file(file)); @@ -173,7 +174,7 @@ export async function navigateToFile(file: string, line: number, column: number) editor.revealRange(new Range(targetPosition, targetPosition)); } -export async function setSelections(ranges: number[][], id: string): Promise { +export async function setSelections(ranges: RSRange[], id: string | null): Promise { // Setting selections can only be done on TextEditors not TextDocuments, but // it is the latter which are the things actually referred to by `id`. In // VSCode it's not possible to get a list of the open text editors. it is not @@ -208,7 +209,7 @@ export async function setSelections(ranges: number[][], id: string): Promise { +export async function documentSave(id: string | null): Promise { const target = findTargetUri(id); const targetDocument = await workspace.openTextDocument(target); await targetDocument.save(); @@ -218,6 +219,17 @@ export async function documentSaveAll(): Promise { await workspace.saveAll(); } +export async function documentClose(id: string | null, save: boolean): Promise { + const target = findTargetUri(id); + if (save) { + const targetDocument = await workspace.openTextDocument(target); + await targetDocument.save(); + } + const tabs = window.tabGroups.all.flatMap(g => g.tabs); + const targetTabs = tabs.filter(t => (t.input as { uri?: Uri })?.uri?.toString() === target.toString()); + await window.tabGroups.close(targetTabs); +} + // TODO: very similar to ./utils.getCurrentWorkspaceFolder() export function projectPath(): { path: string | undefined; } { @@ -287,12 +299,19 @@ interface AddinItem extends QuickPickItem { let addinQuickPicks: AddinItem[] | undefined = undefined; +interface RawAddin { + package: string; + name: string; + description: string; + binding: string; +} + export async function getAddinPickerItems(): Promise { if (typeof addinQuickPicks === 'undefined') { - const addins: any[] = await readJSON(path.join(sessionDir, 'addins.json')). + const addins: RawAddin[] = await readJSON(path.join(sessionDir, 'addins.json')). then( - (result) => result, + (result: RawAddin[]) => result, () => { throw ('Could not find list of installed addins.' + ' options(vsc.rstudioapi = TRUE) must be set in your .Rprofile to use ' + @@ -364,7 +383,7 @@ export async function sendCodeToRTerminal(code: string, execute: boolean, focus: } //utils -function toVSCCoord(coord: any) { +function toVSCCoord(coord: RSCoord) { // this is necessary because RStudio will accept negative or infinite values, // replacing them with the min or max or the document. // These must be clamped non-negative integers accepted by VSCode. @@ -375,7 +394,7 @@ function toVSCCoord(coord: any) { coord_value = 10000000; } else if (coord === '-Inf') { coord_value = 0; - } else if (coord <= 0) { + } else if (typeof coord === 'number' && coord <= 0) { coord_value = 0; } else { // coord > 0 @@ -386,7 +405,7 @@ function toVSCCoord(coord: any) { } -function parsePosition(rs_position: any[], targetDocument: TextDocument) { +function parsePosition(rs_position: RSPosition, targetDocument: TextDocument) { if (rs_position.length !== 2) { throw ('an rstudioapi position must be an array of 2 numbers'); } @@ -396,7 +415,7 @@ function parsePosition(rs_position: any[], targetDocument: TextDocument) { )); } -function parseRange(rs_range: any, targetDocument: TextDocument) { +function parseRange(rs_range: RSRange, targetDocument: TextDocument) { if (rs_range.start.length !== 2 || rs_range.end.length !== 2) { throw ('an rstudioapi range must be an object containing two numeric arrays'); } @@ -415,16 +434,16 @@ function assertSupportedEditOperation(operation: string) { } } -function normaliseEditText(text: string, editLocation: any, +function normaliseEditText(text: string, editLocation: RSPosition | RSRange, operation: string, targetDocument: TextDocument) { // in a document with lines, does the line position extend past the existing // lines in the document? rstudioapi adds a newline in this case, so must we. // n_lines is a count, line is 0 indexed position hence + 1 const editStartLine = operation === 'insertText' ? - editLocation[0] : - editLocation.start[0]; + (editLocation as RSPosition)[0] : + (editLocation as RSRange).start[0]; if (editStartLine === 'Inf' || - (editStartLine + 1 > targetDocument.lineCount && targetDocument.lineCount > 0)) { + (typeof editStartLine === 'number' && editStartLine + 1 > targetDocument.lineCount && targetDocument.lineCount > 0)) { return (text + '\n'); } else { return text; diff --git a/src/session.ts b/src/session.ts index b5959019a..90f8ab622 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,20 +1,33 @@ 'use strict'; import * as fs from 'fs-extra'; -import * as os from 'os'; import * as path from 'path'; -import { Agent } from 'http'; -import fetch from 'node-fetch'; -import { commands, StatusBarItem, Uri, ViewColumn, Webview, window, workspace, env, WebviewPanelOnDidChangeViewStateEvent, WebviewPanel } from 'vscode'; +import * as os from 'os'; +import * as vscode from 'vscode'; +import { commands, Uri, ViewColumn, Webview, window, workspace, env } from 'vscode'; -import { runTextInTerm } from './rTerminal'; -import { FSWatcher } from 'fs-extra'; +import { restartRTerminal } from './rTerminal'; import { config, readContent, setContext, UriIcon } from './util'; -import { purgeAddinPickerItems, dispatchRStudioAPICall } from './rstudioapi'; +import * as rTerminal from './rTerminal'; +import { purgeAddinPickerItems, RSEditOperation, RSRange } from './rstudioapi'; + +import { homeExtDir, rWorkspace, globalRHelp, globalPlotManager, sessionStatusBarItem } from './extension'; + +import { showWebView } from './webViewer'; + +import WebSocket from 'ws'; + +export interface SessionInfo { + version: string; + command: string; + start_time: string; +} -import { IRequest } from './liveShare/shareSession'; -import { homeExtDir, rWorkspace, globalRHelp, globalHttpgdManager, extensionContext, sessionStatusBarItem } from './extension'; -import { UUID, rHostService, rGuestService, isLiveShare, isHost, isGuestSession, closeBrowser, guestResDir, shareBrowser, openVirtualDoc, shareWorkspace } from './liveShare'; +interface ExtWebSocket extends WebSocket { + _terminalPid?: number; + _port?: number; + _token?: string; +} export interface GlobalEnv { [key: string]: { @@ -41,39 +54,55 @@ export interface SessionServer { token: string; } +export class Session { + public server: SessionServer; + public ws: WebSocket; + public pid: string; + public rVer: string; + public info: SessionInfo; + public sessionDir: string; + public workingDir: string; + public workspaceData: WorkspaceData; + + constructor(server: SessionServer, ws: WebSocket) { + this.server = server; + this.ws = ws; + this.pid = ''; + this.rVer = ''; + this.info = { version: '', command: '', start_time: '' }; + this.sessionDir = ''; + this.workingDir = ''; + this.workspaceData = { search: [], loaded_namespaces: [], globalenv: {} }; + } +} + export let workspaceData: WorkspaceData; let resDir: string; export let requestFile: string; export let requestLockFile: string; -let requestTimeStamp: number; -let responseTimeStamp: number; export let sessionDir: string; export let workingDir: string; let rVer: string; let pid: string; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let info: any; -const httpAgent = new Agent({ keepAlive: true }); +let info: SessionInfo; export let server: SessionServer | undefined; export let workspaceFile: string; -let workspaceLockFile: string; -let workspaceTimeStamp: number; -let plotFile: string; -let plotLockFile: string; -let plotTimeStamp: number; -let workspaceWatcher: FSWatcher; -let plotWatcher: FSWatcher; -let activeBrowserPanel: WebviewPanel | undefined; + +const sessions = new Map(); +export let activeSession: Session | undefined; let activeBrowserUri: Uri | undefined; -let activeBrowserExternalUri: Uri | undefined; export function deploySessionWatcher(extensionPath: string): void { console.info(`[deploySessionWatcher] extensionPath: ${extensionPath}`); resDir = path.join(extensionPath, 'dist', 'resources'); - const initPath = path.join(extensionPath, 'R', 'session', 'init.R'); - const linkPath = path.join(homeExtDir(), 'init.R'); - fs.writeFileSync(linkPath, `local(source("${initPath.replace(/\\/g, '\\\\')}", chdir = TRUE, local = TRUE))\n`); + // Initialize the WebSocket server when the extension activates + void getGlobalSessionServer().then(async (srv) => { + await pruneSessionFiles(); + await updateActiveTerminalFiles(srv.port, srv.token); + }).catch(err => { + console.error('Failed to initialize global session server', err); + }); writeSettings(); workspace.onDidChangeConfiguration(event => { @@ -83,28 +112,180 @@ export function deploySessionWatcher(extensionPath: string): void { }); } -export function startRequestWatcher(sessionStatusBarItem: StatusBarItem): void { - console.info('[startRequestWatcher] Starting'); - requestFile = path.join(homeExtDir(), 'request.log'); - requestLockFile = path.join(homeExtDir(), 'request.lock'); - requestTimeStamp = 0; - responseTimeStamp = 0; - if (!fs.existsSync(requestLockFile)) { - fs.createFileSync(requestLockFile); +import * as crypto from 'crypto'; + +let wsClient: ExtWebSocket | undefined; +export const activeConnections = new Set(); + +const pendingRequests = new Map void, reject: (reason?: unknown) => void }>(); + +let globalSessionServer: { port: number, token: string } | undefined; + +function isPidRunning(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch (e) { + return (e as NodeJS.ErrnoException).code !== 'ESRCH'; + } +} + +async function pruneSessionFiles() { + const homeDir = os.homedir(); + const sessionsDir = path.join(homeDir, '.vscode-R', 'sessions'); + if (!await fs.pathExists(sessionsDir)) { + return; + } + const files = await fs.readdir(sessionsDir); + for (const file of files) { + if (file.endsWith('.json')) { + const pidStr = path.basename(file, '.json'); + const pid = parseInt(pidStr, 10); + if (!isNaN(pid)) { + if (!isPidRunning(pid)) { + try { + await fs.remove(path.join(sessionsDir, file)); + } catch (e) { + console.error(`Failed to remove stale session file ${file}`, e); + } + } + } + } + } +} + +export async function writeSessionFile(pid: string, port: number, token: string) { + const homeDir = os.homedir(); + const sessionsDir = path.join(homeDir, '.vscode-R', 'sessions'); + await fs.ensureDir(sessionsDir); + const filePath = path.join(sessionsDir, `${pid}.json`); + await fs.writeJson(filePath, { port, token }); +} + +async function updateActiveTerminalFiles(port: number, token: string) { + const terminals = vscode.window.terminals; + for (const term of terminals) { + if (term.name === 'R Interactive') { + const pid = await term.processId; + if (pid) { + await writeSessionFile(pid.toString(), port, token); + } + } + } +} + +export async function getGlobalSessionServer(): Promise<{ port: number, token: string }> { + if (globalSessionServer) { + return globalSessionServer; } - fs.watch(requestLockFile, {}, () => { - void updateRequest(sessionStatusBarItem); + + return new Promise((resolve, reject) => { + const token = crypto.randomBytes(16).toString('hex'); + const wss = new WebSocket.Server({ port: 0, host: '127.0.0.1' }); + + wss.on('listening', () => { + const address = wss.address(); + if (typeof address === 'object' && address !== null) { + globalSessionServer = { port: address.port, token }; + console.info(`[SessionServer] Listening on ws://127.0.0.1:${address.port}?token=${token}`); + + // Initialize the old global server object for compatibility + server = { host: '127.0.0.1', port: address.port, token }; + resolve(globalSessionServer); + } else { + reject(new Error('Failed to get WebSocket server address')); + } + }); + + wss.on('connection', (ws: ExtWebSocket, req) => { + const url = new URL(req.url || '', `http://${req.headers.host || '127.0.0.1'}`); + const clientToken = url.searchParams.get('token'); + + if (clientToken !== token) { + console.warn('[SessionServer] Connection rejected: invalid token'); + ws.close(); + return; + } + + console.info('[SessionServer] Client connected'); + activeConnections.add(ws); + wsClient = ws; + + ws.on('message', (data: WebSocket.Data) => { + void (async () => { + try { + const message = JSON.parse(data.toString()) as Record; + if (message.id !== undefined && !message.method) { + // Response to a client request + const id = Number(message.id); + const pending = pendingRequests.get(id); + if (pending) { + pendingRequests.delete(id); + if (message.error) { + pending.reject(message.error); + } else { + pending.resolve(message.result); + } + } + } else if (!message.id) { + // Notification from server + await handleNotification(message, ws); + } else { + // Request from server + await handleRequest(message, ws); + } + } catch (e) { + console.error('[SessionServer] Error handling message', e); + } + })(); + }); + + ws.on('close', () => { + console.info('[SessionServer] Client disconnected'); + activeConnections.delete(ws); + if (wsClient === ws) { + wsClient = undefined; + } + }); + }); + + wss.on('error', (err) => { + console.error('[SessionServer] Server error', err); + reject(err); + }); }); - console.info('[startRequestWatcher] Done'); } -export function attachActive(): void { +export async function activateRSession(): Promise { if (config().get('sessionWatcher')) { - console.info('[attachActive]'); - void runTextInTerm('.vsc.attach()'); - if (isLiveShare() && shareWorkspace) { - rHostService?.notifyRequest(requestFile, true); + console.info('[activateRSession]'); + const terminal = window.activeTerminal; + if (terminal) { + const pidArg = await terminal.processId; + if (pidArg) { + const session = sessions.get(String(pidArg)); + if (session) { + console.info(`[activateRSession] Found existing session for PID: ${pidArg}`); + await activateSession(session); + terminal.show(); + return; + } + } } + + if (activeSession) { + console.info('[activateRSession] Focusing terminal of the active session'); + for (const term of window.terminals) { + const termPid = await term.processId; + if (termPid && sessions.get(String(termPid)) === activeSession) { + term.show(); + return; + } + } + } + + console.info('[activateRSession] Creating new R terminal'); + await rTerminal.createRTerm(); } else { void window.showInformationMessage('This command requires that r.sessionWatcher be enabled.'); } @@ -143,82 +324,25 @@ function writeSettings() { fs.writeFileSync(settingPath, JSON.stringify(config())); } -function updateSessionWatcher() { - console.info(`[updateSessionWatcher] PID: ${pid}`); - console.info('[updateSessionWatcher] Create workspaceWatcher'); - workspaceFile = path.join(sessionDir, 'workspace.json'); - workspaceLockFile = path.join(sessionDir, 'workspace.lock'); - workspaceTimeStamp = 0; - if (workspaceWatcher !== undefined) { - workspaceWatcher.close(); - } - if (fs.existsSync(workspaceLockFile)) { - workspaceWatcher = fs.watch(workspaceLockFile, {}, () => { - void updateWorkspace(); - }); - void updateWorkspace(); - } else { - console.info('[updateSessionWatcher] workspaceLockFile not found'); - } - - console.info('[updateSessionWatcher] Create plotWatcher'); - plotFile = path.join(sessionDir, 'plot.png'); - plotLockFile = path.join(sessionDir, 'plot.lock'); - plotTimeStamp = 0; - if (plotWatcher !== undefined) { - plotWatcher.close(); - } - if (fs.existsSync(plotLockFile)) { - plotWatcher = fs.watch(plotLockFile, {}, () => { - void updatePlot(); - }); - void updatePlot(); - } else { - console.info('[updateSessionWatcher] plotLockFile not found'); - } - console.info('[updateSessionWatcher] Done'); -} - async function updatePlot() { - console.info(`[updatePlot] ${plotFile}`); - const lockContent = await fs.readFile(plotLockFile, 'utf8'); - const newTimeStamp = Number.parseFloat(lockContent); - if (newTimeStamp !== plotTimeStamp) { - plotTimeStamp = newTimeStamp; - if (fs.existsSync(plotFile) && fs.statSync(plotFile).size > 0) { - void commands.executeCommand('vscode.open', Uri.file(plotFile), { - preserveFocus: true, - preview: true, - viewColumn: ViewColumn[(config().get('session.viewers.viewColumn.plot') || 'Two') as keyof typeof ViewColumn], - }); - console.info('[updatePlot] Done'); - if (isLiveShare()) { - void rHostService?.notifyPlot(plotFile); - } - } else { - console.info('[updatePlot] File not found'); - } - } + if (!server) {return;} + await globalPlotManager?.showStandardPlot(); } -async function updateWorkspace() { - console.info(`[updateWorkspace] ${workspaceFile}`); - - const lockContent = await fs.readFile(workspaceLockFile, 'utf8'); - const newTimeStamp = Number.parseFloat(lockContent); - if (newTimeStamp !== workspaceTimeStamp) { - workspaceTimeStamp = newTimeStamp; - if (fs.existsSync(workspaceFile)) { - const content = await fs.readFile(workspaceFile, 'utf8'); - workspaceData = JSON.parse(content) as WorkspaceData; +export async function updateWorkspace() { + if (!server) {return;} + try { + const response = await sessionRequest(server, { method: 'workspace' }); + if (response) { + workspaceData = response as WorkspaceData; + if (activeSession) { + activeSession.workspaceData = workspaceData; + } void rWorkspace?.refresh(); console.info('[updateWorkspace] Done'); - if (isLiveShare()) { - rHostService?.notifyWorkspace(workspaceData); - } - } else { - console.info('[updateWorkspace] File not found'); } + } catch (e) { + console.error(e); } } @@ -228,78 +352,22 @@ export async function showBrowser(url: string, title: string, viewer: string | b if (viewer === false) { void env.openExternal(uri); } else { - const externalUri = await env.asExternalUri(uri); - const panel = window.createWebviewPanel( - 'browser', - title, - { - preserveFocus: true, - viewColumn: ViewColumn[String(viewer) as keyof typeof ViewColumn], - }, - { - enableFindWidget: true, - enableScripts: true, - retainContextWhenHidden: true, - }); - if (isHost()) { - await shareBrowser(url, title); - } - panel.onDidChangeViewState((e: WebviewPanelOnDidChangeViewStateEvent) => { - if (e.webviewPanel.active) { - activeBrowserPanel = panel; - activeBrowserUri = uri; - activeBrowserExternalUri = externalUri; - } else { - activeBrowserPanel = undefined; - activeBrowserUri = undefined; - activeBrowserExternalUri = undefined; - } - void commands.executeCommand('setContext', 'r.browser.active', e.webviewPanel.active); + const viewColumn = ViewColumn[String(viewer) as keyof typeof ViewColumn]; + await commands.executeCommand('simpleBrowser.show', url, { + preserveFocus: true, + viewColumn: viewColumn, }); - panel.onDidDispose(() => { - activeBrowserPanel = undefined; - activeBrowserUri = undefined; - activeBrowserExternalUri = undefined; - if (isHost()) { - closeBrowser(url); - } - void commands.executeCommand('setContext', 'r.browser.active', false); - }); - panel.iconPath = new UriIcon('globe'); - panel.webview.html = getBrowserHtml(externalUri); + activeBrowserUri = uri; } console.info('[showBrowser] Done'); } -function getBrowserHtml(uri: Uri): string { - return ` - - - - - - - - -