├── .github └── workflows │ ├── ci.yml │ ├── deploy.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .vscode ├── launch.json └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── demo ├── .vscodeignore ├── package.json ├── pnpm-lock.yaml ├── shim.d.ts ├── src │ ├── commands.ts │ ├── configs.ts │ ├── extension.ts │ ├── states.ts │ ├── treeView.ts │ └── webviewView.ts ├── tsconfig.json └── tsup.config.ts ├── docs ├── .vitepress │ ├── components.d.ts │ ├── config.ts │ └── theme │ │ ├── Layout.vue │ │ ├── components │ │ ├── ApiLink.vue │ │ ├── ExampleFunctions.vue │ │ ├── FunctionBadge.vue │ │ ├── FunctionInfo.vue │ │ ├── FunctionsList.vue │ │ ├── NonProprietary.vue │ │ ├── ReactiveVscode.vue │ │ └── ReactiveVscode2.vue │ │ ├── index.ts │ │ ├── styles.css │ │ └── utils.ts ├── examples │ ├── editor-decoration │ │ ├── 1.ts │ │ ├── 2.ts │ │ └── index.md │ ├── hello-counter │ │ ├── 1.ts │ │ ├── 2.ts │ │ └── index.md │ ├── index.md │ └── theme-detector │ │ ├── 1.ts │ │ ├── 2.ts │ │ └── index.md ├── functions │ ├── [name].md │ ├── [name].paths.ts │ └── index.md ├── guide │ ├── command.md │ ├── config.md │ ├── context.md │ ├── disposable.md │ ├── editor.md │ ├── event.md │ ├── extension.md │ ├── index.md │ ├── terminal.md │ ├── view.md │ ├── vueuse.md │ ├── why.md │ └── window.md ├── index.md ├── package.json ├── public │ ├── favicon.ico │ ├── header.png │ └── logo.svg ├── snippets │ ├── treeView.ts │ └── webviewView.ts └── tsconfig.json ├── eslint.config.js ├── package.json ├── packages ├── core │ ├── package.json │ ├── shim.d.ts │ ├── src │ │ ├── composables │ │ │ ├── index.ts │ │ │ ├── useAbsolutePath.ts │ │ │ ├── useActiveColorTheme.ts │ │ │ ├── useActiveDebugSession.ts │ │ │ ├── useActiveEditorDecorations.ts │ │ │ ├── useActiveNotebookEditor.ts │ │ │ ├── useActiveTerminal.ts │ │ │ ├── useActiveTextEditor.ts │ │ │ ├── useAllExtensions.ts │ │ │ ├── useCommand.ts │ │ │ ├── useCommands.ts │ │ │ ├── useCommentController.ts │ │ │ ├── useControlledTerminal.ts │ │ │ ├── useDefaultShell.ts │ │ │ ├── useDisposable.ts │ │ │ ├── useDocumentText.ts │ │ │ ├── useEditorDecorations.ts │ │ │ ├── useEvent.ts │ │ │ ├── useEventEmitter.ts │ │ │ ├── useFetchTasks.ts │ │ │ ├── useFileUri.ts │ │ │ ├── useFoldingRangeProvider.ts │ │ │ ├── useFsWatcher.ts │ │ │ ├── useIsDarkTheme.ts │ │ │ ├── useIsTelemetryEnabled.ts │ │ │ ├── useL10nText.ts │ │ │ ├── useLogLevel.ts │ │ │ ├── useLogger.ts │ │ │ ├── useNotebookEditorSelection.ts │ │ │ ├── useNotebookEditorSelections.ts │ │ │ ├── useNotebookEditorVisibleRanges.ts │ │ │ ├── useOpenedTerminals.ts │ │ │ ├── useOutputChannel.ts │ │ │ ├── useStatusBarItem.ts │ │ │ ├── useTaskExecutions.ts │ │ │ ├── useTerminal.ts │ │ │ ├── useTerminalState.ts │ │ │ ├── useTextEditorCommand.ts │ │ │ ├── useTextEditorCommands.ts │ │ │ ├── useTextEditorSelection.ts │ │ │ ├── useTextEditorSelections.ts │ │ │ ├── useTextEditorViewColumn.ts │ │ │ ├── useTextEditorVisibleRanges.ts │ │ │ ├── useTreeView.ts │ │ │ ├── useUri.ts │ │ │ ├── useViewBadge.ts │ │ │ ├── useViewTitle.ts │ │ │ ├── useViewVisibility.ts │ │ │ ├── useVisibleNotebookEditors.ts │ │ │ ├── useVisibleTextEditors.ts │ │ │ ├── useVscodeContext.ts │ │ │ ├── useWebviewView.ts │ │ │ ├── useWindowState.ts │ │ │ └── useWorkspaceFolders.ts │ │ ├── index.ts │ │ └── utils │ │ │ ├── asAbsolutePath.ts │ │ │ ├── createKeyedComposable.ts │ │ │ ├── createSingletonComposable.ts │ │ │ ├── defineConfigs.ts │ │ │ ├── defineExtension.ts │ │ │ ├── defineLogger.ts │ │ │ ├── executeCommand.ts │ │ │ ├── index.ts │ │ │ ├── onActivate.ts │ │ │ ├── onDeactivate.ts │ │ │ ├── tryOnScopeDispose.ts │ │ │ └── types.ts │ ├── tsconfig.json │ └── vite.config.ts ├── creator │ ├── index.ts │ ├── package.json │ ├── templates │ │ ├── gitignore.ts │ │ ├── launch.ts │ │ ├── package.ts │ │ ├── readme.ts │ │ ├── src.ts │ │ ├── tsconfig.ts │ │ ├── tsupConfig.ts │ │ └── vscodeignore.ts │ ├── tsup.config.ts │ └── utils │ │ ├── banner.ts │ │ ├── pkgManager.ts │ │ └── pkgName.ts ├── metadata │ ├── README.md │ ├── index.d.ts │ ├── package.json │ ├── scripts │ │ └── update.ts │ ├── tsconfig.json │ └── types.ts ├── reactivity │ ├── README.md │ ├── package.json │ ├── shim.d.ts │ ├── src │ │ ├── apiWatch.ts │ │ ├── enums.ts │ │ ├── errorHandling.ts │ │ ├── index.ts │ │ ├── scheduler.ts │ │ └── warning.ts │ ├── tsconfig.json │ └── vite.config.ts └── vueuse │ ├── README.md │ ├── package.json │ ├── shim.d.ts │ ├── src │ ├── index.ts │ └── vue-demi.ts │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts └── publish.js ├── test ├── composables │ └── useVscodeContext.test.ts ├── mock │ └── vscode.ts ├── package.json ├── shim.d.ts ├── tsconfig.json ├── utils │ ├── defineConfigs.test.ts │ └── keyedComposable.test.ts └── vite.config.ts └── tsconfig.base.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v3 20 | 21 | - name: Set node 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | 26 | - name: Setup 27 | run: npm i -g @antfu/ni 28 | 29 | - name: Install 30 | run: nci 31 | 32 | - name: Build 33 | run: nr core:build 34 | 35 | - name: Lint 36 | run: nr lint 37 | 38 | typecheck: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | 43 | - name: Install pnpm 44 | uses: pnpm/action-setup@v3 45 | 46 | - name: Set node 47 | uses: actions/setup-node@v4 48 | with: 49 | node-version: lts/* 50 | 51 | - name: Setup 52 | run: npm i -g @antfu/ni 53 | 54 | - name: Install 55 | run: nci 56 | 57 | - name: Build 58 | run: nr core:build 59 | 60 | - name: Typecheck 61 | run: nr typecheck 62 | 63 | test: 64 | runs-on: ubuntu-latest 65 | 66 | steps: 67 | - uses: actions/checkout@v4 68 | 69 | - name: Install pnpm 70 | uses: pnpm/action-setup@v3 71 | 72 | - name: Set node 73 | uses: actions/setup-node@v4 74 | with: 75 | node-version: lts/* 76 | 77 | - name: Setup 78 | run: npm i -g @antfu/ni 79 | 80 | - name: Install 81 | run: nci 82 | 83 | - name: Build 84 | run: nr core:build 85 | 86 | - name: Test 87 | run: nr test 88 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy VitePress site to Pages 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: pages 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | # Build job 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Install pnpm 26 | uses: pnpm/action-setup@v3 27 | 28 | - name: Set node 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: lts/* 32 | 33 | - name: Setup @antfu/ni 34 | run: npm i -g @antfu/ni 35 | 36 | - name: Setup Pages 37 | uses: actions/configure-pages@v4 38 | 39 | - name: Install dependencies 40 | run: nci 41 | 42 | - name: Build with VitePress 43 | run: nr docs:build 44 | 45 | - name: Upload artifact 46 | uses: actions/upload-pages-artifact@v3 47 | with: 48 | path: docs/.vitepress/dist 49 | 50 | # Deployment job 51 | deploy: 52 | environment: 53 | name: github-pages 54 | url: ${{ steps.deployment.outputs.page_url }} 55 | needs: build 56 | runs-on: ubuntu-latest 57 | name: Deploy 58 | steps: 59 | - name: Deploy to GitHub Pages 60 | id: deployment 61 | uses: actions/deploy-pages@v4 62 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | permissions: 14 | id-token: write 15 | contents: write 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Install pnpm 23 | uses: pnpm/action-setup@v3 24 | 25 | - name: Set node 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: lts/* 29 | registry-url: https://registry.npmjs.org/ 30 | 31 | - name: Setup @antfu/ni 32 | run: npm i -g @antfu/ni 33 | 34 | - name: Install 35 | run: nci 36 | 37 | - run: npx changelogithub 38 | env: 39 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 40 | 41 | - name: Publish 42 | run: node scripts/publish.js 43 | env: 44 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 45 | NPM_CONFIG_PROVENANCE: true 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | coverage 7 | dist 8 | lib-cov 9 | logs 10 | node_modules 11 | temp 12 | docs/.vitepress/dist 13 | docs/.vitepress/cache 14 | packages/core/README.md 15 | packages/metadata/metadata.json 16 | packages/metadata/index.js 17 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "args": [ 9 | "--extensionDevelopmentPath=${workspaceFolder}/demo" 10 | ], 11 | "outFiles": [ 12 | "${workspaceFolder}/demo/dist/**/*.js" 13 | ] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enable the ESlint flat config support 3 | "eslint.experimental.useFlatConfig": true, 4 | 5 | // Disable the default formatter, use eslint instead 6 | "prettier.enable": false, 7 | "editor.formatOnSave": false, 8 | 9 | // Auto fix 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll.eslint": "explicit", 12 | "source.organizeImports": "never" 13 | }, 14 | 15 | // Silent the stylistic rules in you IDE, but still auto fix them 16 | "eslint.rules.customizations": [ 17 | { "rule": "style/*", "severity": "off" }, 18 | { "rule": "*-indent", "severity": "off" }, 19 | { "rule": "*-spacing", "severity": "off" }, 20 | { "rule": "*-spaces", "severity": "off" }, 21 | { "rule": "*-order", "severity": "off" }, 22 | { "rule": "*-dangle", "severity": "off" }, 23 | { "rule": "*-newline", "severity": "off" }, 24 | { "rule": "*quotes", "severity": "off" }, 25 | { "rule": "*semi", "severity": "off" } 26 | ], 27 | 28 | // Enable eslint for all supported languages 29 | "eslint.validate": [ 30 | "javascript", 31 | "javascriptreact", 32 | "typescript", 33 | "typescriptreact", 34 | "vue", 35 | "html", 36 | "markdown", 37 | "json", 38 | "jsonc", 39 | "yaml" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Excited to hear that you are interested in contributing to this project! Thanks! 4 | 5 | ## Setup (locally) 6 | 7 | This project uses [`pnpm`](https://pnpm.io/) to manage the dependencies, install it if you haven't via 8 | 9 | ```bash 10 | npm i -g pnpm 11 | ``` 12 | 13 | Clone this repo to your local machine and install the dependencies. 14 | 15 | ```bash 16 | pnpm install 17 | ``` 18 | 19 | ## Development 20 | 21 | First, run `pnpm metadata:dev` to generate the metadata for the development environment. 22 | 23 | To start the demo extension, run 24 | 25 | ```bash 26 | pnpm dev 27 | ``` 28 | 29 | And press `F5` to start the extension. 30 | 31 | To develop the documentation, run 32 | 33 | ```bash 34 | pnpm dev:docs 35 | ``` 36 | 37 | To run the tests, run 38 | 39 | ```bash 40 | pnpm test 41 | ``` 42 | 43 | ## Project Structure 44 | 45 | ### Monorepo 46 | 47 | We use monorepo to manage multiple packages. 48 | 49 | ``` 50 | demo/ - the demo extension 51 | docs/ - the documentation 52 | packages/ 53 | metadata/ - metadata generator and metadata 54 | reactive-vscode/ - the main package 55 | tests/ - tests 56 | ``` 57 | 58 | ## Code Style 59 | 60 | Don't worry about the code style as long as you install the dev dependencies. Git hooks will format and fix them for you on committing. If the autofix CI still fails, run 61 | 62 | ```bash 63 | pnpm lint --fix 64 | ``` 65 | 66 | ## Thanks 67 | 68 | Thank you again for being interested in this project! You are awesome! 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 _Kerman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reactive-vscode 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![License][license-src]][license-href] 6 | 7 | ![Header](./docs/public/header.png) 8 | **Develop VSCode extension with Vue Reactivity API** 9 | 10 | [**Documentation**](https://kermanx.github.io/reactive-vscode/) | [**Why reactive-vscode**](https://kermanx.github.io/reactive-vscode/guide/why) | [**Functions**](https://kermanx.github.io/reactive-vscode/functions/) | [**Examples**](https://kermanx.github.io/reactive-vscode/examples/) 11 | 12 | ## License 13 | 14 | [MIT](./LICENSE) License © 2024-PRESENT [_Kerman](https://github.com/KermanX) 15 | 16 | Source code in [the `./packages/reactivity` directory](https://github.com/KermanX/reactive-vscode/blob/main/packages/core/src/reactivity) is ported from [`@vue/runtime-core`](https://github.com/vuejs/core/blob/main/packages/runtime-core). Licensed under a [MIT License](https://github.com/vueuse/vueuse/blob/main/LICENSE). 17 | 18 | The logo is modified from [Vue Reactity Artworks](https://github.com/vue-reactivity/art). Licensed under a [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-nc-sa/4.0/). 19 | 20 | Part of the docs website is ported from [VueUse](https://github.com/vueuse/vueuse). Licensed under a [MIT License](https://github.com/vueuse/vueuse/blob/main/LICENSE). 21 | 22 | 23 | 24 | [npm-version-src]: https://img.shields.io/npm/v/reactive-vscode?style=flat&colorA=080f12&colorB=1fa669 25 | [npm-version-href]: https://npmjs.com/package/reactive-vscode 26 | [npm-downloads-src]: https://img.shields.io/npm/dm/reactive-vscode?style=flat&colorA=080f12&colorB=1fa669 27 | [npm-downloads-href]: https://npmjs.com/package/reactive-vscode 28 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/reactive-vscode?style=flat&colorA=080f12&colorB=1fa669&label=minzip 29 | [bundle-href]: https://bundlephobia.com/result?p=reactive-vscode 30 | [license-src]: https://img.shields.io/github/license/KermanX/reactive-vscode.svg?style=flat&colorA=080f12&colorB=1fa669 31 | [license-href]: https://github.com/KermanX/reactive-vscode/blob/main/LICENSE 32 | [jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669 33 | [jsdocs-href]: https://www.jsdocs.io/package/reactive-vscode 34 | -------------------------------------------------------------------------------- /demo/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | **/.vscode-test.* 12 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactive-vscode/demo", 3 | "displayName": "demo", 4 | "type": "module", 5 | "version": "0.0.1", 6 | "private": true, 7 | "description": "The demo for reactive-vscode", 8 | "categories": [ 9 | "Other" 10 | ], 11 | "main": "./dist/extension.cjs", 12 | "engines": { 13 | "vscode": "^1.89.0" 14 | }, 15 | "activationEvents": [ 16 | "onStartupFinished" 17 | ], 18 | "contributes": { 19 | "commands": [ 20 | { 21 | "command": "reactive-vscode-demo.helloWorld", 22 | "title": "Hello World" 23 | } 24 | ], 25 | "configuration": { 26 | "title": "Reactive VSCode Demo", 27 | "properties": { 28 | "reactive-vscode-demo.message": { 29 | "type": "string", 30 | "default": "Hello World", 31 | "description": "The message to show in the notification" 32 | } 33 | } 34 | }, 35 | "viewsContainers": { 36 | "activitybar": [ 37 | { 38 | "id": "reactive-vscode-demo", 39 | "title": "Reactive VSCode Demo", 40 | "icon": "$(list-flat)" 41 | } 42 | ] 43 | }, 44 | "views": { 45 | "reactive-vscode-demo": [ 46 | { 47 | "id": "reactive-tree-view", 48 | "name": "Tree View" 49 | }, 50 | { 51 | "type": "webview", 52 | "id": "reactive-webview-view", 53 | "name": "Webview View" 54 | } 55 | ] 56 | } 57 | }, 58 | "scripts": { 59 | "build": "tsup --env.NODE_ENV production --treeshake", 60 | "dev": "tsup --watch ./src --watch ../packages --env.NODE_ENV development", 61 | "typecheck": "tsc --noEmit" 62 | }, 63 | "devDependencies": { 64 | "@types/node": "18.x", 65 | "@types/vscode": "^1.89.0", 66 | "reactive-vscode": "workspace:*", 67 | "tsup": "^8.0.2", 68 | "typescript": "^5.4.5" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /demo/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | '@vue/runtime-core': 12 | specifier: ^3.4.27 13 | version: 3.4.27 14 | reactive-vscode: 15 | specifier: link:.. 16 | version: link:.. 17 | 18 | packages: 19 | 20 | '@vue/reactivity@3.4.27': 21 | resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==} 22 | 23 | '@vue/runtime-core@3.4.27': 24 | resolution: {integrity: sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==} 25 | 26 | '@vue/shared@3.4.27': 27 | resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==} 28 | 29 | snapshots: 30 | 31 | '@vue/reactivity@3.4.27': 32 | dependencies: 33 | '@vue/shared': 3.4.27 34 | 35 | '@vue/runtime-core@3.4.27': 36 | dependencies: 37 | '@vue/reactivity': 3.4.27 38 | '@vue/shared': 3.4.27 39 | 40 | '@vue/shared@3.4.27': {} 41 | -------------------------------------------------------------------------------- /demo/shim.d.ts: -------------------------------------------------------------------------------- 1 | export { } 2 | declare global { 3 | const __DEV__: boolean 4 | } 5 | -------------------------------------------------------------------------------- /demo/src/commands.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/reactive-vscode/a3a7941118bcb1724ce8ecdaea8958f806d10971/demo/src/commands.ts -------------------------------------------------------------------------------- /demo/src/configs.ts: -------------------------------------------------------------------------------- 1 | import { defineConfigs } from 'reactive-vscode' 2 | 3 | export const { message } = defineConfigs('reactive-vscode-demo', { 4 | message: 'string', 5 | }) 6 | -------------------------------------------------------------------------------- /demo/src/extension.ts: -------------------------------------------------------------------------------- 1 | import { defineExtension, useCommand, useIsDarkTheme, useLogger, watchEffect } from 'reactive-vscode' 2 | import { window } from 'vscode' 3 | import { message } from './configs' 4 | import { calledTimes } from './states' 5 | import { useDemoTreeView } from './treeView' 6 | import { useDemoWebviewView } from './webviewView' 7 | 8 | const logger = useLogger('Reactive VSCode') 9 | 10 | export = defineExtension(() => { 11 | logger.info('Extension Activated') 12 | logger.show() 13 | 14 | useCommand('reactive-vscode-demo.helloWorld', () => { 15 | window.showInformationMessage(message.value) 16 | calledTimes.value++ 17 | }) 18 | 19 | const isDark = useIsDarkTheme() 20 | watchEffect(() => { 21 | logger.info('Is Dark Theme:', isDark.value) 22 | }) 23 | 24 | useDemoTreeView() 25 | useDemoWebviewView() 26 | }) 27 | -------------------------------------------------------------------------------- /demo/src/states.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'reactive-vscode' 2 | 3 | export const calledTimes = ref(0) 4 | -------------------------------------------------------------------------------- /demo/src/treeView.ts: -------------------------------------------------------------------------------- 1 | import type { TreeViewNode } from 'reactive-vscode' 2 | import { computed, createSingletonComposable, useTreeView } from 'reactive-vscode' 3 | import { TreeItemCollapsibleState } from 'vscode' 4 | import { calledTimes } from './states' 5 | 6 | export const useDemoTreeView = createSingletonComposable(() => { 7 | function getRootNode(index: number) { 8 | return { 9 | children: [ 10 | getChildNode(index * 10 + 1), 11 | getChildNode(index * 10 + 2), 12 | ], 13 | treeItem: { 14 | label: `Root ${index}`, 15 | collapsibleState: TreeItemCollapsibleState.Expanded, 16 | }, 17 | } 18 | } 19 | 20 | function getChildNode(index: number) { 21 | return { 22 | treeItem: { 23 | label: `Child ${index}`, 24 | collapsibleState: TreeItemCollapsibleState.None, 25 | }, 26 | } 27 | } 28 | 29 | const treeData = computed(() => { 30 | const roots: TreeViewNode[] = [] 31 | for (let i = 0; i < calledTimes.value; i++) 32 | roots.push(getRootNode(i)) 33 | return roots 34 | }) 35 | 36 | return useTreeView( 37 | 'reactive-tree-view', 38 | treeData, 39 | ) 40 | }) 41 | -------------------------------------------------------------------------------- /demo/src/webviewView.ts: -------------------------------------------------------------------------------- 1 | import { computed, createSingletonComposable, useWebviewView } from 'reactive-vscode' 2 | import { message } from './configs' 3 | import { calledTimes } from './states' 4 | 5 | export const useDemoWebviewView = createSingletonComposable(() => { 6 | const html = computed(() => ` 7 | 16 |

Webview View

17 |

${message.value} for ${calledTimes.value} times

18 |

Say Hello World

19 |
20 | 21 | 22 |
23 | `) 24 | 25 | return useWebviewView( 26 | 'reactive-webview-view', 27 | html, 28 | { 29 | webviewOptions: { 30 | enableScripts: true, 31 | enableCommandUris: true, 32 | }, 33 | onDidReceiveMessage(ev) { 34 | if (ev.type === 'updateMessage') 35 | message.value = ev.message 36 | }, 37 | }, 38 | ) 39 | }) 40 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "Preserve", 5 | "strict": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/extension.ts'], 5 | format: ['cjs'], 6 | target: 'node18', 7 | clean: true, 8 | minify: true, 9 | external: [ 10 | 'vscode', 11 | ], 12 | }) 13 | -------------------------------------------------------------------------------- /docs/.vitepress/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | ApiLink: typeof import('./theme/components/ApiLink.vue')['default'] 11 | ExampleFunctions: typeof import('./theme/components/ExampleFunctions.vue')['default'] 12 | FunctionBadge: typeof import('./theme/components/FunctionBadge.vue')['default'] 13 | FunctionInfo: typeof import('./theme/components/FunctionInfo.vue')['default'] 14 | FunctionsList: typeof import('./theme/components/FunctionsList.vue')['default'] 15 | NonProprietary: typeof import('./theme/components/NonProprietary.vue')['default'] 16 | ReactiveVscode: typeof import('./theme/components/ReactiveVscode.vue')['default'] 17 | ReactiveVscode2: typeof import('./theme/components/ReactiveVscode2.vue')['default'] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises' 2 | import { resolve } from 'node:path' 3 | import presetIcons from '@unocss/preset-icons' 4 | import presetUno from '@unocss/preset-uno' 5 | import presetAttributify from '@unocss/preset-attributify' 6 | import transformerDirectives from '@unocss/transformer-directives' 7 | import transformerVariantGroup from '@unocss/transformer-variant-group' 8 | import UnoCSS from 'unocss/vite' 9 | import Components from 'unplugin-vue-components/vite' 10 | import { defineConfig } from 'vitepress' 11 | import { transformerTwoslash } from '@shikijs/vitepress-twoslash' 12 | 13 | // https://vitepress.dev/reference/site-config 14 | export default defineConfig({ 15 | title: 'Reactive VSCode', 16 | description: 'Develop VSCode extension with Vue Reactivity API', 17 | base: '/reactive-vscode/', 18 | lang: 'en-US', 19 | themeConfig: { 20 | logo: '/logo.svg', 21 | 22 | nav: [ 23 | { text: 'Guide', link: '/guide/' }, 24 | { text: 'Functions', link: '/functions' }, 25 | { text: 'Examples', link: '/examples/' }, 26 | ], 27 | 28 | sidebar: { 29 | '/guide': [ 30 | { 31 | text: 'Guide', 32 | items: [ 33 | { text: 'Why reactive-vscode', link: '/guide/why' }, 34 | { text: 'Getting Started', link: '/guide/' }, 35 | { text: 'Extension', link: '/guide/extension' }, 36 | { text: 'Commands', link: '/guide/command' }, 37 | { text: 'Views', link: '/guide/view' }, 38 | { text: 'Configurations', link: '/guide/config' }, 39 | { text: 'Editor & Document', link: '/guide/editor' }, 40 | { text: 'Window & Workspace', link: '/guide/window' }, 41 | { text: 'Terminals', link: '/guide/terminal' }, 42 | { text: 'Custom contexts', link: '/guide/context' }, 43 | { text: 'Disposables', link: '/guide/disposable' }, 44 | { text: 'Events', link: '/guide/event' }, 45 | { text: 'VueUse Integration', link: '/guide/vueuse' }, 46 | ], 47 | }, 48 | { 49 | items: [ 50 | { text: 'Examples', link: '/examples/' }, 51 | ], 52 | }, 53 | ], 54 | '/examples': [ 55 | { 56 | text: 'Examples', 57 | items: [ 58 | { text: 'Index', link: '/examples/' }, 59 | { text: 'Hello Counter', link: '/examples/hello-counter/' }, 60 | { text: 'Editor Decoration', link: '/examples/editor-decoration/' }, 61 | { text: 'Theme Detector', link: '/examples/theme-detector/' }, 62 | ], 63 | }, 64 | ], 65 | }, 66 | 67 | socialLinks: [ 68 | { icon: 'github', link: 'https://github.com/KermanX/reactive-vscode' }, 69 | { icon: 'discord', link: 'https://discord.gg/8YNDMA5Hcq' }, 70 | ], 71 | 72 | search: { 73 | provider: 'local', 74 | }, 75 | 76 | editLink: { 77 | pattern: 'https://github.com/kermanx/reactive-vscode/edit/main/docs/:path', 78 | }, 79 | }, 80 | head: [['link', { rel: 'icon', href: '/reactive-vscode/favicon.ico' }]], 81 | lastUpdated: true, 82 | 83 | vite: { 84 | plugins: [ 85 | Components({ 86 | dirs: resolve(__dirname, 'theme/components'), 87 | include: [/\.vue$/, /\.vue\?vue/, /\.md$/], 88 | dts: resolve(__dirname, 'components.d.ts'), 89 | transformer: 'vue3', 90 | }) as any, 91 | UnoCSS({ 92 | presets: [ 93 | presetUno(), 94 | presetAttributify(), 95 | presetIcons({ 96 | extraProperties: { 97 | 'display': 'inline-block', 98 | 'vertical-align': 'middle', 99 | }, 100 | collections: { 101 | 'reactive-vscode': { 102 | logo: () => readFile(resolve(__dirname, '../public/logo.svg'), 'utf-8'), 103 | }, 104 | }, 105 | }), 106 | ], 107 | theme: { 108 | colors: { 109 | 'primary': '#1F9CF0', 110 | 'reactive': '#229863', 111 | 'vscode': '#1F9CF0', 112 | 'vscode-darker': '#007ACC', 113 | }, 114 | fontFamily: { 115 | mono: 'var(--vp-font-family-mono)', 116 | }, 117 | }, 118 | shortcuts: { 119 | 'border-main': 'border-$vp-c-divider', 120 | 'bg-main': 'bg-gray-400', 121 | 'bg-base': 'bg-white dark:bg-hex-1a1a1a', 122 | }, 123 | transformers: [ 124 | transformerDirectives(), 125 | transformerVariantGroup(), 126 | ], 127 | }), 128 | { 129 | name: 'api-link', 130 | enforce: 'pre', 131 | transform(code, id) { 132 | if (!id.endsWith('.md')) 133 | return 134 | return code.replace(/`(\w+)::([^(`]+)(\(\S+?\))?`/g, (_, scope, name, link) => { 135 | return `` 136 | }) 137 | }, 138 | }, 139 | ], 140 | }, 141 | 142 | markdown: { 143 | codeTransformers: [ 144 | transformerTwoslash({ 145 | explicitTrigger: false, 146 | twoslashOptions: { 147 | compilerOptions: { 148 | module: 200, // ModuleKind.Preserve, 149 | }, 150 | vfsRoot: resolve(__dirname, '../snippets'), 151 | }, 152 | }), 153 | ], 154 | }, 155 | }) 156 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/Layout.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 47 | 48 | 65 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/ApiLink.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 48 | 49 | 74 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/ExampleFunctions.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 34 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/FunctionBadge.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 49 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/FunctionInfo.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 38 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/FunctionsList.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 158 | 159 | 205 | 206 | 219 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/NonProprietary.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/ReactiveVscode.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/ReactiveVscode2.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client' 4 | import '@shikijs/vitepress-twoslash/style.css' 5 | import 'virtual:uno.css' 6 | import type { EnhanceAppContext } from 'vitepress' 7 | import DefaultTheme from 'vitepress/theme' 8 | import Layout from './Layout.vue' 9 | import './styles.css' 10 | 11 | export default { 12 | extends: DefaultTheme, 13 | Layout, 14 | enhanceApp({ app }: EnhanceAppContext) { 15 | app.use(TwoslashFloatingVue) 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vp-c-reactive: #229863; 3 | --vp-c-vscode: #1F9CF0; 4 | --vp-c-vscode-darker: #007ACC; 5 | --vp-c-vue: var(--vp-c-reactive); 6 | --vp-c-npm: color-mix(in srgb, var(--vp-c-text-1), red 50%); 7 | 8 | --vp-c-brand-1: var(--vp-c-vscode); 9 | --vp-c-brand-2: var(--vp-c-reactive); 10 | --vp-c-brand-3: var(--vp-c-vscode-darker); 11 | --vp-c-brand-soft: var(--vp-c-indigo-soft); 12 | 13 | --vp-home-hero-name-color: transparent; 14 | --vp-home-hero-name-background: linear-gradient( 90deg, var(--vp-c-reactive) 30%, var(--vp-c-vscode) 80%); 15 | --vp-home-hero-image-background-image: linear-gradient( 130deg, var(--vp-c-reactive) 50%, var(--vp-c-vscode-darker) 50%); 16 | --vp-home-hero-image-filter: blur(180px); 17 | } 18 | 19 | .VPHero .main .text { 20 | opacity: 0.8; 21 | } 22 | 23 | .api-link { 24 | text-decoration: none !important; 25 | line-height: 1.6; 26 | @apply !inline-block !font-mono !px-1; 27 | } 28 | 29 | .api-link:hover { 30 | @apply op80; 31 | } 32 | 33 | .api-link.scope-vscode { 34 | color: var(--vp-c-vscode); 35 | } 36 | 37 | .api-link.scope-vscode::before { 38 | content: ''; 39 | @apply mr-1 !i-vscode-icons:file-type-vscode; 40 | } 41 | 42 | .api-link.scope-reactive { 43 | color: var(--vp-c-reactive); 44 | } 45 | 46 | .api-link.scope-reactive::before { 47 | content: ''; 48 | @apply mr-1 !i-reactive-vscode:logo; 49 | } 50 | 51 | .api-link.scope-vue { 52 | color: var(--vp-c-vue); 53 | } 54 | 55 | .api-link.scope-vue::before { 56 | content: ''; 57 | @apply mr-1 !i-vscode-icons:file-type-vue; 58 | } 59 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/utils.ts: -------------------------------------------------------------------------------- 1 | export function renderMarkdown(markdownText = '') { 2 | const htmlText = markdownText 3 | .replace(/`vscode::(\S+?)`/g, '$1') 4 | // eslint-disable-next-line node/prefer-global/process 5 | .replace(/`reactive::(\S+?)`/g, `$1`) 6 | .replace(/`vue::([^(`]+)\((\S+?)\)`/g, `$1`) 7 | .replace(/^### (.*$)/gm, '

$1

') 8 | .replace(/^## (.*$)/gm, '

$1

') 9 | .replace(/^# (.*$)/gm, '

$1

') 10 | .replace(/^> (.*$)/gm, '
$1
') 11 | .replace(/\*\*(.*)\*\*/g, '$1') 12 | .replace(/\*(.*)\*/g, '$1') 13 | .replace(/!\[(.*?)\]\((.*?)\)/g, '\'$1\'') 14 | .replace(/\[(.*?)\]\((.*?)\)/g, '$1') 15 | .replace(/`(.*?)`/g, '$1') 16 | .replace(/\n$/gm, '
') 17 | 18 | return htmlText.trim() 19 | } 20 | 21 | export function renderCommitMessage(msg: string) { 22 | return renderMarkdown(msg) 23 | .replace(/#(\d+)/g, '#$1') 24 | } 25 | -------------------------------------------------------------------------------- /docs/examples/editor-decoration/1.ts: -------------------------------------------------------------------------------- 1 | import { defineConfigs, defineExtension, useActiveEditorDecorations } from 'reactive-vscode' 2 | 3 | const { decorations } = defineConfigs('demo', { decorations: Boolean }) 4 | 5 | export = defineExtension(() => { 6 | useActiveEditorDecorations( 7 | { 8 | backgroundColor: 'red', 9 | }, 10 | () => decorations.value ? [/* ... Caclulated ranges ... */] : [], 11 | ) 12 | }) 13 | -------------------------------------------------------------------------------- /docs/examples/editor-decoration/2.ts: -------------------------------------------------------------------------------- 1 | import type { ExtensionContext } from 'vscode' 2 | import { window, workspace } from 'vscode' 3 | 4 | const decorationType = window.createTextEditorDecorationType({ 5 | backgroundColor: 'red', 6 | }) 7 | 8 | function updateDecorations(enabled: boolean) { 9 | window.activeTextEditor?.setDecorations( 10 | decorationType, 11 | enabled ? [/* ... Caclulated ranges ... */] : [], 12 | ) 13 | } 14 | 15 | export function activate(context: ExtensionContext) { 16 | const configurations = workspace.getConfiguration('demo') 17 | let decorationsEnabled = configurations.get('decorations')! 18 | 19 | context.subscriptions.push(workspace.onDidChangeConfiguration((e) => { 20 | if (e.affectsConfiguration('demo.decorations')) { 21 | decorationsEnabled = configurations.get('decorations')! 22 | updateDecorations(decorationsEnabled) 23 | } 24 | })) 25 | context.subscriptions.push(window.onDidChangeActiveTextEditor(() => { 26 | updateDecorations(decorationsEnabled) 27 | })) 28 | 29 | updateDecorations(decorationsEnabled) 30 | } 31 | -------------------------------------------------------------------------------- /docs/examples/editor-decoration/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: false 3 | --- 4 | 5 | # Editor Decoration 6 | 7 | A simple example that adds decorations to the active editor depending on the configuration. 8 | 9 | 13 | 14 | ::: code-group 15 | 16 | <<< ./1.ts [] 17 | 18 | <<< ./2.ts [Original VSCode API] 19 | 20 | ::: 21 | -------------------------------------------------------------------------------- /docs/examples/hello-counter/1.ts: -------------------------------------------------------------------------------- 1 | import { defineExtension, ref, useCommands, useStatusBarItem } from 'reactive-vscode' 2 | import { StatusBarAlignment } from 'vscode' 3 | 4 | export = defineExtension(() => { 5 | const counter = ref(0) 6 | 7 | useStatusBarItem({ 8 | alignment: StatusBarAlignment.Right, 9 | priority: 100, 10 | text: () => `$(megaphone) Hello*${counter.value}`, 11 | }) 12 | 13 | useCommands({ 14 | 'extension.sayHello': () => counter.value++, 15 | 'extension.sayGoodbye': () => counter.value--, 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /docs/examples/hello-counter/2.ts: -------------------------------------------------------------------------------- 1 | import type { ExtensionContext } from 'vscode' 2 | import { StatusBarAlignment, commands, window } from 'vscode' 3 | 4 | export function activate(extensionContext: ExtensionContext) { 5 | let counter = 0 6 | 7 | const item = window.createStatusBarItem(StatusBarAlignment.Right, 100) 8 | 9 | function updateStatusBar() { 10 | item.text = `$(megaphone) Hello*${counter}` 11 | item.show() 12 | } 13 | 14 | updateStatusBar() 15 | 16 | extensionContext.subscriptions.push( 17 | commands.registerCommand('extension.sayHello', () => { 18 | counter++ 19 | updateStatusBar() 20 | }), 21 | commands.registerCommand('extension.sayGoodbye', () => { 22 | counter-- 23 | updateStatusBar() 24 | }), 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /docs/examples/hello-counter/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: false 3 | --- 4 | 5 | # Hello Counter 6 | 7 | A simple example that shows how many times the user has called the commands. 8 | 9 | 13 | 14 | ::: code-group 15 | 16 | <<< ./1.ts [] 17 | 18 | <<< ./2.ts [Original VSCode API] 19 | 20 | ::: 21 | -------------------------------------------------------------------------------- /docs/examples/index.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | In this section, some examples are provided to help you understand . 4 | 5 | For each example, there are two implementations that achieve the same goal. The first implementation uses original VSCode API, and the second one uses API. 6 | 7 | - [Hello Counter](./hello-counter/index) - A simple example that shows how many times the user has called the commands. 8 | 9 | - [Editor Decoration](./editor-decoration/index) - A simple example that adds decorations to the active editor depending on the configuration. 10 | 11 | - [Theme Detector](./theme-detector/index) - Detects the user's theme, and show a message accordingly. 12 | -------------------------------------------------------------------------------- /docs/examples/theme-detector/1.ts: -------------------------------------------------------------------------------- 1 | import { defineExtension, useActiveColorTheme, useIsDarkTheme, watchEffect } from 'reactive-vscode' 2 | import { window } from 'vscode' 3 | 4 | export = defineExtension(() => { 5 | const theme = useActiveColorTheme() 6 | const isDark = useIsDarkTheme() 7 | watchEffect(() => { 8 | window.showInformationMessage(`Your theme is ${theme.value} (kind: ${isDark.value ? 'dark' : 'light'})`) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /docs/examples/theme-detector/2.ts: -------------------------------------------------------------------------------- 1 | import type { ExtensionContext } from 'vscode' 2 | import { ColorThemeKind, window } from 'vscode' 3 | 4 | function showMessage() { 5 | const theme = window.activeColorTheme 6 | const isDark = theme.kind === ColorThemeKind.Dark || theme.kind === ColorThemeKind.HighContrast 7 | window.showInformationMessage(`Your theme is ${theme} (kind: ${isDark ? 'dark' : 'light'})`) 8 | } 9 | 10 | export function activate(extensionContext: ExtensionContext) { 11 | showMessage() 12 | extensionContext.subscriptions.push(window.onDidChangeActiveColorTheme(showMessage)) 13 | } 14 | -------------------------------------------------------------------------------- /docs/examples/theme-detector/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: false 3 | --- 4 | 5 | # Theme Detector 6 | 7 | Detects the user's theme, and show a message accordingly. 8 | 9 | 13 | 14 | ::: code-group 15 | 16 | <<< ./1.ts [] 17 | 18 | <<< ./2.ts [Original VSCode API] 19 | 20 | ::: 21 | -------------------------------------------------------------------------------- /docs/functions/[name].md: -------------------------------------------------------------------------------- 1 |

{{ $params.name }}

2 | 3 | 4 | 5 | 19 | 20 | ## Usage 21 | 22 | 23 | 24 | ## Source 25 | 26 | View Source 27 | -------------------------------------------------------------------------------- /docs/functions/[name].paths.ts: -------------------------------------------------------------------------------- 1 | import { metadata } from '@reactive-vscode/metadata' 2 | import { renderMarkdown } from '../.vitepress/theme/utils' 3 | 4 | export default { 5 | paths() { 6 | return metadata.functions.map(fn => ({ 7 | content: renderMarkdown(fn.description), 8 | params: { 9 | name: fn.name, 10 | }, 11 | })) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /docs/functions/index.md: -------------------------------------------------------------------------------- 1 | # Functions 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/guide/command.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | Commands trigger actions. Commands expose functionality to users, bind to actions in VS Code's UI, and implement internal logic. 4 | 5 | There are some [built-in commands](https://code.visualstudio.com/api/references/commands) in VS Code, and you can also define your own commands. 6 | 7 | ## Define in Manifest 8 | 9 | As described in the [official documentation](https://code.visualstudio.com/api/references/contribution-points#contributes.commands), you need to define the commands in the `contributes.commands` field in the `package.json`. 10 | 11 | ```json 12 | { 13 | "contributes": { 14 | "commands": [ 15 | { 16 | "command": "extension.sayHello", 17 | "title": "Hello World", 18 | "category": "Hello", 19 | "icon": { 20 | "light": "path/to/light/icon.svg", 21 | "dark": "path/to/dark/icon.svg" 22 | } 23 | } 24 | ] 25 | } 26 | } 27 | ``` 28 | 29 | ## Register Commands 30 | 31 | You can use the `reactive::useCommand` or `reactive::useCommands` function to register commands in your extension. 32 | 33 | ```ts {6-9} 34 | import { window } from 'vscode' 35 | import { defineExtension, ref, useCommand, watchEffect } from 'reactive-vscode' 36 | 37 | export = defineExtension(() => { 38 | const helloCounter = ref(0) 39 | useCommand('extension.sayHello', () => { 40 | window.showInformationMessage('Hello World') 41 | helloCounter.value++ 42 | }) 43 | 44 | watchEffect(() => { 45 | if (helloCounter.value > 99) 46 | window.showWarningMessage('You have said hello too many times!') 47 | }) 48 | }) 49 | ``` 50 | 51 | ## Caveats 52 | 53 | ### Command Palette Visibility 54 | 55 | Commands can be used as view actions, or be called by other extensions. In that case, commands may have params and shouldn't be called via the [Command Palette](https://code.visualstudio.com/api/ux-guidelines/command-palette). We should hide these commands from the Command Palette by setting the `contributes.menus[*].when` property to `false`: 56 | 57 | ```json 58 | { 59 | "contributes": { 60 | "commands": [ 61 | { 62 | "command": "extension.doSomething", 63 | "title": "This requires params" 64 | } 65 | ], 66 | "menus": { 67 | "commandPalette": [ 68 | { 69 | "command": "extension.doSomething", 70 | "when": "false" 71 | } 72 | ] 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | See the [official documentation](https://code.visualstudio.com/api/references/contribution-points#Context-specific-visibility-of-Command-Palette-menu-items) for more information. 79 | -------------------------------------------------------------------------------- /docs/guide/config.md: -------------------------------------------------------------------------------- 1 | # Configurations 2 | 3 | An extension can contribute extension-specific settings. 4 | 5 | ## Define in Manifest 6 | 7 | To define the settings in the `package.json`, you need to add the `contributes.configuration` field. The `configuration` field is an object that contains the configuration settings. 8 | 9 | ```json 10 | { 11 | "contributes": { 12 | "configuration": { 13 | "title": "My Extension", 14 | "properties": { 15 | "myExtension.enable": { 16 | "type": "boolean", 17 | "default": true, 18 | "description": "Enable My Extension" 19 | }, 20 | "myExtension.greeting": { 21 | "type": ["string", "null"], 22 | "default": "Hello!", 23 | "description": "Greeting messag. Set to null to disable" 24 | } 25 | } 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | Visit the [official documentation](https://code.visualstudio.com/api/references/contribution-points#contributes.configuration) for more information. 32 | 33 | ## Use in Extension 34 | 35 | To use the settings in the extension, you can use the `reactive::defineConfigs` function to define the configuration. The following code is corresponding to the above configuration. 36 | 37 | ```ts 38 | import { defineConfigs } from 'reactive-vscode' 39 | 40 | const { enable, greeting } = defineConfigs('your-extension', { 41 | enable: Boolean, 42 | greeting: [String, null], 43 | }) 44 | ``` 45 | 46 | Note that you should always set the default value in the manifest file. `reactive::defineConfigs` does not provide default values. 47 | 48 | In the above example, `enable` is of type `ConfigRef`, which extends `Ref`. Note that setting `enable.value` will not update the configuration. You should use `enable.update` instead. 49 | 50 | ```ts 51 | import { defineConfigs } from 'reactive-vscode' 52 | 53 | const { enable, greeting } = defineConfigs('your-extension', { 54 | enable: Boolean, 55 | greeting: [String, null], 56 | }) 57 | // ---cut--- 58 | // This will not update the configuration. Only the value in the memory is changed. 59 | enable.value = false 60 | 61 | // This will write the value back to the configuration. 62 | enable.update(false) 63 | ``` 64 | 65 | The `update` method also accepts `configurationTarget` and `overrideInLanguage` options. Visit the [official documentation](https://code.visualstudio.com/api/references/vscode-api#WorkspaceConfiguration.update) for more information. 66 | -------------------------------------------------------------------------------- /docs/guide/context.md: -------------------------------------------------------------------------------- 1 | # Custom contexts 2 | 3 | VSCode's [when clause contexts](https://code.visualstudio.com/api/references/when-clause-contexts) can be used to selectively enable or disable extension commands and UI elements, such as menus and views. provides `reactive::useVscodeContext` to define custom contexts in a reactive way. 4 | 5 | 6 | 7 | ```ts 8 | import { computed, defineExtension, ref, useVscodeContext } from 'reactive-vscode' 9 | 10 | export = defineExtension(() => { 11 | const contextA = useVscodeContext('demo.fromValue', true) // [!code highlight] 12 | const contextB = useVscodeContext('demo.fromRef', contextA) // [!code highlight] 13 | const contextC = useVscodeContext('demo.fromGetter', () => !contextA.value) // [!code highlight] 14 | }) 15 | ``` 16 | 17 | Note that `contextA` and `contextB` are `ref`s, which means you can set them later, and the context will be updated accordingly. `contextC` is a `computed` value, which means it will be updated automatically when `contextA` changes. 18 | 19 | For more information about when clause contexts, please refer to the [official documentation](https://code.visualstudio.com/api/references/when-clause-contexts). 20 | -------------------------------------------------------------------------------- /docs/guide/disposable.md: -------------------------------------------------------------------------------- 1 | # Disposables 2 | 3 | Although most of the VSCode API is covered by , sometimes you still need to work with `vscode::Disposable`, which is also described in [VSCode API Patterns](https://code.visualstudio.com/api/references/vscode-api#disposables). 4 | 5 | `reactive::useDisposable` accepts a disposable object and automatically disposes it when the current effect scope is disposed (e.g., when the extension is deactivated, if `vscode::useDisposable` is called in the extension's setup function). `reactive::useDisposable` returns the disposable object itself as is. 6 | 7 | ```ts 8 | import { defineExtension, useDisposable } from 'reactive-vscode' 9 | import type { TextDocument } from 'vscode' 10 | import { languages } from 'vscode' 11 | 12 | export = defineExtension(() => { 13 | useDisposable(languages.registerFoldingRangeProvider( 14 | { language: 'markdown' }, 15 | { 16 | provideFoldingRanges(document: TextDocument) { 17 | return [] 18 | }, 19 | }, 20 | )) 21 | }) 22 | ``` 23 | 24 | Note that you needn't to use `reactive::useDisposable` for disposables created by any functions. They are automatically disposed when the current effect scope is disposed. 25 | -------------------------------------------------------------------------------- /docs/guide/editor.md: -------------------------------------------------------------------------------- 1 | # Editor and Document 2 | 3 | Editors and documents are the core of the VS Code extension development. In this guide, we will learn how to interact with the editor and document. 4 | 5 | ## The Active Editor 6 | 7 | The active editor is the one that currently has focus or, when none has focus, the one that has changed input most recently. The `reactive::useActiveTextEditor` and `reactive::useActiveNotebookEditor` composable can be used to get the active text editor. 8 | 9 | ```ts 10 | import { defineExtension, useActiveNotebookEditor, useActiveTextEditor, watchEffect } from 'reactive-vscode' 11 | import type { ExtensionContext } from 'vscode' 12 | 13 | export = defineExtension(() => { 14 | const textEditor = useActiveTextEditor() // [!code highlight] 15 | const notebookEditor = useActiveNotebookEditor() // [!code highlight] 16 | 17 | watchEffect(() => { 18 | console.log('Active Text Editor:', textEditor.value) 19 | console.log('Active Notebook Editor:', notebookEditor.value) 20 | // ^? 21 | }) 22 | }) 23 | ``` 24 | 25 | ## Visible Editors 26 | 27 | The `reactive::useVisibleTextEditors` and `reactive::useVisibleNotebookEditors` composable can be used to get the visible text editors. 28 | 29 | ```ts 30 | import { defineExtension, useVisibleNotebookEditors, useVisibleTextEditors, watchEffect } from 'reactive-vscode' 31 | 32 | export = defineExtension(() => { 33 | const textEditors = useVisibleTextEditors() 34 | const notebookEditors = useVisibleNotebookEditors() 35 | 36 | watchEffect(() => { 37 | console.log('Visible Text Editors:', textEditors.value) 38 | console.log('Visible Notebook Editors:', notebookEditors.value) 39 | // ^? 40 | }) 41 | }) 42 | ``` 43 | 44 | ## Get Editor Document 45 | 46 | - `vscode::TextEditor.document` is the document associated with this text editor. 47 | 48 | - `vscode::NotebookEditor.notebook` the [notebook document](https://code.visualstudio.com/api/references/vscode-api#NotebookDocument) associated with this notebook editor. 49 | 50 | The document will be the same for the entire lifetime of this text editor or notebook editor. 51 | 52 | ```ts 53 | import { computed, defineExtension, useActiveTextEditor } from 'reactive-vscode' 54 | import type { ExtensionContext } from 'vscode' 55 | 56 | export = defineExtension(() => { 57 | const textEditor = useActiveTextEditor() 58 | const document = computed(() => textEditor.value?.document) // [!code highlight] 59 | // ^? 60 | }) 61 | ``` 62 | 63 | ## Document Text 64 | 65 | The `reactive::useDocumentText` composable can be used to get the text of the active document. 66 | 67 | ```ts 68 | import { computed, defineExtension, useActiveTextEditor, useDocumentText } from 'reactive-vscode' 69 | import type { ExtensionContext } from 'vscode' 70 | 71 | export = defineExtension(() => { 72 | const textEditor = useActiveTextEditor() 73 | const document = computed(() => textEditor.value?.document) 74 | const text = useDocumentText(document) // [!code highlight] 75 | // ^? 76 | }) 77 | ``` 78 | 79 | The returned `text` is settable, which means you can update the text of the document. 80 | 81 | 82 | ```ts 83 | import { computed, defineExtension, ref, useActiveTextEditor, useDocumentText, watchEffect } from 'reactive-vscode' 84 | import type { ExtensionContext } from 'vscode' 85 | 86 | export = defineExtension(() => { 87 | const editor = useActiveTextEditor() 88 | const text = useDocumentText(() => editor.value?.document) 89 | 90 | // Reactive, may be set from other places 91 | const name = ref('John Doe') 92 | 93 | watchEffect(() => { 94 | text.value = `Hello, ${name.value}!` // [!code highlight] 95 | }) 96 | }) 97 | ``` 98 | 99 | ## Editor Decoration 100 | 101 | The `reactive::useEditorDecorations` composable can be used to add decorations to the editor. 102 | 103 | ```ts {5-9} 104 | import { defineExtension, useActiveTextEditor, useEditorDecorations } from 'reactive-vscode' 105 | 106 | export = defineExtension(() => { 107 | const editor = useActiveTextEditor() 108 | useEditorDecorations( 109 | editor, 110 | { backgroundColor: 'red' }, // Or created decoration type 111 | () => [/* Dynamic caculated ranges */] // Or Ref/Computed 112 | ) 113 | }) 114 | ``` 115 | 116 | See `vscode::TextEditor.setDecorations` for more information. To create a decoration type, use `vscode::window.createTextEditorDecorationType`. 117 | 118 | ## Editor Selections 119 | 120 | The following 4 composable can be used to **get and set** the selections of editors. 121 | 122 | - `reactive::useTextEditorSelections` - All selections in the text editor. 123 | - `reactive::useTextEditorSelection` - The primary selection in the text editor. 124 | - `reactive::useNotebookEditorSelections` - All selections in the notebook editor. 125 | - `reactive::useNotebookEditorSelection` - The primary selection in the notebook editor. 126 | 127 | See their docs for more information. Note that `reactive::useTextEditorSelections` and `reactive::useTextEditorSelection` also support an `acceptKind` option to filter the change kind which has triggered this event (See `vscode::TextEditorSelectionChangeKind`). 128 | 129 | ## Editor Viewport 130 | 131 | The following 3 composable can be used to **get** the viewport information of editors. 132 | 133 | - `reactive::useTextEditorViewColumn` - The view column of the text editor. 134 | - `reactive::useTextEditorVisibleRanges` - The visible ranges of the text editor. 135 | - `reactive::useNotebookEditorVisibleRanges` - The visible ranges of the notebook editor. 136 | 137 | See their docs for more information. 138 | -------------------------------------------------------------------------------- /docs/guide/event.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | Although most of the VSCode API is covered by , sometimes you still need to create or listen original [VSCode events](https://code.visualstudio.com/api/references/vscode-api#events). 4 | 5 | `reactive::useEvent` converts an raw event to a auto-disposed event: 6 | 7 | ```ts 8 | import { defineExtension, useEvent } from 'reactive-vscode' 9 | import { workspace } from 'vscode' 10 | 11 | const onDidCreateFiles = useEvent(workspace.onDidCreateFiles) 12 | 13 | export = defineExtension(() => { 14 | // No need to dispose the event 15 | onDidCreateFiles((e) => { 16 | console.log('Files created:', e.files) 17 | }) 18 | }) 19 | ``` 20 | 21 | `reactive::useEventEmitter` creates a frindly event emitter, which still extends `vscode::EventEmitter`: 22 | 23 | 24 | ```ts 25 | import type { Event } from 'vscode' 26 | declare function someVscodeApi(options: { onSomeEvent: Event }): void 27 | // ---cut--- 28 | import { defineExtension, useEventEmitter } from 'reactive-vscode' 29 | 30 | export = defineExtension(() => { 31 | const myEvent = useEventEmitter([/* optional listenrs */]) 32 | 33 | myEvent.addListener((msg) => { 34 | console.log(`Received message: ${msg}`) 35 | }) 36 | 37 | myEvent.fire('Hello, World!') 38 | 39 | someVscodeApi({ 40 | onSomeEvent: myEvent.event, 41 | }) 42 | }) 43 | ``` 44 | 45 | You can also convert a raw event to a friendly event emitter: 46 | 47 | ```ts {6} 48 | import { defineExtension, useEventEmitter } from 'reactive-vscode' 49 | import { EventEmitter } from 'vscode' 50 | 51 | export = defineExtension(() => { 52 | const rawEvent = new EventEmitter() 53 | const myEvent = useEventEmitter(rawEvent, [/* optional listenrs */]) 54 | }) 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/guide/extension.md: -------------------------------------------------------------------------------- 1 | # Extension 2 | 3 | It's simple to create a VSCode extension with . You just need to define your extension code inside the `reactive::defineExtension` function, and export the returned `activate` and `deactivate` functions. 4 | 5 | ```ts 6 | import { defineExtension } from 'reactive-vscode' 7 | 8 | export = defineExtension(() => { 9 | // Setup your extension here 10 | }) 11 | ``` 12 | 13 | ::: details TypeScript Configuration 14 | VSCode extensions should be CommonJS modules. Since `export =` statement is not allowed in ESM, you need to add this in your `tsconfig.json` to make TypeScript happy if you use a Bundler like `tsup`. 15 | 16 | ```json 17 | { 18 | "compilerOptions": { 19 | "moduleResolution": "Bundler", 20 | "module": "Preserve" 21 | } 22 | } 23 | ``` 24 | 25 | Or you can avoid the `export =` statement in this way: 26 | 27 | ```ts 28 | import { defineExtension } from 'reactive-vscode' 29 | const { activate, deactivate } = defineExtension(() => { 30 | // Your extension code here 31 | }) 32 | export { activate, deactivate } 33 | ``` 34 | ::: 35 | 36 | ## The Setup Function 37 | 38 | Like the `setup` function in Vue 3, the setup function in is a function that defines how your extension should behave. When the extension is activated, this function will be called once. 39 | 40 | You can do these things in the setup function: 41 | 42 | - Register commands (via `reactive::useCommand` or `reactive::useCommands`) 43 | - Register views (covered in [the next section](./view.md)) 44 | - Define other (reactive) logics (via `vue::watchEffect` or `vue::watch`, etc.) 45 | - Use other composables (like `reactive::useActiveTextEditor`) 46 | 47 | Here is an example: 48 | 49 | 50 | ```ts 51 | import type { Ref } from 'reactive-vscode' 52 | /** 53 | * Defined via `defineConfigs` 54 | */ 55 | declare const message: Ref 56 | // ---cut--- 57 | import { defineExtension, useCommand, useIsDarkTheme, useLogger, watchEffect } from 'reactive-vscode' 58 | import { window } from 'vscode' 59 | import { useDemoTreeView } from './treeView' 60 | 61 | export = defineExtension(() => { 62 | const logger = useLogger('Reactive VSCode') 63 | logger.info('Extension Activated') 64 | logger.show() 65 | 66 | useCommand('reactive-vscode-demo.helloWorld', () => { 67 | window.showInformationMessage(message.value) 68 | }) 69 | 70 | const isDark = useIsDarkTheme() 71 | watchEffect(() => { 72 | logger.info('Is Dark Theme:', isDark.value) 73 | }) 74 | 75 | useDemoTreeView() 76 | }) 77 | ``` 78 | 79 | ## The Extension Context 80 | 81 | The [extension context](https://code.visualstudio.com/api/references/vscode-api#ExtensionContext) can be imported from `reactive-vscode`. It is a global `shallowRef` that contains the `vscode::ExtensionContext` object. 82 | 83 | ```ts 84 | import { extensionContext } from 'reactive-vscode' 85 | 86 | extensionContext.value?.extensionPath 87 | // ^? 88 | ``` 89 | 90 |
91 | 92 | A common use case is to get the absolute path of some resources in your extension. In this case, you can use `reactive::useAbsolutePath` as a shortcut. 93 | -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: [2,3] 3 | --- 4 | 5 | # Get Started 6 | 7 | Reactive VSCode is a library that helps you develop Visual Studio Code extensions with [Vue's Reactivity API](https://vuejs.org/api/reactivity-core.html). We assume you are already familiar with the basic ideas of [Vue's Reactivity API](https://vuejs.org/guide/essentials/reactivity-fundamentals.html). 8 | 9 | Read [Why reactive-vscode](./why.md) for more information about why is created. 10 | 11 | ## Create a New Project 12 | 13 | ::: code-group 14 | 15 | ```bash [pnpm] 16 | pnpm create reactive-vscode 17 | ``` 18 | 19 | ```bash [npm] 20 | npm init reactive-vscode@latest 21 | ``` 22 | 23 | ```bash [yarn] 24 | yarn create reactive-vscode 25 | ``` 26 | 27 | ::: 28 | 29 | Or you can add it to an existing project by installing the `reactive-vscode` package. 30 | 31 | ## Package Exports 32 | 33 | The package exports the following: 34 | 35 | - Utility functions and types, like `reactive::defineExtension` 36 | - Wrappers of VSCode APIs as composables, like `reactive::useActiveTextEditor` 37 | - All exports from `npm::@vue/reactivity`, like `vue::ref(https://vuejs.org/api/reactivity-core.html#ref)` 38 | - Exports that are useful for VSCode extension from `npm::@vue/runtime-core`, like `vue::watchEffect(https://vuejs.org/api/reactivity-core.html#watcheffect)` 39 | 40 | You can find all the implemented composables [here](../functions/index.md). They will be documented in the future. 41 | 42 | ## Extension Basics 43 | 44 | ### Extension Manifest 45 | 46 | Each VS Code extension must have a `package.json` as its [Extension Manifest](https://code.visualstudio.com/api/get-started/extension-anatomy#extension-manifest). Please visit the [official documentation](https://code.visualstudio.com/api/get-started/extension-anatomy#extension-manifest) for more information. 47 | 48 | ### Extension Entry File 49 | 50 | Usually, the [extension entry file](https://code.visualstudio.com/api/get-started/extension-anatomy#extension-entry-file) is `src/extension.ts`. You can define your extension by using the `reactive::defineExtension` function: 51 | 52 | ```ts 53 | import { defineExtension } from 'reactive-vscode' 54 | 55 | export = defineExtension(() => { 56 | // Setup your extension here 57 | }) 58 | ``` 59 | 60 | We will introduce how to write the body of your extension in [the next section](./extension.md). 61 | 62 | ## Developing the Extension 63 | 64 | 1. Open a new terminal and run the following command: 65 | 66 | ::: code-group 67 | 68 | ```bash [pnpm] 69 | pnpm dev 70 | ``` 71 | 72 | ```bash [npm] 73 | npm run dev 74 | ``` 75 | 76 | ```bash [yarn] 77 | yarn dev 78 | ``` 79 | 80 | ::: 81 | 82 | 2. Inside the editor, press F5 or run the command **Debug: Start Debugging** from the Command Palette (Ctrl+Shift+P). This will run the extension in a new window. 83 | 84 | > Visit the [VSCode Documentation](https://code.visualstudio.com/api/get-started/your-first-extension#debugging-the-extension) for more information about debugging the extension. 85 | 86 |
87 | 88 | --- 89 | 90 | ::: info [Twoslash](https://twoslash.netlify.app/) powered docs! 91 | 92 | You can hover the tokens in code blocks to see the type definitions for some uncovered functions. 93 | 94 | ::: 95 | -------------------------------------------------------------------------------- /docs/guide/terminal.md: -------------------------------------------------------------------------------- 1 | # Terminals 2 | 3 | VSCode provides a powerful terminal system that allows you to run shell commands in the integrated terminal. provides a set of composable functions to create and manage terminals in a reactive way. 4 | 5 | ## Create a Terminal 6 | 7 | `reactive::useTerminal` creates a terminal via `vscode::window.createTerminal`. The params are the same as `vscode::window.createTerminal`. 8 | 9 | ```ts 10 | import { defineExtension, useTerminal } from 'reactive-vscode' 11 | 12 | export = defineExtension(() => { 13 | const { 14 | terminal, 15 | name, 16 | processId, 17 | creationOptions, 18 | exitStatus, 19 | sendText, 20 | show, 21 | hide, 22 | state, 23 | } = useTerminal('My Terminal', '/bin/bash') 24 | }) 25 | ``` 26 | 27 | ## Create a Controlled Terminal 28 | 29 | `reactive::useControlledTerminal` creates a terminal which allows you to control its lifecycle. The params are the same as `vscode::window.createTerminal`. 30 | 31 | ```ts 32 | import { defineExtension, useControlledTerminal } from 'reactive-vscode' 33 | 34 | export = defineExtension(() => { 35 | const { 36 | terminal, 37 | getIsActive, 38 | show, 39 | sendText, 40 | close, 41 | state, 42 | } = useControlledTerminal('My Terminal', '/bin/bash') 43 | }) 44 | ``` 45 | 46 | ### The Active Terminal 47 | 48 | You can use `reactive::useActiveTerminal` to get the currently active terminal. 49 | 50 | ```ts 51 | import { defineExtension, useActiveTerminal } from 'reactive-vscode' 52 | 53 | export = defineExtension(() => { 54 | const activeTerminal = useActiveTerminal() // [!code highlight] 55 | // ^? 56 | }) 57 | ``` 58 | 59 | ### All Opened Terminals 60 | 61 | You can use `reactive::useOpenedTerminals` to get all open terminals. 62 | 63 | ```ts 64 | import { defineExtension, useOpenedTerminals } from 'reactive-vscode' 65 | 66 | export = defineExtension(() => { 67 | const terminals = useOpenedTerminals() // [!code highlight] 68 | // ^? 69 | }) 70 | ``` 71 | 72 | ### Get Terminal State 73 | 74 | You can use `reactive::useTerminalState` to get the state of an existing terminal. 75 | 76 | ```ts 77 | import { defineExtension, useActiveTerminal, useOpenedTerminals, useTerminalState } from 'reactive-vscode' 78 | 79 | export = defineExtension(() => { 80 | const activeTerminal = useActiveTerminal() 81 | const activeTerminalState = useTerminalState(activeTerminal) // [!code highlight] 82 | 83 | const allTerminals = useOpenedTerminals() 84 | const firstTerminalState = useTerminalState(() => allTerminals.value[0]) // [!code highlight] 85 | // ^? 86 | }) 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/guide/view.md: -------------------------------------------------------------------------------- 1 | # Views 2 | 3 | Views are an important part of a VSCode extension. There are two types of views in VSCode: [Tree View](https://code.visualstudio.com/api/extension-guides/tree-view) and [Webview](https://code.visualstudio.com/api/extension-guides/webview). Please read the [official UX guidelines](https://code.visualstudio.com/api/ux-guidelines/views) for a basic understanding. 4 | 5 | ## Define in Manifest 6 | 7 | As described in the [official documentation](https://code.visualstudio.com/api/references/contribution-points#contributes.viewsContainers), first, you need view containers to be defined in the `contributes.viewsContainers.[viewContainerType]` section in the `package.json`. Then you can define your views in the `contributes.views.[viewContainerId]` section. 8 | 9 | ```json 10 | { 11 | "contributes": { 12 | "viewsContainers": { 13 | "activitybar": [ 14 | { 15 | "id": "package-explorer", 16 | "title": "Package Explorer", 17 | "icon": "resources/package-explorer.svg" 18 | } 19 | ] 20 | }, 21 | "views": { 22 | "package-explorer": [ 23 | { 24 | "id": "package-dependencies", 25 | "name": "Dependencies" 26 | }, 27 | { 28 | "id": "package-outline", 29 | "name": "Outline" 30 | } 31 | ] 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | ![Custom views container](https://code.visualstudio.com/assets/api/references/contribution-points/custom-views-container.png) 38 | 39 | ## Register Tree View 40 | 41 | [Tree views](https://code.visualstudio.com/api/extension-guides/tree-view) are used to display hierarchical data. You can define a tree view by using the `reactive::useTreeView` function. 42 | 43 | Here is an example of a tree view: 44 | 45 | <<< @/snippets/treeView.ts {35-41} 46 | 47 | Then you can call the `useDemoTreeView` function every where to register the tree view and get the returned value: 48 | 49 | ```ts {2,5} 50 | import { defineExtension } from 'reactive-vscode' 51 | import { useDemoTreeView } from './treeView' 52 | 53 | export = defineExtension(() => { 54 | const demoTreeView = useDemoTreeView() 55 | // ... 56 | }) 57 | ``` 58 | 59 | The `children` property in nodes is used to define the children of the node. The `treeItem` propert is required and is used to define the tree item of the node. It should be a `vscode::TreeItem` object, or a promise that resolves to a `vscode::TreeItem` object. 60 | 61 | If you want to trigger an update based on some reactive values that aren't tracked in `treeData`, you can pass them to the `watchSource` option. 62 | 63 | ::: details About `reactive::createSingletonComposable` 64 | `reactive::createSingletonComposable` is a helper function to create a singleton composable. It will only create the composable once and return the same instance every time it is called. 65 | ::: 66 | 67 | ::: warning 68 | For the above example, `useDemoTreeView` should **not** be called top-level in the module, because the extension context is not available at that time. Instead, you should **always** call it in the `setup` function. 69 | ::: 70 | 71 | ## Register Webview 72 | 73 | [Webviews](https://code.visualstudio.com/api/extension-guides/webview) are used to display web content in the editor. You can define a webview by using the `reactive::useWebviewView` function. 74 | 75 | Here is an example of a webview: 76 | 77 | <<< @/snippets/webviewView.ts 78 | 79 | The time to call `useDemoWebviewView` is the same as the tree view in the previous section. 80 | -------------------------------------------------------------------------------- /docs/guide/vueuse.md: -------------------------------------------------------------------------------- 1 | # VueUse Integration 2 | 3 | provides an optional integration of [VueUse](https://vueuse.org/) for VSCode extension development. 4 | 5 | This package contains a subset of VueUse functions that are compatible with the Node.js environment. This means functions that rely on the browser environment are removed. 6 | 7 | Also, this package uses Vue reactivity API from `npm::@reactive-vscode/reactivity` instead of `npm::vue-demi` package. This means functions that rely on Vue's rendering API are removed. 8 | 9 | ## Usage 10 | 11 | ::: code-group 12 | 13 | ```bash [pnpm] 14 | pnpm install -D @reactive-vscode/vueuse 15 | ``` 16 | 17 | ```bash [npm] 18 | npm install -D @reactive-vscode/vueuse 19 | ``` 20 | 21 | ```bash [yarn] 22 | yarn add -D @reactive-vscode/vueuse 23 | ``` 24 | 25 | ::: 26 | 27 | ```ts 28 | import { useIntervalFn } from '@reactive-vscode/vueuse' 29 | import { defineExtension } from 'reactive-vscode' 30 | 31 | export = defineExtension(() => { 32 | useIntervalFn(() => { 33 | console.log('Hello World') 34 | }, 1000) 35 | }) 36 | ``` 37 | 38 | ## Available Functions 39 | 40 | Every VueUse function that is compatible with the Node.js environment and doesn't require Vue's rendering API is available in this package. Check out [`packages/vueuse/src/index.ts`](https://github.com/KermanX/reactive-vscode/blob/main/packages/vueuse/src/index.ts) for the full list. 41 | -------------------------------------------------------------------------------- /docs/guide/why.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: 'deep' 3 | --- 4 | 5 | # Why 6 | 7 | VSCode extensions are powerful tools to enhance your development experience. But developing a VSCode extension is not easy. This library is created to help you develop a VSCode extension with Vue's reactivity system. 8 | 9 | ## The Problems 10 | 11 | Developing a VSCode extension is not easy. The official APIs are kind of primitive, which has several problems: 12 | 13 | ### Hard to watch states 14 | 15 | The official API is event-based, which means you have to listen to events to watch the state. This produces a lot of redundant code, and not familiar to Vue developers. 16 | 17 | ### The Disposables 18 | 19 | Disposables are everywhere in a VSCode extension. You have to store all of them to `context.subscriptions`, or dispose them manually. 20 | 21 | ### When to Initialize 22 | 23 | Views in a VSCode extension are created lazily. If you want to access a view instance, you have to store it, and even listen to a event which is fired when the view is created. 24 | 25 | ### Want to use Vue 26 | 27 | Vue's reactivity system is powerful. It's much easier to watch states and update views with Vue's reactivity system. But VSCode APIs are not designed to work with Vue. 28 | 29 | ## The solution 30 | 31 | [Vue's Reactivity API](https://vuejs.org/api/reactivity-core.html) is all you need. This library wraps most of the VSCode APIs into [Vue Composables](https://vuejs.org/guide/reusability/composables.html). You can use them as you use Vue Reactivity API, which is familiar to Vue developers. 32 | 33 | With the help of this library, you can develop a VSCode extension just like developing a Vue 3 web application. You can use Vue's reactivity system to watch states, and implement views as Vue composables. 34 | 35 | ### Result 36 | 37 | Here is an example which shows how this library can help you develop a VSCode extension. The following extension decorates the active text editor depending on a configuration. 38 | 39 | ::: code-group 40 | 41 | <<< ../examples/editor-decoration/1.ts [] 42 | 43 | <<< ../examples/editor-decoration/2.ts [Original VSCode API] 44 | 45 | ::: 46 | 47 | As you can see, after using , the code is much cleaner and easier to understand. With composables like `reactive::useActiveTextEditor` provided by this library, you can use Vue's reactivity API like `vue::watchEffect(https://vuejs.org/api/reactivity-core.html#watcheffect)` smoothly when developing a VSCode extension. 48 | 49 | More examples [here](../examples/){target="_blank"}. 50 | 51 | ## FAQ 52 | 53 | ### Why use `npm::@vue/runtime-core` internally? 54 | 55 | This library is built on top of `npm::@vue/reactivity`, and ported some code from `npm::@vue/runtime-core` (See [the `./packages/core/src/reactivity` directory](https://github.com/KermanX/reactive-vscode/blob/main/packages/core/src/reactivity)). 56 | 57 | The size of the minimal extension built with this library is about 12KB. 58 | 59 | ### Use Vue in Webview? 60 | 61 | This library is **not** designed for using Vue in a webview. If you want to use Vue in a webview, you can use the CDN version of Vue or bundler plugins like `npm::@tomjs/vite-plugin-vscode`. 62 | -------------------------------------------------------------------------------- /docs/guide/window.md: -------------------------------------------------------------------------------- 1 | # Window and Workspace 2 | 3 | ## Theme 4 | 5 | You may want to apply different styles to your extension based on the current theme. Although many of the APIs (like `vscode::TreeItem.iconPath`) have built-in support for dual themes, some don't. You may also want to sync the theme in your webview. 6 | 7 | The `reactive::useActiveColorTheme` and `reactive::useIsDrakTheme` composable can be used to get the current theme and whether it's dark or not. 8 | 9 | ```ts {5,6} 10 | import { defineExtension, useActiveColorTheme, useIsDarkTheme, watchEffect } from 'reactive-vscode' 11 | import { useDemoWebviewView } from './webviewView' 12 | 13 | export = defineExtension(() => { 14 | const theme = useActiveColorTheme() 15 | const isDark = useIsDarkTheme() 16 | 17 | const webviewView = useDemoWebviewView() 18 | 19 | watchEffect(() => { 20 | webviewView.postMessage({ 21 | type: 'updateTheme', 22 | isDark: isDark.value, 23 | // ^? 24 | }) 25 | }) 26 | }) 27 | ``` 28 | 29 | ## Window State 30 | 31 | The `reactive::useWindowState` composable can be used to get the current window state: 32 | 33 | - `vscode::WindowState.active` - Whether the window has been interacted with recently. This will change immediately on activity, or after a short time of user inactivity. 34 | - `vscode::WindowState.focused` - Whether the current window is focused. 35 | 36 | ```ts {4} 37 | import { defineExtension, useWindowState, watchEffect } from 'reactive-vscode' 38 | 39 | export = defineExtension(() => { 40 | const { active: isWindowActive, focused: isWindowFocused } = useWindowState() 41 | 42 | watchEffect(() => { 43 | console.log('Window is active:', isWindowActive.value) 44 | console.log('Window is focused:', isWindowFocused.value) 45 | }) 46 | }) 47 | ``` 48 | 49 | ## Workspace Folders 50 | 51 | The `reactive::useWorkspaceFolders` composable can be used to get the workspace folders: 52 | 53 | ```ts {4} 54 | import { defineExtension, useWorkspaceFolders, watchEffect } from 'reactive-vscode' 55 | 56 | export = defineExtension(() => { 57 | const workspaceFolders = useWorkspaceFolders() 58 | 59 | watchEffect(() => { 60 | console.log('There are', workspaceFolders.value?.length, 'workspace folders') 61 | // ^? 62 | }) 63 | }) 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: Reactive VSCode # 'Reactive VSCode' 7 | text: "Extension API" 8 | tagline: | 9 | Develop Extension with Composition API 10 | image: /logo.svg 11 | actions: 12 | - theme: brand 13 | text: Get Started 14 | link: /guide/ 15 | - theme: alt 16 | text: Why? 17 | link: /guide/why 18 | - theme: alt 19 | text: Functions 20 | link: /functions 21 | - theme: alt 22 | text: Examples 23 | link: /examples/ 24 | 25 | features: 26 | - icon: 🚀 27 | title: Easy to use 28 | details: Familiar Vue Reactivity API 29 | - icon: 🦾 30 | title: Feature rich 31 | details: Most of the VSCode APIs included 32 | - icon: ⚡ 33 | title: Fully tree shakeable 34 | details: Only take what you want 35 | --- 36 | 37 | 40 | 41 |
42 | 43 | ::: code-group 44 | 45 | <<< ./examples/editor-decoration/1.ts [] 46 | 47 | <<< ./examples/editor-decoration/2.ts [Original VSCode API] 48 | 49 | ::: 50 | 51 | 56 | 57 |
58 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactive-vscode/docs", 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vitepress dev", 6 | "build": "vitepress build", 7 | "preview": "vitepress preview" 8 | }, 9 | "devDependencies": { 10 | "@iconify-json/carbon": "^1.1.34", 11 | "@iconify-json/vscode-icons": "^1.1.34", 12 | "@reactive-vscode/metadata": "workspace:*", 13 | "@reactive-vscode/vueuse": "workspace:*", 14 | "@shikijs/vitepress-twoslash": "^1.6.1", 15 | "@types/vscode": "^1.89.0", 16 | "@unocss/preset-attributify": "^0.60.3", 17 | "@unocss/preset-icons": "^0.60.3", 18 | "@unocss/preset-uno": "^0.60.3", 19 | "@unocss/transformer-directives": "^0.60.3", 20 | "@unocss/transformer-variant-group": "^0.60.3", 21 | "@vueuse/core": "^10.10.0", 22 | "fuse.js": "^7.0.0", 23 | "reactive-vscode": "workspace:*", 24 | "unocss": "^0.60.3", 25 | "unplugin-vue-components": "^0.27.0", 26 | "vitepress": "^1.2.2", 27 | "vue": "^3.4.27" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/reactive-vscode/a3a7941118bcb1724ce8ecdaea8958f806d10971/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/reactive-vscode/a3a7941118bcb1724ce8ecdaea8958f806d10971/docs/public/header.png -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/snippets/treeView.ts: -------------------------------------------------------------------------------- 1 | import type { TreeViewNode } from 'reactive-vscode' 2 | import { computed, createSingletonComposable, useTreeView } from 'reactive-vscode' 3 | import { TreeItemCollapsibleState } from 'vscode' 4 | 5 | export const useDemoTreeView = createSingletonComposable(() => { 6 | function getRootNode(index: number) { 7 | return { 8 | children: [ 9 | getChildNode(index * 10 + 1), 10 | getChildNode(index * 10 + 2), 11 | ], 12 | treeItem: { 13 | label: `Root ${index}`, 14 | collapsibleState: TreeItemCollapsibleState.Expanded, 15 | }, 16 | } 17 | } 18 | 19 | function getChildNode(index: number) { 20 | return { 21 | treeItem: { 22 | label: `Child ${index}`, 23 | collapsibleState: TreeItemCollapsibleState.None, 24 | }, 25 | } 26 | } 27 | 28 | const treeData = computed(() => { 29 | const roots: TreeViewNode[] = [] 30 | for (let i = 1; i < 5; i++) 31 | roots.push(getRootNode(i)) 32 | return roots 33 | }) 34 | 35 | const view = useTreeView( 36 | 'reactive-tree-view', 37 | treeData, 38 | { 39 | title: () => `Tree with ${treeData.value.length} roots`, 40 | }, 41 | ) 42 | 43 | // return anything you want to expose 44 | return view 45 | }) 46 | -------------------------------------------------------------------------------- /docs/snippets/webviewView.ts: -------------------------------------------------------------------------------- 1 | import { computed, createSingletonComposable, ref, useWebviewView } from 'reactive-vscode' 2 | 3 | export const useDemoWebviewView = createSingletonComposable(() => { 4 | const message = ref('') 5 | const html = computed(() => ` 6 | 15 |

${message.value}

16 |
17 | 18 | 19 |
20 | `) 21 | 22 | const { postMessage } = useWebviewView( 23 | 'reactive-webview-view', 24 | html, 25 | { 26 | webviewOptions: { 27 | enableScripts: true, 28 | enableCommandUris: true, 29 | }, 30 | onDidReceiveMessage(ev) { 31 | if (ev.type === 'updateMessage') 32 | message.value = ev.message 33 | }, 34 | }, 35 | ) 36 | 37 | return { message, postMessage } 38 | }) 39 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": [ 5 | "DOM" 6 | ], 7 | "module": "Preserve" 8 | }, 9 | "include": [ 10 | ".vitepress/**/*", 11 | "**/*" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import antfu from '@antfu/eslint-config' 3 | 4 | export default antfu( 5 | { 6 | ignores: [ 7 | 'packages/reactivity/**/*.ts', 8 | ], 9 | }, 10 | { 11 | files: ['**/*.ts'], 12 | rules: { 13 | 'node/no-exports-assign': 'off', 14 | 'no-restricted-syntax': 'off', 15 | }, 16 | }, 17 | ) 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "version": "0.2.0-beta.3", 4 | "packageManager": "pnpm@9.4.0", 5 | "scripts": { 6 | "dev": "nr demo:dev", 7 | "lint": "eslint .", 8 | "release": "bumpp package.json packages/*/package.json --all", 9 | "test": "nr -C test test", 10 | "prepare": "simple-git-hooks", 11 | "core:build": "nr --filter reactive-vscode... build", 12 | "demo:dev": "nr -C demo dev", 13 | "demo:build": "pnpm --filter demo... build", 14 | "metadata:dev": "nr -C packages/metadata dev", 15 | "metadata:build": "nr -C packages/metadata build", 16 | "creator:dev": "nr -C packages/creator dev", 17 | "creator:build": "nr -C packages/creator build", 18 | "docs:dev": "pnpm --parallel --filter docs... dev", 19 | "docs:build": "pnpm --filter docs... build", 20 | "docs:preview": "nr -C docs preview", 21 | "typecheck": "nr -r --parallel typecheck" 22 | }, 23 | "devDependencies": { 24 | "@antfu/eslint-config": "^2.21.1", 25 | "@antfu/ni": "^0.21.12", 26 | "bumpp": "^9.4.1", 27 | "eslint": "^9.5.0", 28 | "lint-staged": "^15.2.7", 29 | "pnpm": "^9.4.0", 30 | "simple-git-hooks": "^2.11.1", 31 | "typescript": "^5.5.2", 32 | "zx": "^8.1.3" 33 | }, 34 | "simple-git-hooks": { 35 | "pre-commit": "pnpm lint-staged" 36 | }, 37 | "lint-staged": { 38 | "*": "eslint --fix" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactive-vscode", 3 | "type": "module", 4 | "version": "0.2.0-beta.3", 5 | "description": "Develop VSCode extension with Vue Reactivity API", 6 | "author": "_Kerman ", 7 | "license": "MIT", 8 | "homepage": "https://github.com/KermanX/reactive-vscode#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/KermanX/reactive-vscode.git" 12 | }, 13 | "bugs": "https://github.com/KermanX/reactive-vscode/issues", 14 | "keywords": [ 15 | "vscode", 16 | "extension", 17 | "extension-development", 18 | "vue", 19 | "composition-api", 20 | "reactive", 21 | "reactivity" 22 | ], 23 | "sideEffects": false, 24 | "exports": { 25 | ".": { 26 | "types": "./src/index.ts", 27 | "import": "./src/index.ts" 28 | } 29 | }, 30 | "main": "./src/index.ts", 31 | "types": "./src/index.ts", 32 | "files": [ 33 | "README.md", 34 | "dist" 35 | ], 36 | "scripts": { 37 | "typecheck": "tsc --noEmit", 38 | "build": "vite build", 39 | "prepublishOnly": "pnpm typecheck && pnpm build" 40 | }, 41 | "peerDependencies": { 42 | "@types/vscode": "^1.89.0" 43 | }, 44 | "dependencies": { 45 | "@reactive-vscode/reactivity": "workspace:*" 46 | }, 47 | "devDependencies": { 48 | "@types/node": "^20.14.2", 49 | "@types/vscode": "^1.89.0", 50 | "typescript": "^5.4.5", 51 | "vite": "^5.2.12", 52 | "vite-plugin-dts": "^3.9.1" 53 | }, 54 | "publishConfig": { 55 | "exports": { 56 | ".": { 57 | "types": "./dist/index.d.ts", 58 | "import": "./dist/index.js" 59 | } 60 | }, 61 | "main": "./dist/index.js", 62 | "types": "./dist/index.d.ts" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/core/shim.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | declare global { 3 | const __DEV__: boolean 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/src/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useAbsolutePath' 2 | export * from './useActiveColorTheme' 3 | export * from './useActiveDebugSession' 4 | export * from './useActiveEditorDecorations' 5 | export * from './useActiveNotebookEditor' 6 | export * from './useActiveTerminal' 7 | export * from './useActiveTextEditor' 8 | export * from './useAllExtensions' 9 | export * from './useCommand' 10 | export * from './useCommands' 11 | export * from './useCommentController' 12 | export * from './useControlledTerminal' 13 | export * from './useDefaultShell' 14 | export * from './useDisposable' 15 | export * from './useDocumentText' 16 | export * from './useEditorDecorations' 17 | export * from './useEvent' 18 | export * from './useEventEmitter' 19 | export * from './useFetchTasks' 20 | export * from './useFileUri' 21 | export * from './useFoldingRangeProvider' 22 | export * from './useFsWatcher' 23 | export * from './useIsDarkTheme' 24 | export * from './useIsTelemetryEnabled' 25 | export * from './useL10nText' 26 | export * from './useLogLevel' 27 | export * from './useLogger' 28 | export * from './useNotebookEditorSelection' 29 | export * from './useNotebookEditorSelections' 30 | export * from './useNotebookEditorVisibleRanges' 31 | export * from './useOpenedTerminals' 32 | export * from './useOutputChannel' 33 | export * from './useStatusBarItem' 34 | export * from './useTaskExecutions' 35 | export * from './useTerminal' 36 | export * from './useTerminalState' 37 | export * from './useTextEditorCommand' 38 | export * from './useTextEditorCommands' 39 | export * from './useTextEditorSelection' 40 | export * from './useTextEditorSelections' 41 | export * from './useTextEditorViewColumn' 42 | export * from './useTextEditorVisibleRanges' 43 | export * from './useTreeView' 44 | export * from './useUri' 45 | export * from './useViewBadge' 46 | export * from './useViewTitle' 47 | export * from './useViewVisibility' 48 | export * from './useVisibleNotebookEditors' 49 | export * from './useVisibleTextEditors' 50 | export * from './useVscodeContext' 51 | export * from './useWebviewView' 52 | export * from './useWindowState' 53 | export * from './useWorkspaceFolders' 54 | -------------------------------------------------------------------------------- /packages/core/src/composables/useAbsolutePath.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef, MaybeRefOrGetter } from '@reactive-vscode/reactivity' 2 | import { computed, toValue } from '@reactive-vscode/reactivity' 3 | import { asAbsolutePath } from '../utils/asAbsolutePath' 4 | 5 | /** 6 | * @category utilities 7 | * @reactive `ExtensionContext.asAbsolutePath` 8 | */ 9 | export function useAbsolutePath(relativePath: MaybeRefOrGetter, slient: true): ComputedRef 10 | export function useAbsolutePath(relativePath: MaybeRefOrGetter, slient?: false): ComputedRef 11 | export function useAbsolutePath(relativePath: MaybeRefOrGetter, slient = false) { 12 | return computed(() => asAbsolutePath(toValue(relativePath), slient)) 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/composables/useActiveColorTheme.ts: -------------------------------------------------------------------------------- 1 | import { shallowRef } from '@reactive-vscode/reactivity' 2 | import { window } from 'vscode' 3 | import { createSingletonComposable } from '../utils' 4 | import { useDisposable } from './useDisposable' 5 | 6 | /** 7 | * @reactive `window.activeColorTheme` 8 | */ 9 | export const useActiveColorTheme = createSingletonComposable(() => { 10 | const result = shallowRef(window.activeColorTheme) 11 | 12 | useDisposable(window.onDidChangeActiveColorTheme((theme) => { 13 | result.value = theme 14 | })) 15 | 16 | return result 17 | }) 18 | -------------------------------------------------------------------------------- /packages/core/src/composables/useActiveDebugSession.ts: -------------------------------------------------------------------------------- 1 | import { computed, shallowRef } from '@reactive-vscode/reactivity' 2 | import { debug } from 'vscode' 3 | import { createSingletonComposable } from '../utils' 4 | import { useDisposable } from './useDisposable' 5 | 6 | /** 7 | * @reactive `debug.activeDebugSession` 8 | */ 9 | export const useActiveDebugSession = createSingletonComposable(() => { 10 | const session = shallowRef(debug.activeDebugSession) 11 | 12 | useDisposable(debug.onDidChangeActiveDebugSession((ev) => { 13 | session.value = ev 14 | })) 15 | 16 | return computed(() => session.value) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/core/src/composables/useActiveEditorDecorations.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from '@reactive-vscode/reactivity' 2 | import type { DecorationOptions, DecorationRenderOptions, Range, TextEditorDecorationType } from 'vscode' 3 | import { useActiveTextEditor } from './useActiveTextEditor' 4 | import { useEditorDecorations } from './useEditorDecorations' 5 | 6 | /** 7 | * Reactively set decorations on the active editor. See `vscode::window.activeTextEditor`. 8 | * 9 | * @category editor 10 | */ 11 | export function useActiveEditorDecorations( 12 | decorationTypeOrOptions: TextEditorDecorationType | DecorationRenderOptions, 13 | rangesOrOptions: MaybeRefOrGetter, 14 | ) { 15 | const activeEditor = useActiveTextEditor() 16 | 17 | useEditorDecorations(activeEditor, decorationTypeOrOptions, rangesOrOptions) 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/composables/useActiveNotebookEditor.ts: -------------------------------------------------------------------------------- 1 | import { shallowRef } from '@reactive-vscode/reactivity' 2 | import { window } from 'vscode' 3 | import { createSingletonComposable } from '../utils' 4 | import { useDisposable } from './useDisposable' 5 | 6 | /** 7 | * @reactive `window.activeNotebookEditor` 8 | * @category editor 9 | */ 10 | export const useActiveNotebookEditor = createSingletonComposable(() => { 11 | const activeNotebookEditor = shallowRef(window.activeNotebookEditor) 12 | 13 | useDisposable(window.onDidChangeActiveNotebookEditor((editor) => { 14 | activeNotebookEditor.value = editor 15 | })) 16 | 17 | return activeNotebookEditor 18 | }) 19 | -------------------------------------------------------------------------------- /packages/core/src/composables/useActiveTerminal.ts: -------------------------------------------------------------------------------- 1 | import { shallowRef } from '@reactive-vscode/reactivity' 2 | import { window } from 'vscode' 3 | import { createSingletonComposable } from '../utils' 4 | import { useDisposable } from './useDisposable' 5 | 6 | /** 7 | * @reactive `window.activeTerminal` 8 | * @category terminal 9 | */ 10 | export const useActiveTerminal = createSingletonComposable(() => { 11 | const activeTerminal = shallowRef(window.activeTerminal) 12 | 13 | useDisposable(window.onDidChangeActiveTerminal((terminal) => { 14 | activeTerminal.value = terminal 15 | })) 16 | 17 | return activeTerminal 18 | }) 19 | -------------------------------------------------------------------------------- /packages/core/src/composables/useActiveTextEditor.ts: -------------------------------------------------------------------------------- 1 | import { shallowRef } from '@reactive-vscode/reactivity' 2 | import { window } from 'vscode' 3 | import { createSingletonComposable } from '../utils' 4 | import { useDisposable } from './useDisposable' 5 | 6 | /** 7 | * @reactive `window.activeTextEditor` 8 | * @category editor 9 | */ 10 | export const useActiveTextEditor = createSingletonComposable(() => { 11 | const activeTextEditor = shallowRef(window.activeTextEditor) 12 | 13 | useDisposable(window.onDidChangeActiveTextEditor((editor) => { 14 | activeTextEditor.value = editor 15 | })) 16 | 17 | return activeTextEditor 18 | }) 19 | -------------------------------------------------------------------------------- /packages/core/src/composables/useAllExtensions.ts: -------------------------------------------------------------------------------- 1 | import { computed, shallowRef } from '@reactive-vscode/reactivity' 2 | import { extensions } from 'vscode' 3 | import { createSingletonComposable } from '../utils' 4 | import { useDisposable } from './useDisposable' 5 | 6 | /** 7 | * @reactive `extensions.all` 8 | */ 9 | export const useAllExtensions = createSingletonComposable(() => { 10 | const allExtensions = shallowRef(extensions.all) 11 | 12 | useDisposable(extensions.onDidChange(() => { 13 | allExtensions.value = extensions.all 14 | })) 15 | 16 | return computed(() => allExtensions.value) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/core/src/composables/useCommand.ts: -------------------------------------------------------------------------------- 1 | import { commands } from 'vscode' 2 | import type { Commands } from '../utils' 3 | import { useDisposable } from './useDisposable' 4 | 5 | /** 6 | * Register a command. See `vscode::commands.registerCommand`. 7 | * 8 | * @category commands 9 | */ 10 | export function useCommand>(command: K, callback: Commands[K]) { 11 | useDisposable(commands.registerCommand(command, callback)) 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/composables/useCommands.ts: -------------------------------------------------------------------------------- 1 | import type { Commands } from '../utils' 2 | import { useCommand } from './useCommand' 3 | 4 | /** 5 | * Register multiple commands. See `vscode::commands.registerCommand`. 6 | * 7 | * @category commands 8 | */ 9 | export function useCommands(commands: Partial) { 10 | for (const [command, callback] of Object.entries(commands)) 11 | callback && useCommand(command, callback) 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/composables/useCommentController.ts: -------------------------------------------------------------------------------- 1 | import { comments } from 'vscode' 2 | import { useDisposable } from './useDisposable' 3 | 4 | /** 5 | * @reactive `comments.createCommentController` 6 | */ 7 | export function useCommentController(id: string, label: string) { 8 | return useDisposable(comments.createCommentController(id, label)) 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/composables/useControlledTerminal.ts: -------------------------------------------------------------------------------- 1 | // Ported from https://github.com/antfu/vscode-vite/blob/main/src/terminal.ts 2 | 3 | import type { ComputedRef, Ref } from '@reactive-vscode/reactivity' 4 | import { onScopeDispose, ref } from '@reactive-vscode/reactivity' 5 | import type { ExtensionTerminalOptions, Terminal, TerminalOptions, TerminalState } from 'vscode' 6 | import { window } from 'vscode' 7 | import { useTerminalState } from './useTerminalState' 8 | 9 | interface UseControlledTerminalReturn { 10 | terminal: Ref 11 | getIsActive: () => boolean 12 | show: () => void 13 | sendText: (text: string) => void 14 | close: () => void 15 | state: ComputedRef 16 | } 17 | 18 | /** 19 | * Create terminal, and allows you to control the terminal lifecycle. 20 | * 21 | * @category terminal 22 | */ 23 | export function useControlledTerminal(name?: string, shellPath?: string, shellArgs?: readonly string[] | string): UseControlledTerminalReturn 24 | export function useControlledTerminal(options: TerminalOptions): UseControlledTerminalReturn 25 | export function useControlledTerminal(options: ExtensionTerminalOptions): UseControlledTerminalReturn 26 | export function useControlledTerminal(...args: any[]): UseControlledTerminalReturn { 27 | const terminal = ref(null) 28 | 29 | function getIsActive() { 30 | return !!terminal.value && terminal.value.exitStatus == null 31 | } 32 | 33 | function ensureTerminal() { 34 | if (getIsActive()) 35 | return terminal.value! 36 | return terminal.value = window.createTerminal(...args) 37 | } 38 | 39 | function sendText(text: string) { 40 | ensureTerminal().sendText(text) 41 | } 42 | 43 | function show() { 44 | ensureTerminal().show() 45 | } 46 | 47 | function close() { 48 | if (getIsActive()) { 49 | terminal.value!.sendText('\x03') 50 | terminal.value!.dispose() 51 | terminal.value = null 52 | } 53 | } 54 | 55 | onScopeDispose(close) 56 | 57 | return { 58 | terminal, 59 | getIsActive, 60 | show, 61 | sendText, 62 | close, 63 | state: useTerminalState(terminal), 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/core/src/composables/useDefaultShell.ts: -------------------------------------------------------------------------------- 1 | import { computed, shallowRef } from '@reactive-vscode/reactivity' 2 | import { env } from 'vscode' 3 | import { createSingletonComposable } from '../utils' 4 | import { useDisposable } from './useDisposable' 5 | 6 | /** 7 | * @reactive `env.shell` 8 | */ 9 | export const useDefaultShell = createSingletonComposable(() => { 10 | const defaultShell = shallowRef(env.shell) 11 | 12 | useDisposable(env.onDidChangeShell((ev) => { 13 | defaultShell.value = ev 14 | })) 15 | 16 | return computed(() => defaultShell.value) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/core/src/composables/useDisposable.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentScope } from '@reactive-vscode/reactivity' 2 | import type { Disposable } from 'vscode' 3 | import { extensionScope } from '../utils' 4 | 5 | /** 6 | * Dispose the disposable when the current scope is disposed. See `vscode::Disposable`. 7 | * 8 | * @category lifecycle 9 | */ 10 | export function useDisposable(disposable: T): T { 11 | const scope = getCurrentScope() ?? extensionScope 12 | 13 | // @ts-expect-error internal property 14 | scope.cleanups.push(disposable.dispose.bind(disposable)) 15 | 16 | return disposable 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/composables/useDocumentText.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from '@reactive-vscode/reactivity' 2 | import { shallowRef, toValue, watchEffect } from '@reactive-vscode/reactivity' 3 | import type { TextDocument } from 'vscode' 4 | import { workspace } from 'vscode' 5 | import { useDisposable } from './useDisposable' 6 | 7 | /** 8 | * @reactive `TextDocument.getText` 9 | * @category document 10 | */ 11 | export function useDocumentText(doc: MaybeRefOrGetter) { 12 | const text = shallowRef(toValue(doc)?.getText()) 13 | 14 | watchEffect(() => { 15 | text.value = toValue(doc)?.getText() 16 | }) 17 | 18 | useDisposable(workspace.onDidChangeTextDocument((ev) => { 19 | if (ev.document === toValue(doc)) 20 | text.value = ev.document.getText() 21 | })) 22 | 23 | return text 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/composables/useEditorDecorations.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from '@reactive-vscode/reactivity' 2 | import { toValue, watchEffect } from '@reactive-vscode/reactivity' 3 | import type { DecorationOptions, DecorationRenderOptions, Range, TextEditor, TextEditorDecorationType } from 'vscode' 4 | import { window } from 'vscode' 5 | import type { Nullable } from '../utils/types' 6 | 7 | /** 8 | * Reactively set decorations on the given editor. See `vscode::TextEditor.setDecorations`. 9 | * 10 | * @category editor 11 | */ 12 | export function useEditorDecorations( 13 | editor: MaybeRefOrGetter>, 14 | decorationTypeOrOptions: TextEditorDecorationType | DecorationRenderOptions, 15 | rangesOrOptions: MaybeRefOrGetter, 16 | ) { 17 | const decorationType = 'key' in decorationTypeOrOptions ? decorationTypeOrOptions : window.createTextEditorDecorationType(decorationTypeOrOptions) 18 | 19 | watchEffect(() => { 20 | toValue(editor)?.setDecorations(decorationType, toValue(rangesOrOptions)) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/composables/useEvent.ts: -------------------------------------------------------------------------------- 1 | import type { Event } from 'vscode' 2 | import { useDisposable } from './useDisposable' 3 | 4 | /** 5 | * @category utilities 6 | * @reactive `Event` 7 | */ 8 | export function useEvent(event: Event, listeners: ((e: T) => any)[] = []) { 9 | const addListener: Event = (listener, thisArgs, disposables) => { 10 | return useDisposable(event(listener, thisArgs, disposables)) 11 | } 12 | 13 | for (const listener of listeners) 14 | addListener(listener) 15 | 16 | return addListener 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/composables/useEventEmitter.ts: -------------------------------------------------------------------------------- 1 | import type { Event } from 'vscode' 2 | import { EventEmitter } from 'vscode' 3 | import { useDisposable } from './useDisposable' 4 | import { useEvent } from './useEvent' 5 | 6 | interface UseEventEmitterReturn { 7 | event: Event 8 | fire: (data: T) => void 9 | addListener: (listener: (e: T) => any) => void 10 | } 11 | 12 | /** 13 | * @category utilities 14 | * @reactive `EventEmitter` 15 | */ 16 | export function useEventEmitter(eventEmitter?: EventEmitter, listeners?: ((e: T) => any)[]): UseEventEmitterReturn 17 | export function useEventEmitter(listeners?: ((e: T) => any)[]): UseEventEmitterReturn 18 | export function useEventEmitter(eventEmitterOrLlisteners?: EventEmitter | ((e: T) => any)[], listeners2: ((e: T) => any)[] = []) { 19 | const listeners = Array.isArray(eventEmitterOrLlisteners) ? eventEmitterOrLlisteners : listeners2 ?? [] 20 | const emitter = useDisposable(Array.isArray(eventEmitterOrLlisteners) || eventEmitterOrLlisteners == null ? new EventEmitter() : eventEmitterOrLlisteners) 21 | 22 | const addListener = useEvent(emitter.event, listeners) 23 | 24 | for (const listener of listeners) 25 | addListener(listener) 26 | 27 | return { 28 | event: emitter.event, 29 | fire: emitter.fire.bind(emitter), 30 | addListener, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/composables/useFetchTasks.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from '@reactive-vscode/reactivity' 2 | import { computed, toValue } from '@reactive-vscode/reactivity' 3 | import type { TaskFilter } from 'vscode' 4 | import { tasks } from 'vscode' 5 | 6 | /** 7 | * @reactive `tasks.fetchTasks` 8 | */ 9 | export function useFetchTasks(filter?: MaybeRefOrGetter) { 10 | return computed(() => tasks.fetchTasks(toValue(filter))) 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/composables/useFileUri.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from '@reactive-vscode/reactivity' 2 | import { toValue } from '@reactive-vscode/reactivity' 3 | import { Uri } from 'vscode' 4 | import { useUri } from './useUri' 5 | 6 | /** 7 | * @reactive `Uri.file` 8 | * @category utilities 9 | */ 10 | export function useFileUri(path: MaybeRefOrGetter) { 11 | return useUri(() => Uri.file(toValue(path))) 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/composables/useFoldingRangeProvider.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from '@reactive-vscode/reactivity' 2 | import { shallowRef, toValue, watchEffect } from '@reactive-vscode/reactivity' 3 | import type { DocumentSelector, FoldingRangeProvider } from 'vscode' 4 | import { EventEmitter, languages } from 'vscode' 5 | import { useDisposable } from './useDisposable' 6 | 7 | /** 8 | * @reactive `languages.registerFoldingRangeProvider` 9 | */ 10 | export function useFoldingRangeProvider( 11 | selector: DocumentSelector, 12 | provideFoldingRanges: MaybeRefOrGetter, 13 | ) { 14 | const changeEventEmitter = new EventEmitter() 15 | 16 | const provideFn = shallowRef() 17 | 18 | watchEffect(() => { 19 | if (provideFn.value) 20 | changeEventEmitter.fire() 21 | provideFn.value = toValue(provideFoldingRanges) 22 | }) 23 | 24 | useDisposable(languages.registerFoldingRangeProvider( 25 | selector, 26 | { 27 | onDidChangeFoldingRanges: changeEventEmitter.event, 28 | provideFoldingRanges(document, context, token) { 29 | return provideFn.value?.(document, context, token) 30 | }, 31 | }, 32 | )) 33 | } 34 | -------------------------------------------------------------------------------- /packages/core/src/composables/useFsWatcher.ts: -------------------------------------------------------------------------------- 1 | import type { GlobPattern } from 'vscode' 2 | import { workspace } from 'vscode' 3 | import { useDisposable } from './useDisposable' 4 | import { useEvent } from './useEvent' 5 | 6 | /** 7 | * @reactive `workspace.createFileSystemWatcher` 8 | */ 9 | export function useFsWatcher(globPattern: GlobPattern, ignoreCreateEvents?: boolean | undefined, ignoreChangeEvents?: boolean | undefined, ignoreDeleteEvents?: boolean | undefined) { 10 | const watcher = useDisposable(workspace.createFileSystemWatcher(globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents)) 11 | 12 | return { 13 | ...watcher, 14 | onDidCreate: useEvent(watcher.onDidCreate), 15 | onDidChange: useEvent(watcher.onDidChange), 16 | onDidDelete: useEvent(watcher.onDidDelete), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/composables/useIsDarkTheme.ts: -------------------------------------------------------------------------------- 1 | import { computed } from '@reactive-vscode/reactivity' 2 | import { ColorThemeKind } from 'vscode' 3 | import { createSingletonComposable } from '../utils' 4 | import { useActiveColorTheme } from './useActiveColorTheme' 5 | 6 | /** 7 | * Determines if the current color theme is dark. See `vscode::ColorTheme.kind`. 8 | * 9 | * @category window 10 | */ 11 | export const useIsDarkTheme = createSingletonComposable(() => { 12 | const theme = useActiveColorTheme() 13 | 14 | return computed(() => theme.value.kind === ColorThemeKind.Dark || theme.value.kind === ColorThemeKind.HighContrast) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/core/src/composables/useIsTelemetryEnabled.ts: -------------------------------------------------------------------------------- 1 | import { computed, shallowRef } from '@reactive-vscode/reactivity' 2 | import { env } from 'vscode' 3 | import { createSingletonComposable } from '../utils' 4 | import { useDisposable } from './useDisposable' 5 | 6 | /** 7 | * @reactive `env.isTelemetryEnabled` 8 | */ 9 | export const useIsTelemetryEnabled = createSingletonComposable(() => { 10 | const isTelemetryEnabled = shallowRef(env.isTelemetryEnabled) 11 | 12 | useDisposable(env.onDidChangeTelemetryEnabled((ev) => { 13 | isTelemetryEnabled.value = ev 14 | })) 15 | 16 | return computed(() => isTelemetryEnabled.value) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/core/src/composables/useL10nText.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef, MaybeRefOrGetter } from '@reactive-vscode/reactivity' 2 | import { computed, toRaw, toValue } from '@reactive-vscode/reactivity' 3 | import { l10n } from 'vscode' 4 | 5 | /** 6 | * @reactive `l10n.t` 7 | */ 8 | export function useL10nText(message: MaybeRefOrGetter, ...args: Array>): ComputedRef 9 | export function useL10nText(message: MaybeRefOrGetter, args: Record): ComputedRef 10 | export function useL10nText(message: MaybeRefOrGetter, ...args: Array> | [Record]) { 11 | return computed(() => { 12 | return typeof args[0] === 'object' 13 | ? l10n.t(toValue(message), toRaw(args[0])) 14 | : l10n.t(toValue(message), ...(args as MaybeRefOrGetter[]).map(toValue)) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/composables/useLogLevel.ts: -------------------------------------------------------------------------------- 1 | import { computed, shallowRef } from '@reactive-vscode/reactivity' 2 | import { env } from 'vscode' 3 | import { createSingletonComposable } from '../utils' 4 | import { useDisposable } from './useDisposable' 5 | 6 | /** 7 | * @reactive `env.logLevel` 8 | */ 9 | export const useLogLevel = createSingletonComposable(() => { 10 | const logLevel = shallowRef(env.logLevel) 11 | 12 | useDisposable(env.onDidChangeLogLevel((ev) => { 13 | logLevel.value = ev 14 | })) 15 | 16 | return computed(() => logLevel.value) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/core/src/composables/useLogger.ts: -------------------------------------------------------------------------------- 1 | import type { OutputChannel } from 'vscode' 2 | import { useOutputChannel } from '../composables/useOutputChannel' 3 | 4 | export function getDefaultLoggerPrefix(type: string) { 5 | const date = new Date() 6 | const year = String(date.getFullYear()).padStart(4, '0') 7 | const month = String(date.getMonth() + 1).padStart(2, '0') 8 | const day = String(date.getDate()).padStart(2, '0') 9 | const hour = String(date.getHours()).padStart(2, '0') 10 | const minute = String(date.getMinutes()).padStart(2, '0') 11 | const second = String(date.getSeconds()).padStart(2, '0') 12 | const millisecond = String(date.getMilliseconds()).padStart(3, '0') 13 | return `${year}-${month}-${day} ${hour}:${minute}:${second}.${millisecond} [${type}] ` 14 | } 15 | 16 | export interface UseLoggerOptions { 17 | outputChannel?: OutputChannel 18 | getPrefix?: ((type: string) => string) | null 19 | } 20 | 21 | /** 22 | * Creates a logger that writes to the output channel. 23 | * 24 | * @category view 25 | */ 26 | export function useLogger(name: string, options: UseLoggerOptions = {}) { 27 | const outputChannel = options.outputChannel ?? useOutputChannel(name) 28 | 29 | const createLoggerFunc = (type: string) => (...message: any[]) => { 30 | outputChannel.appendLine((options.getPrefix?.(type) ?? '') + message.join(' ')) 31 | } 32 | 33 | return { 34 | outputChannel, 35 | createLoggerFunc, 36 | info: createLoggerFunc('INFO'), 37 | warn: createLoggerFunc('WARN'), 38 | error: createLoggerFunc('ERROR'), 39 | append: outputChannel.append.bind(outputChannel), 40 | appendLine: outputChannel.appendLine.bind(outputChannel), 41 | replace: outputChannel.replace.bind(outputChannel), 42 | clear: outputChannel.clear.bind(outputChannel), 43 | show: outputChannel.show.bind(outputChannel), 44 | hide: outputChannel.hide.bind(outputChannel), 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/core/src/composables/useNotebookEditorSelection.ts: -------------------------------------------------------------------------------- 1 | import { computed } from '@reactive-vscode/reactivity' 2 | import type { NotebookEditor } from 'vscode' 3 | import type { MaybeNullableRefOrGetter } from '../utils' 4 | import { useNotebookEditorSelections } from './useNotebookEditorSelections' 5 | 6 | /** 7 | * @reactive `NotebookEditor.selection` 8 | * @category editor 9 | */ 10 | export function useNotebookEditorSelection(notebookEditor: MaybeNullableRefOrGetter) { 11 | const selections = useNotebookEditorSelections(notebookEditor) 12 | 13 | return computed({ 14 | get() { 15 | return selections.value[0] 16 | }, 17 | set(newSelection) { 18 | selections.value = selections.value.toSpliced(0, 1, newSelection) 19 | }, 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/composables/useNotebookEditorSelections.ts: -------------------------------------------------------------------------------- 1 | import { computed, shallowRef, toValue, watch } from '@reactive-vscode/reactivity' 2 | import type { NotebookEditor } from 'vscode' 3 | import { window } from 'vscode' 4 | import type { MaybeNullableRefOrGetter } from '../utils' 5 | import { useDisposable } from './useDisposable' 6 | 7 | /** 8 | * @reactive `NotebookEditor.selections` 9 | * @category editor 10 | */ 11 | export function useNotebookEditorSelections(notebookEditor: MaybeNullableRefOrGetter) { 12 | const selections = shallowRef(toValue(notebookEditor)?.selections ?? []) 13 | 14 | watch(notebookEditor, () => { 15 | selections.value = toValue(notebookEditor)?.selections ?? [] 16 | }) 17 | 18 | useDisposable(window.onDidChangeNotebookEditorSelection((ev) => { 19 | if (ev.notebookEditor === toValue(notebookEditor)) 20 | selections.value = ev.selections 21 | })) 22 | 23 | return computed({ 24 | get() { 25 | return selections.value 26 | }, 27 | set(newSelections) { 28 | selections.value = newSelections 29 | const editor = toValue(notebookEditor) 30 | if (editor) 31 | editor.selections = newSelections 32 | }, 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/composables/useNotebookEditorVisibleRanges.ts: -------------------------------------------------------------------------------- 1 | import { computed, shallowRef, toValue, watch } from '@reactive-vscode/reactivity' 2 | import type { NotebookEditor } from 'vscode' 3 | import { window } from 'vscode' 4 | import type { MaybeNullableRefOrGetter } from '../utils' 5 | import { useDisposable } from './useDisposable' 6 | 7 | /** 8 | * @reactive `NotebookEditor.visibleRanges` 9 | * @category editor 10 | */ 11 | export function useNotebookEditorVisibleRanges(notebookEditor: MaybeNullableRefOrGetter) { 12 | const ranges = shallowRef(toValue(notebookEditor)?.visibleRanges ?? []) 13 | 14 | watch(notebookEditor, () => { 15 | ranges.value = toValue(notebookEditor)?.visibleRanges ?? [] 16 | }) 17 | 18 | useDisposable(window.onDidChangeNotebookEditorVisibleRanges((ev) => { 19 | if (ev.notebookEditor === toValue(notebookEditor)) 20 | ranges.value = ev.visibleRanges 21 | })) 22 | 23 | return computed(() => ranges.value) 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/composables/useOpenedTerminals.ts: -------------------------------------------------------------------------------- 1 | import { shallowRef } from '@reactive-vscode/reactivity' 2 | import { window } from 'vscode' 3 | import { createSingletonComposable } from '../utils' 4 | import { useDisposable } from './useDisposable' 5 | 6 | /** 7 | * @reactive `window.terminals` 8 | * @category terminal 9 | */ 10 | export const useOpenedTerminals = createSingletonComposable(() => { 11 | const openedTerminals = shallowRef(window.terminals) 12 | 13 | function update() { 14 | openedTerminals.value = window.terminals 15 | } 16 | 17 | useDisposable(window.onDidOpenTerminal(update)) 18 | useDisposable(window.onDidCloseTerminal(update)) 19 | 20 | return openedTerminals 21 | }) 22 | -------------------------------------------------------------------------------- /packages/core/src/composables/useOutputChannel.ts: -------------------------------------------------------------------------------- 1 | import { window } from 'vscode' 2 | import { useDisposable } from './useDisposable' 3 | 4 | /** 5 | * @reactive `window.createOutputChannel` 6 | * @category view 7 | */ 8 | export function useOutputChannel(name: string, languageId?: string) { 9 | return useDisposable(window.createOutputChannel(name, languageId)) 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/composables/useStatusBarItem.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from '@reactive-vscode/reactivity' 2 | import { toValue, watchEffect } from '@reactive-vscode/reactivity' 3 | import type { StatusBarAlignment, StatusBarItem } from 'vscode' 4 | import { window } from 'vscode' 5 | import { useDisposable } from './useDisposable' 6 | 7 | export interface UseStatusBarItemOptions { 8 | id?: string 9 | alignment?: StatusBarAlignment 10 | priority?: number 11 | name?: MaybeRefOrGetter 12 | text?: MaybeRefOrGetter 13 | tooltip?: MaybeRefOrGetter 14 | color?: MaybeRefOrGetter 15 | backgroundColor?: MaybeRefOrGetter 16 | command?: MaybeRefOrGetter 17 | accessibilityInformation?: MaybeRefOrGetter 18 | } 19 | 20 | /** 21 | * @reactive `window.createStatusBarItem` 22 | */ 23 | export function useStatusBarItem(options: UseStatusBarItemOptions): StatusBarItem { 24 | const item = useDisposable(options.id 25 | ? window.createStatusBarItem(options.id, options.alignment, options.priority) 26 | : window.createStatusBarItem(options.alignment, options.priority)) 27 | 28 | function reactivelySet(key: string) { 29 | const value = (options as any)[key] 30 | if (value != null) 31 | watchEffect(() => (item as any)[key] = toValue(value)) 32 | } 33 | 34 | [ 35 | 'name', 36 | 'text', 37 | 'tooltip', 38 | 'color', 39 | 'backgroundColor', 40 | 'command', 41 | 'accessibilityInformation', 42 | ].forEach(reactivelySet) 43 | 44 | return item 45 | } 46 | -------------------------------------------------------------------------------- /packages/core/src/composables/useTaskExecutions.ts: -------------------------------------------------------------------------------- 1 | import { computed, shallowRef } from '@reactive-vscode/reactivity' 2 | import { tasks } from 'vscode' 3 | import { createSingletonComposable } from '../utils' 4 | import { useDisposable } from './useDisposable' 5 | 6 | /** 7 | * @reactive `tasks.taskExecutions` 8 | */ 9 | export const useTaskExecutions = createSingletonComposable(() => { 10 | const taskExecutions = shallowRef(tasks.taskExecutions) 11 | 12 | function update() { 13 | taskExecutions.value = tasks.taskExecutions 14 | } 15 | 16 | useDisposable(tasks.onDidStartTask(update)) 17 | useDisposable(tasks.onDidEndTask(update)) 18 | 19 | return computed(() => taskExecutions.value) 20 | }) 21 | -------------------------------------------------------------------------------- /packages/core/src/composables/useTerminal.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef } from '@reactive-vscode/reactivity' 2 | import type { ExtensionTerminalOptions, Terminal, TerminalOptions, TerminalState } from 'vscode' 3 | import { window } from 'vscode' 4 | import { useDisposable } from './useDisposable' 5 | import { useTerminalState } from './useTerminalState' 6 | 7 | interface UseTerminalReturn extends Omit { 8 | terminal: Terminal 9 | state: ComputedRef 10 | } 11 | 12 | /** 13 | * @category terminal 14 | * @reactive `window.createTerminal()` 15 | */ 16 | export function useTerminal(name?: string, shellPath?: string, shellArgs?: readonly string[] | string): UseTerminalReturn 17 | export function useTerminal(options: TerminalOptions): UseTerminalReturn 18 | export function useTerminal(options: ExtensionTerminalOptions): UseTerminalReturn 19 | export function useTerminal(...args: any[]): UseTerminalReturn { 20 | const terminal = useDisposable(window.createTerminal(...args)) 21 | 22 | return { 23 | terminal, 24 | get name() { 25 | return terminal.name 26 | }, 27 | get processId() { 28 | return terminal.processId 29 | }, 30 | get creationOptions() { 31 | return terminal.creationOptions 32 | }, 33 | get exitStatus() { 34 | return terminal.exitStatus 35 | }, 36 | sendText: terminal.sendText.bind(terminal), 37 | show: terminal.show.bind(terminal), 38 | hide: terminal.hide.bind(terminal), 39 | state: useTerminalState(terminal) as ComputedRef, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/src/composables/useTerminalState.ts: -------------------------------------------------------------------------------- 1 | import { computed, shallowRef, toValue, watch } from '@reactive-vscode/reactivity' 2 | import type { Terminal } from 'vscode' 3 | import { window } from 'vscode' 4 | import type { MaybeNullableRefOrGetter } from '../utils' 5 | import { useDisposable } from './useDisposable' 6 | 7 | /** 8 | * @reactive `Terminal.state` 9 | * @category terminal 10 | */ 11 | export function useTerminalState(terminal: MaybeNullableRefOrGetter) { 12 | const state = shallowRef(toValue(terminal)?.state) 13 | 14 | watch(terminal, () => { 15 | state.value = toValue(terminal)?.state 16 | }) 17 | 18 | useDisposable(window.onDidChangeTerminalState((ev) => { 19 | if (ev === toValue(terminal)) 20 | state.value = ev.state 21 | })) 22 | 23 | return computed(() => state.value) 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/composables/useTextEditorCommand.ts: -------------------------------------------------------------------------------- 1 | import type { TextEditor, TextEditorEdit } from 'vscode' 2 | import { commands } from 'vscode' 3 | import { useDisposable } from './useDisposable' 4 | 5 | export type TextEditorCommandCallback = (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void 6 | 7 | /** 8 | * Register a text editor command. See `vscode::commands.registerTextEditorCommand`. 9 | * 10 | * @category commands 11 | */ 12 | export function useTextEditorCommand(command: string, callback: TextEditorCommandCallback) { 13 | useDisposable(commands.registerTextEditorCommand(command, callback)) 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/composables/useTextEditorCommands.ts: -------------------------------------------------------------------------------- 1 | import type { TextEditorCommandCallback } from './useTextEditorCommand' 2 | import { useTextEditorCommand } from './useTextEditorCommand' 3 | 4 | /** 5 | * Register multiple text editor commands. See `vscode::commands.registerTextEditorCommand`. 6 | * 7 | * @category commands 8 | */ 9 | export function useTextEditorCommands(commands: Record) { 10 | for (const [command, callback] of Object.entries(commands)) 11 | useTextEditorCommand(command, callback) 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/composables/useTextEditorSelection.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from '@reactive-vscode/reactivity' 2 | import { computed } from '@reactive-vscode/reactivity' 3 | import type { TextEditor, TextEditorSelectionChangeKind } from 'vscode' 4 | import type { MaybeNullableRefOrGetter } from '../utils' 5 | import { useTextEditorSelections } from './useTextEditorSelections' 6 | 7 | /** 8 | * @reactive `TextEditor.selection` 9 | * @category editor 10 | */ 11 | export function useTextEditorSelection(textEditor: MaybeNullableRefOrGetter, acceptKind?: MaybeRefOrGetter<(TextEditorSelectionChangeKind | undefined)[]>) { 12 | const selections = useTextEditorSelections(textEditor, acceptKind) 13 | 14 | return computed({ 15 | get() { 16 | return selections.value[0] 17 | }, 18 | set(newSelection) { 19 | selections.value = selections.value.toSpliced(0, 1, newSelection) 20 | }, 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/composables/useTextEditorSelections.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from '@reactive-vscode/reactivity' 2 | import { computed, shallowRef, toValue, watch } from '@reactive-vscode/reactivity' 3 | import type { TextEditor, TextEditorSelectionChangeKind } from 'vscode' 4 | import { window } from 'vscode' 5 | import type { MaybeNullableRefOrGetter } from '../utils' 6 | import { useDisposable } from './useDisposable' 7 | 8 | /** 9 | * @reactive `TextEditor.selections` 10 | * @category editor 11 | */ 12 | export function useTextEditorSelections(textEditor: MaybeNullableRefOrGetter, acceptKind?: MaybeRefOrGetter<(TextEditorSelectionChangeKind | undefined)[]>) { 13 | const selections = shallowRef(toValue(textEditor)?.selections ?? []) 14 | 15 | watch(textEditor, () => { 16 | selections.value = toValue(textEditor)?.selections ?? [] 17 | }) 18 | 19 | useDisposable(window.onDidChangeTextEditorSelection((ev) => { 20 | const editor = toValue(textEditor) 21 | const kinds = toValue(acceptKind) 22 | if (ev.textEditor === editor && (!kinds || kinds.includes(ev.kind))) 23 | selections.value = ev.selections 24 | })) 25 | 26 | return computed({ 27 | get() { 28 | return selections.value 29 | }, 30 | set(newSelections) { 31 | selections.value = newSelections 32 | const editor = toValue(textEditor) 33 | if (editor) 34 | editor.selections = newSelections 35 | }, 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/src/composables/useTextEditorViewColumn.ts: -------------------------------------------------------------------------------- 1 | import { computed, shallowRef, toValue, watch } from '@reactive-vscode/reactivity' 2 | import type { TextEditor } from 'vscode' 3 | import { window } from 'vscode' 4 | import type { MaybeNullableRefOrGetter } from '../utils' 5 | import { useDisposable } from './useDisposable' 6 | 7 | /** 8 | * @reactive `TextEditor.viewColumn` 9 | * @category editor 10 | */ 11 | export function useTextEditorViewColumn(textEditor: MaybeNullableRefOrGetter) { 12 | const viewColumn = shallowRef(toValue(textEditor)?.viewColumn) 13 | 14 | watch(textEditor, () => { 15 | viewColumn.value = toValue(textEditor)?.viewColumn 16 | }) 17 | 18 | useDisposable(window.onDidChangeTextEditorViewColumn((ev) => { 19 | if (ev.textEditor === toValue(textEditor)) 20 | viewColumn.value = ev.viewColumn 21 | })) 22 | 23 | return computed(() => viewColumn.value) 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/composables/useTextEditorVisibleRanges.ts: -------------------------------------------------------------------------------- 1 | import { computed, shallowRef, toValue, watch } from '@reactive-vscode/reactivity' 2 | import type { TextEditor } from 'vscode' 3 | import { window } from 'vscode' 4 | import type { MaybeNullableRefOrGetter } from '../utils' 5 | import { useDisposable } from './useDisposable' 6 | 7 | /** 8 | * @reactive `TextEditor.visibleRanges` 9 | * @category editor 10 | */ 11 | export function useTextEditorVisibleRanges(textEditor: MaybeNullableRefOrGetter) { 12 | const ranges = shallowRef(toValue(textEditor)?.visibleRanges ?? []) 13 | 14 | watch(textEditor, () => { 15 | ranges.value = toValue(textEditor)?.visibleRanges ?? [] 16 | }) 17 | 18 | useDisposable(window.onDidChangeTextEditorVisibleRanges((ev) => { 19 | if (ev.textEditor === toValue(textEditor)) 20 | ranges.value = ev.visibleRanges 21 | })) 22 | 23 | return computed(() => ranges.value) 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/composables/useTreeView.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter, WatchSource } from '@reactive-vscode/reactivity' 2 | import { toValue, watch } from '@reactive-vscode/reactivity' 3 | import type { TreeDataProvider, TreeItem, TreeView, TreeViewOptions, ViewBadge } from 'vscode' 4 | import { window } from 'vscode' 5 | import { createKeyedComposable } from '../utils' 6 | import { useDisposable } from './useDisposable' 7 | import { useEventEmitter } from './useEventEmitter' 8 | import { useViewBadge } from './useViewBadge' 9 | import { useViewTitle } from './useViewTitle' 10 | 11 | export interface TreeViewNode { 12 | readonly children?: this[] 13 | readonly treeItem: TreeItem | Thenable 14 | } 15 | 16 | export type UseTreeViewOptions = 17 | & Omit, 'treeDataProvider'> 18 | & Pick, 'resolveTreeItem'> 19 | & { 20 | title?: MaybeRefOrGetter 21 | badge?: MaybeRefOrGetter 22 | /** 23 | * Additional watch source to trigger a change event. Useful when `treeItem` is a promise. 24 | */ 25 | watchSource?: WatchSource 26 | } 27 | 28 | /** 29 | * Register a tree view. See `vscode::window.createTreeView`. 30 | * 31 | * @category view 32 | */ 33 | export const useTreeView = createKeyedComposable( 34 | ( 35 | viewId: string, 36 | treeData: MaybeRefOrGetter, 37 | options?: UseTreeViewOptions, 38 | ): TreeView => { 39 | const changeEventEmitter = useEventEmitter() 40 | 41 | watch(treeData, () => changeEventEmitter.fire()) 42 | 43 | if (options?.watchSource) 44 | watch(options.watchSource, () => changeEventEmitter.fire()) 45 | 46 | const childrenToParentMap = new WeakMap() 47 | 48 | const view = useDisposable(window.createTreeView(viewId, { 49 | ...options, 50 | treeDataProvider: { 51 | ...options, 52 | onDidChangeTreeData: changeEventEmitter.event, 53 | getTreeItem(node: T) { 54 | return node.treeItem 55 | }, 56 | getChildren(node?: T) { 57 | if (node) { 58 | node.children?.forEach(child => childrenToParentMap.set(child, node)) 59 | return node.children 60 | } 61 | return toValue(treeData) 62 | }, 63 | getParent(node: T) { 64 | return childrenToParentMap.get(node) 65 | }, 66 | }, 67 | })) 68 | 69 | if (options?.title) 70 | useViewTitle(view, options.title) 71 | 72 | if (options?.badge) 73 | useViewBadge(view, options.badge) 74 | 75 | return view 76 | }, 77 | viewId => viewId, 78 | ) 79 | -------------------------------------------------------------------------------- /packages/core/src/composables/useUri.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from '@reactive-vscode/reactivity' 2 | import { shallowReactive, toValue } from '@reactive-vscode/reactivity' 3 | import type { Uri } from 'vscode' 4 | 5 | /** 6 | * @reactive `Uri` 7 | * @category utilities 8 | */ 9 | export function useUri(uri: MaybeRefOrGetter) { 10 | return shallowReactive({ 11 | get scheme() { 12 | return toValue(uri).scheme 13 | }, 14 | get authority() { 15 | return toValue(uri).authority 16 | }, 17 | get path() { 18 | return toValue(uri).path 19 | }, 20 | get query() { 21 | return toValue(uri).query 22 | }, 23 | get fragment() { 24 | return toValue(uri).fragment 25 | }, 26 | get fsPath() { 27 | return toValue(uri).fsPath 28 | }, 29 | with(change) { 30 | return toValue(uri).with(change) 31 | }, 32 | toString() { 33 | return toValue(uri).toString() 34 | }, 35 | toJSON() { 36 | return toValue(uri).toJSON() 37 | }, 38 | } satisfies Uri) 39 | } 40 | -------------------------------------------------------------------------------- /packages/core/src/composables/useViewBadge.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from '@reactive-vscode/reactivity' 2 | import { toValue, watchEffect } from '@reactive-vscode/reactivity' 3 | import type { TreeView, ViewBadge, WebviewView } from 'vscode' 4 | import type { Nullable } from '../utils/types' 5 | 6 | type ViewWithBadge = Pick | WebviewView, 'badge'> 7 | 8 | /** 9 | * Reactively set the badge of a view (`vscode::TreeView` or `vscode::WebviewView`). 10 | * 11 | * @category view 12 | */ 13 | export function useViewBadge( 14 | view: MaybeRefOrGetter>, 15 | badge: MaybeRefOrGetter, 16 | ) { 17 | watchEffect(() => { 18 | const viewValue = toValue(view) 19 | if (viewValue) 20 | viewValue.badge = toValue(badge) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/composables/useViewTitle.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from '@reactive-vscode/reactivity' 2 | import { toValue, watchEffect } from '@reactive-vscode/reactivity' 3 | import type { TreeView, WebviewView } from 'vscode' 4 | import type { Nullable } from '../utils/types' 5 | 6 | type ViewWithTitle = Pick | WebviewView, 'title'> 7 | 8 | /** 9 | * Reactively set the title of a view (`vscode::TreeView` or `vscode::WebviewView`). 10 | * 11 | * @category view 12 | */ 13 | export function useViewTitle( 14 | view: MaybeRefOrGetter>, 15 | title: MaybeRefOrGetter, 16 | ) { 17 | watchEffect(() => { 18 | const viewValue = toValue(view) 19 | if (viewValue) 20 | viewValue.title = toValue(title) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/composables/useViewVisibility.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef } from '@reactive-vscode/reactivity' 2 | import { computed, ref, toValue, watchEffect } from '@reactive-vscode/reactivity' 3 | import type { TreeView, WebviewView } from 'vscode' 4 | import type { MaybeNullableRefOrGetter } from '../utils/types' 5 | 6 | type ViewWithVisibility = Pick | WebviewView, 'visible' | 'onDidChangeVisibility'> 7 | 8 | /** 9 | * Reactively get the visibility of a view (`vscode::TreeView` or `vscode::WebviewView`). 10 | * 11 | * @category view 12 | */ 13 | export function useViewVisibility(view: MaybeNullableRefOrGetter): ComputedRef { 14 | const visible = ref(toValue(view)?.visible) 15 | 16 | function update() { 17 | visible.value = toValue(view)?.visible 18 | } 19 | 20 | watchEffect((onCleanup) => { 21 | const viewValue = toValue(view) 22 | if (viewValue) { 23 | const disposable = viewValue.onDidChangeVisibility(update) 24 | onCleanup(() => disposable.dispose()) 25 | } 26 | }) 27 | 28 | watchEffect(update) 29 | 30 | // Visiblility should be readonly 31 | return computed(() => !!visible.value) 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/composables/useVisibleNotebookEditors.ts: -------------------------------------------------------------------------------- 1 | import { shallowRef } from '@reactive-vscode/reactivity' 2 | import { window } from 'vscode' 3 | import { createSingletonComposable } from '../utils' 4 | import { useDisposable } from './useDisposable' 5 | 6 | /** 7 | * @reactive `window.visibleNotebookEditors` 8 | * @category editor 9 | */ 10 | export const useVisibleNotebookEditors = createSingletonComposable(() => { 11 | const visibleNotebookEditors = shallowRef(window.visibleNotebookEditors) 12 | 13 | useDisposable(window.onDidChangeVisibleNotebookEditors((ev) => { 14 | visibleNotebookEditors.value = ev 15 | })) 16 | 17 | return visibleNotebookEditors 18 | }) 19 | -------------------------------------------------------------------------------- /packages/core/src/composables/useVisibleTextEditors.ts: -------------------------------------------------------------------------------- 1 | import { shallowRef } from '@reactive-vscode/reactivity' 2 | import { window } from 'vscode' 3 | import { createSingletonComposable } from '../utils' 4 | import { useDisposable } from './useDisposable' 5 | 6 | /** 7 | * @reactive `window.visibleTextEditors` 8 | * @category editor 9 | */ 10 | export const useVisibleTextEditors = createSingletonComposable(() => { 11 | const visibleTextEditors = shallowRef(window.visibleTextEditors) 12 | 13 | useDisposable(window.onDidChangeVisibleTextEditors((ev) => { 14 | visibleTextEditors.value = ev 15 | })) 16 | 17 | return visibleTextEditors 18 | }) 19 | -------------------------------------------------------------------------------- /packages/core/src/composables/useVscodeContext.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef, MaybeRef, MaybeRefOrGetter, Ref, WritableComputedRef } from '@reactive-vscode/reactivity' 2 | import { computed, isRef, ref, toValue, watchEffect } from '@reactive-vscode/reactivity' 3 | import { commands } from 'vscode' 4 | 5 | export function useVscodeContext( 6 | name: string, 7 | value: ComputedRef | (() => T), 8 | shouldUpdate?: MaybeRefOrGetter, 9 | ): ComputedRef 10 | export function useVscodeContext( 11 | name: string, 12 | value: WritableComputedRef, 13 | shouldUpdate?: MaybeRefOrGetter, 14 | ): WritableComputedRef 15 | export function useVscodeContext( 16 | name: string, 17 | value: MaybeRef, 18 | shouldUpdate?: MaybeRefOrGetter, 19 | ): Ref 20 | 21 | /** 22 | * Reactively set a VS Code context. See [custom when clause context](https://code.visualstudio.com/api/references/when-clause-contexts#add-a-custom-when-clause-context). 23 | * 24 | * @category lifecycle 25 | */ 26 | export function useVscodeContext( 27 | name: string, 28 | value: Ref | (() => T), 29 | shouldUpdate: MaybeRefOrGetter = true, 30 | ) { 31 | const normalized = isRef(value) ? value : typeof value === 'function' ? computed(value) : ref(value) 32 | watchEffect(() => { 33 | if (toValue(shouldUpdate)) 34 | commands.executeCommand('setContext', name, normalized.value) 35 | }) 36 | return normalized 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/src/composables/useWebviewView.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from '@reactive-vscode/reactivity' 2 | import { ref, shallowRef, toValue, watchEffect } from '@reactive-vscode/reactivity' 3 | import type { ViewBadge, WebviewOptions, WebviewView } from 'vscode' 4 | import { window } from 'vscode' 5 | import { createKeyedComposable } from '../utils' 6 | import { useDisposable } from './useDisposable' 7 | import { useViewBadge } from './useViewBadge' 8 | import { useViewTitle } from './useViewTitle' 9 | 10 | interface WebviewRegisterOptions { 11 | retainContextWhenHidden?: boolean 12 | onDidReceiveMessage?: (message: any) => void 13 | webviewOptions?: MaybeRefOrGetter 14 | title?: MaybeRefOrGetter 15 | badge?: MaybeRefOrGetter 16 | } 17 | 18 | /** 19 | * Register a webview view. See `vscode::window.registerWebviewViewProvider`. 20 | * 21 | * @category view 22 | */ 23 | export const useWebviewView = createKeyedComposable( 24 | ( 25 | viewId: string, 26 | html: MaybeRefOrGetter, 27 | options?: WebviewRegisterOptions, 28 | ) => { 29 | const view = shallowRef() 30 | const context = shallowRef() 31 | useDisposable(window.registerWebviewViewProvider( 32 | viewId, 33 | { 34 | resolveWebviewView(viewArg, contextArg) { 35 | view.value = viewArg 36 | context.value = contextArg 37 | if (options?.onDidReceiveMessage) 38 | viewArg.webview.onDidReceiveMessage(options.onDidReceiveMessage) 39 | }, 40 | }, 41 | { 42 | webviewOptions: { 43 | retainContextWhenHidden: options?.retainContextWhenHidden, 44 | }, 45 | }, 46 | )) 47 | 48 | const forceRefreshId = ref(0) 49 | 50 | function forceRefresh() { 51 | forceRefreshId.value++ 52 | } 53 | 54 | watchEffect(() => { 55 | if (view.value) 56 | view.value.webview.html = `${toValue(html)}` 57 | }) 58 | 59 | if (options?.webviewOptions) { 60 | const webviewOptions = options.webviewOptions 61 | watchEffect(() => { 62 | if (view.value) 63 | view.value.webview.options = toValue(webviewOptions) 64 | }) 65 | } 66 | 67 | if (options?.title) 68 | useViewTitle(view, options.title) 69 | 70 | if (options?.badge) 71 | useViewBadge(view, options.badge) 72 | 73 | function postMessage(message: any) { 74 | return view.value?.webview.postMessage(message) 75 | } 76 | 77 | return { view, context, postMessage, forceRefresh } 78 | }, 79 | viewId => viewId, 80 | ) 81 | -------------------------------------------------------------------------------- /packages/core/src/composables/useWindowState.ts: -------------------------------------------------------------------------------- 1 | import { computed, shallowRef } from '@reactive-vscode/reactivity' 2 | import { window } from 'vscode' 3 | import { createSingletonComposable } from '../utils' 4 | import { useDisposable } from './useDisposable' 5 | 6 | /** 7 | * @reactive `window.state` 8 | */ 9 | export const useWindowState = createSingletonComposable(() => { 10 | const windowState = shallowRef(window.state) 11 | 12 | useDisposable(window.onDidChangeWindowState((ev) => { 13 | windowState.value = ev 14 | })) 15 | 16 | return { 17 | focused: computed(() => windowState.value.focused), 18 | active: computed(() => windowState.value.active), 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /packages/core/src/composables/useWorkspaceFolders.ts: -------------------------------------------------------------------------------- 1 | import { computed, shallowRef } from '@reactive-vscode/reactivity' 2 | import { workspace } from 'vscode' 3 | import { createSingletonComposable } from '../utils' 4 | import { useDisposable } from './useDisposable' 5 | 6 | /** 7 | * @reactive `workspace.workspaceFolders` 8 | */ 9 | export const useWorkspaceFolders = createSingletonComposable(() => { 10 | const folders = shallowRef(workspace.workspaceFolders) 11 | 12 | useDisposable(workspace.onDidChangeWorkspaceFolders(() => { 13 | folders.value = workspace.workspaceFolders 14 | })) 15 | 16 | return computed(() => folders.value) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@reactive-vscode/reactivity' 2 | export * from './composables' 3 | export * from './utils' 4 | -------------------------------------------------------------------------------- /packages/core/src/utils/asAbsolutePath.ts: -------------------------------------------------------------------------------- 1 | import { extensionContext } from '../utils' 2 | 3 | /** 4 | * A shorthand for `vscode::ExtensionContext.asAbsolutePath` 5 | */ 6 | export function asAbsolutePath(relativePath: string, slient?: false): string 7 | export function asAbsolutePath(relativePath: string, slient?: boolean): string | undefined 8 | export function asAbsolutePath(relativePath: string, slient = false) { 9 | const extCtx = extensionContext.value 10 | if (!extCtx && !slient) 11 | throw new Error('Cannot get absolute path because the extension is not activated yet') 12 | return extCtx?.asAbsolutePath(relativePath) 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/utils/createKeyedComposable.ts: -------------------------------------------------------------------------------- 1 | import { tryOnScopeDispose } from './tryOnScopeDispose' 2 | 3 | /** 4 | * Creates a composable that caches the result of a function based on a key. 5 | * 6 | * @category utilities 7 | */ 8 | export function createKeyedComposable

( 9 | fn: (...args: P) => R, 10 | key: NoInfer<(...args: P) => K>, 11 | ): (...args: P) => R { 12 | const cache = new Map() 16 | return (...args: P): R => { 17 | const k = key(...args) 18 | 19 | let cached = cache.get(k) 20 | if (cached) { 21 | cached.refCount++ 22 | } 23 | else { 24 | cached = { 25 | data: fn(...args), 26 | refCount: 1, 27 | } 28 | cache.set(k, cached) 29 | } 30 | 31 | tryOnScopeDispose(() => { 32 | if (--cached.refCount === 0) 33 | cache.delete(k) 34 | }) 35 | 36 | return cached.data 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/core/src/utils/createSingletonComposable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a composable that should only be called once. 3 | * 4 | * @category utilities 5 | */ 6 | export function createSingletonComposable(fn: () => T): () => T { 7 | let result: T | undefined 8 | return () => result ??= fn() 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/utils/defineConfigs.ts: -------------------------------------------------------------------------------- 1 | import type { ShallowRef } from '@reactive-vscode/reactivity' 2 | import { shallowRef } from '@reactive-vscode/reactivity' 3 | import type { ConfigurationScope, ConfigurationTarget } from 'vscode' 4 | import { workspace } from 'vscode' 5 | import { useDisposable } from '../composables' 6 | import { onActivate } from './onActivate' 7 | 8 | export interface ConfigRef extends ShallowRef { 9 | update: (value: T, configurationTarget?: ConfigurationTarget | boolean | null, overrideInLanguage?: boolean) => Promise 10 | } 11 | 12 | const ConfigTypeSymbol = Symbol('ConfigType') 13 | export interface ConfigType extends ObjectConstructor { 14 | [ConfigTypeSymbol]: T 15 | } 16 | type ConfigTypeSingle = string | typeof String | typeof Number | typeof Boolean | typeof Array | typeof Object | null | ConfigType 17 | type ConfigTypeRaw = ConfigTypeSingle | ConfigTypeSingle[] 18 | type ConfigTypeOptions = Record> 19 | 20 | type ParseConfigType> = 21 | C extends string ? ( 22 | C extends 'string' ? string : 23 | C extends 'number' ? number : 24 | C extends 'boolean' ? boolean : 25 | C extends 'null' ? null : 26 | C extends 'integer' ? number : 27 | C extends 'array' ? any[] : 28 | C extends 'object' ? Record : never 29 | ) 30 | : C extends (infer C1)[] ? (C1 extends ConfigTypeSingle ? ParseConfigType : never) 31 | : C extends ConfigType ? T : ( 32 | C extends typeof String ? string : 33 | C extends typeof Number ? number : 34 | C extends typeof Boolean ? boolean : 35 | C extends typeof Array ? any[] : 36 | C extends typeof Object ? Record : 37 | C extends null ? null : never 38 | ) 39 | type ParseConfigTypeOptions = { 40 | [K in keyof C]: ConfigRef> 41 | } 42 | 43 | /** 44 | * Define configurations of an extension. See `vscode::workspace.getConfiguration`. 45 | * 46 | * @category lifecycle 47 | */ 48 | export function defineConfigs(section: string, configs: C, scope?: ConfigurationScope | null | undefined): ParseConfigTypeOptions { 49 | const workspaceConfig = workspace.getConfiguration(section, scope) 50 | 51 | function createConfigRef(key: string, value: T): ConfigRef { 52 | const ref = shallowRef(value) as unknown as ConfigRef 53 | ref.update = async (value, configurationTarget, overrideInLanguage) => { 54 | await workspaceConfig.update(key, value, configurationTarget, overrideInLanguage) 55 | ref.value = value 56 | } 57 | return ref 58 | } 59 | 60 | const configRefs = Object.fromEntries( 61 | Object.keys(configs).map((key) => { 62 | return [key, createConfigRef(key, workspaceConfig.get(key))] 63 | }), 64 | ) as ParseConfigTypeOptions 65 | 66 | onActivate(() => { 67 | useDisposable(workspace.onDidChangeConfiguration((e) => { 68 | if (!e.affectsConfiguration(section)) 69 | return 70 | const newWorkspaceConfig = workspace.getConfiguration(section) 71 | for (const key in configs) { 72 | if (e.affectsConfiguration(`${section}.${key}`)) 73 | configRefs[key].value = newWorkspaceConfig.get(key) as any 74 | } 75 | })) 76 | }) 77 | 78 | return configRefs 79 | } 80 | -------------------------------------------------------------------------------- /packages/core/src/utils/defineExtension.ts: -------------------------------------------------------------------------------- 1 | import { effectScope, shallowRef } from '@reactive-vscode/reactivity' 2 | import type { ExtensionContext } from 'vscode' 3 | import { activateCbs } from './onActivate' 4 | import { deactivateCbs } from './onDeactivate' 5 | 6 | export const extensionContext = shallowRef(null!) 7 | 8 | /** 9 | * @internal 10 | */ 11 | export const extensionScope = effectScope() 12 | 13 | /** 14 | * Define a new extension. 15 | * 16 | * @category lifecycle 17 | */ 18 | export function defineExtension(setup: () => void) { 19 | return { 20 | activate: (extCtx: ExtensionContext) => { 21 | extensionContext.value = extCtx 22 | extensionScope.run(() => { 23 | activateCbs.map(fn => fn(extCtx)) 24 | setup() 25 | }) 26 | }, 27 | deactivate: () => { 28 | deactivateCbs.map(fn => fn()) 29 | extensionScope.stop() 30 | }, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/utils/defineLogger.ts: -------------------------------------------------------------------------------- 1 | import { computed, shallowRef } from '@reactive-vscode/reactivity' 2 | import type { UseLoggerOptions } from '../composables' 3 | import { useLogger } from '../composables' 4 | import { onActivate } from './onActivate' 5 | 6 | /** 7 | * Define a logger which is usable before activation. 8 | * 9 | * @category view 10 | */ 11 | export function defineLogger(name: string, options?: UseLoggerOptions) { 12 | type Logger = ReturnType 13 | const logger = shallowRef(null) 14 | 15 | const delayedOps: [string, any[]][] = [] 16 | const createDelayedOp = >(key: K) => 17 | (...args: Parameters): ReturnType | null => { 18 | if (logger.value) { 19 | // @ts-expect-error - K is always a literal string 20 | return logger.value[key](...args) 21 | } 22 | else { 23 | delayedOps.push([key, args]) 24 | return null 25 | } 26 | } 27 | 28 | onActivate(() => { 29 | logger.value = useLogger(name, options) 30 | for (const [key, args] of delayedOps) { 31 | // @ts-expect-error - missing types 32 | logger.value[key](...args) 33 | } 34 | }) 35 | 36 | return { 37 | logger, 38 | outputChannel: computed(() => logger.value?.outputChannel), 39 | info: createDelayedOp('info'), 40 | warn: createDelayedOp('warn'), 41 | error: createDelayedOp('error'), 42 | append: createDelayedOp('append'), 43 | appendLine: createDelayedOp('appendLine'), 44 | replace: createDelayedOp('replace'), 45 | clear: createDelayedOp('clear'), 46 | show: createDelayedOp('show'), 47 | hide: createDelayedOp('hide'), 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/core/src/utils/executeCommand.ts: -------------------------------------------------------------------------------- 1 | import type { Uri } from 'vscode' 2 | import { commands } from 'vscode' 3 | 4 | // Extend this interface via declaration merging 5 | export interface Commands extends Record any> { 6 | 'vscode.open': (uri: Uri) => void 7 | } 8 | 9 | /** 10 | * Execute a command, with type checking. See `vscode::commands.executeCommand`. 11 | * 12 | * @category commands 13 | */ 14 | export function executeCommand>(command: K, ...args: Parameters) { 15 | return commands.executeCommand(command, ...args) 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createKeyedComposable' 2 | export * from './createSingletonComposable' 3 | export * from './defineConfigs' 4 | export * from './defineExtension' 5 | export * from './defineLogger' 6 | export * from './executeCommand' 7 | export * from './onActivate' 8 | export * from './onDeactivate' 9 | export * from './tryOnScopeDispose' 10 | export * from './types' 11 | -------------------------------------------------------------------------------- /packages/core/src/utils/onActivate.ts: -------------------------------------------------------------------------------- 1 | import type { ExtensionContext } from 'vscode' 2 | import { extensionContext } from './defineExtension' 3 | 4 | type OnActivateCb = (context: ExtensionContext) => void 5 | 6 | /** 7 | * @internal 8 | */ 9 | export const activateCbs: OnActivateCb[] = [] 10 | 11 | /** 12 | * Registers a callback to be called after the extension has been activated. 13 | * 14 | * @category lifecycle 15 | */ 16 | export function onActivate(fn: OnActivateCb) { 17 | if (extensionContext.value) 18 | fn(extensionContext.value) 19 | else 20 | activateCbs.push(fn) 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/utils/onDeactivate.ts: -------------------------------------------------------------------------------- 1 | type OnDeactivateCb = () => void 2 | 3 | /** 4 | * @internal 5 | */ 6 | export const deactivateCbs: OnDeactivateCb[] = [] 7 | 8 | /** 9 | * Registers a callback to be called when the extension is deactivated. 10 | * 11 | * @category lifecycle 12 | */ 13 | export function onDeactivate(fn: OnDeactivateCb) { 14 | deactivateCbs.push(fn) 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/utils/tryOnScopeDispose.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentScope, onScopeDispose } from '@reactive-vscode/reactivity' 2 | 3 | /** 4 | * The safe version of `vue::onScopeDispose(https://vuejs.org/api/reactivity-advanced.html#onscopedispose)`. 5 | * 6 | * @category lifecycle 7 | */ 8 | export function tryOnScopeDispose(fn: () => void) { 9 | if (getCurrentScope()) { 10 | onScopeDispose(fn) 11 | return true 12 | } 13 | return false 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from '@reactive-vscode/reactivity' 2 | 3 | export type Nullable = T | null | undefined 4 | 5 | export type MaybeNullableRefOrGetter = T | Ref> | (() => Nullable) 6 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "vscode", 6 | "node" 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import Dts from 'vite-plugin-dts' 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | { 7 | name: 'replace dev flag', 8 | enforce: 'pre', 9 | transform: { 10 | order: 'pre', 11 | handler(code, id) { 12 | if (id.endsWith('.ts')) 13 | return code.replaceAll(`__DEV__`, `!!(process.env.NODE_ENV !== "production")`) 14 | }, 15 | }, 16 | }, 17 | Dts({ 18 | include: [ 19 | './src/**/*.ts', 20 | './tsconfig.json', 21 | './shim.d.ts', 22 | ], 23 | rollupTypes: true, 24 | beforeWriteFile(_, content) { 25 | return { 26 | content: content.replaceAll('\'@vue/reactivity\'', '\'@reactive-vscode/reactivity\''), 27 | } 28 | }, 29 | }), 30 | ], 31 | build: { 32 | lib: { 33 | entry: 'src/index.ts', 34 | formats: ['es'], 35 | fileName: 'index', 36 | }, 37 | rollupOptions: { 38 | external: ['vscode', '@reactive-vscode/reactivity'], 39 | }, 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /packages/creator/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import fs from 'node:fs' 4 | import path from 'node:path' 5 | import process from 'node:process' 6 | import chalk from 'chalk' 7 | import prompts from 'prompts' 8 | import corePackage from '../core/package.json' 9 | import tSrc from './templates/src' 10 | import tGitignore from './templates/gitignore' 11 | import tLaunch from './templates/launch' 12 | import tPackage from './templates/package' 13 | import tReadme from './templates/readme' 14 | import tTsconfig from './templates/tsconfig' 15 | import tTsupConfig from './templates/tsupConfig' 16 | import tVscodeignore from './templates/vscodeignore' 17 | import { banner } from './utils/banner' 18 | import { packageManager, runCommand } from './utils/pkgManager' 19 | import { isValidPackageName, toValidPackageName } from './utils/pkgName' 20 | 21 | async function main() { 22 | console.log() 23 | console.log(banner) 24 | console.log() 25 | 26 | const cwd = process.cwd() 27 | 28 | let displayName = process.argv.slice(2).filter(s => /^[\w\- ]+$/.test(s)).map(s => s.trim()).join(' ') || 'Your Extension' 29 | let publisher = '' 30 | let identifier = '' 31 | let targetDir = '' 32 | 33 | await prompts( 34 | [ 35 | { 36 | name: 'displayName', 37 | type: 'text', 38 | message: 'What\'s the display name of your extension?', 39 | initial: displayName, 40 | onState: state => displayName = state.value || displayName, 41 | }, 42 | { 43 | name: 'identifier', 44 | type: 'text', 45 | message: 'What\'s the package name of your extension?', 46 | initial: () => identifier = toValidPackageName(displayName), 47 | validate: id => isValidPackageName(id) || 'Invalid package.json name', 48 | onState: state => identifier = state.value || identifier, 49 | }, 50 | { 51 | name: 'publisher', 52 | type: 'text', 53 | message: 'What\'s your publisher name?', 54 | initial: publisher, 55 | validate: pub => isValidPackageName(pub) || 'Invalid publisher name', 56 | onState: state => publisher = state.value || publisher, 57 | }, 58 | { 59 | name: 'targetDir', 60 | type: 'text', 61 | message: 'Which directory do you want to scaffold the project in?', 62 | initial: () => targetDir = identifier, 63 | validate: dir => 64 | !fs.existsSync(path.resolve(cwd, dir)) 65 | || 'Target directory already exists, please choose another name', 66 | onState: state => targetDir = state.value || targetDir, 67 | }, 68 | ], 69 | { 70 | onCancel: () => { 71 | console.log() 72 | console.log(`${chalk.red('×')} Aborted.`) 73 | console.log() 74 | process.exit(1) 75 | }, 76 | }, 77 | ) 78 | 79 | const root = path.resolve(cwd, targetDir) 80 | 81 | console.log() 82 | console.log(`Scaffolding project in ${root}...`) 83 | 84 | fs.mkdirSync(root, { recursive: true }) 85 | process.chdir(root) 86 | 87 | fs.writeFileSync('package.json', tPackage(publisher, identifier, displayName, `^${corePackage.version}`)) 88 | fs.writeFileSync('tsconfig.json', tTsconfig) 89 | fs.writeFileSync('.gitignore', tGitignore) 90 | fs.writeFileSync('.vscodeignore', tVscodeignore) 91 | fs.writeFileSync('tsup.config.ts', tTsupConfig) 92 | fs.writeFileSync('README.md', tReadme(publisher, identifier, displayName)) 93 | 94 | fs.mkdirSync('src') 95 | const srcCode = tSrc(identifier, displayName) 96 | for (const [name, code] of Object.entries(srcCode)) 97 | fs.writeFileSync(`src/${name}.ts`, code) 98 | 99 | fs.mkdirSync('.vscode') 100 | fs.writeFileSync('.vscode/launch.json', tLaunch) 101 | 102 | console.log() 103 | console.log('Done. Now run:') 104 | console.log() 105 | console.log(chalk.bold.green(` cd ${targetDir}`)) 106 | console.log(chalk.bold.green(` ${packageManager} install`)) 107 | console.log(chalk.bold.green(` code .`)) 108 | console.log(chalk.bold.green(` ${runCommand} dev`)) 109 | console.log() 110 | } 111 | 112 | main() 113 | -------------------------------------------------------------------------------- /packages/creator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-reactive-vscode", 3 | "type": "module", 4 | "version": "0.2.0-beta.3", 5 | "description": "Reactive-vscode project creator", 6 | "author": "_Kerman ", 7 | "license": "MIT", 8 | "homepage": "https://github.com/KermanX/reactive-vscode#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/KermanX/reactive-vscode.git" 12 | }, 13 | "bugs": "https://github.com/KermanX/reactive-vscode/issues", 14 | "keywords": [ 15 | "reactive-vscode", 16 | "creator" 17 | ], 18 | "bin": { 19 | "create-reactive-vscode": "dist/index.cjs" 20 | }, 21 | "files": [ 22 | "README.md", 23 | "dist/index.cjs", 24 | "template" 25 | ], 26 | "scripts": { 27 | "dev": "tsup --watch", 28 | "build": "tsup", 29 | "check": "tsc --noEmit", 30 | "prepublishOnly": "npm run build" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^20.11.26", 34 | "@types/prompts": "^2.4.9", 35 | "chalk": "^5.3.0", 36 | "latest-version": "^7.0.0", 37 | "prompts": "^2.4.2", 38 | "tsup": "^8.0.2", 39 | "typescript": "^5.4.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/creator/templates/gitignore.ts: -------------------------------------------------------------------------------- 1 | export default `dist 2 | node_modules 3 | ` 4 | -------------------------------------------------------------------------------- /packages/creator/templates/launch.ts: -------------------------------------------------------------------------------- 1 | export default `{ 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "args": [ 9 | "--extensionDevelopmentPath=\${workspaceFolder}" 10 | ], 11 | "outFiles": [ 12 | "\${workspaceFolder}/dist/**/*.js" 13 | ] 14 | } 15 | ] 16 | } 17 | ` 18 | -------------------------------------------------------------------------------- /packages/creator/templates/package.ts: -------------------------------------------------------------------------------- 1 | export default (publisher: string, identifier: string, displayName: string, coreVersion: string) => `{ 2 | "publisher": "${publisher}", 3 | "name": "${identifier}", 4 | "displayName": "${displayName}", 5 | "type": "module", 6 | "version": "0.0.1", 7 | "private": true, 8 | "categories": [ 9 | "Other" 10 | ], 11 | "main": "./dist/extension.cjs", 12 | "engines": { 13 | "vscode": "^1.89.0" 14 | }, 15 | "activationEvents": [ 16 | "onStartupFinished" 17 | ], 18 | "contributes": { 19 | "commands": [ 20 | { 21 | "command": "${identifier}.helloWorld", 22 | "title": "Hello World" 23 | } 24 | ], 25 | "configuration": { 26 | "title": "${displayName}", 27 | "properties": { 28 | "${identifier}.message": { 29 | "type": "string", 30 | "default": "Hello World", 31 | "description": "The message to show in the notification" 32 | } 33 | } 34 | } 35 | }, 36 | "scripts": { 37 | "build": "tsup --env.NODE_ENV production --treeshake", 38 | "dev": "tsup --watch ./src --env.NODE_ENV development", 39 | "typecheck": "tsc --noEmit", 40 | "vscode:prepublish": "pnpm run build" 41 | }, 42 | "devDependencies": { 43 | "@types/node": "18.x", 44 | "@types/vscode": "^1.89.0", 45 | "reactive-vscode": "${coreVersion}", 46 | "tsup": "^8.0.2", 47 | "typescript": "^5.4.5" 48 | } 49 | } 50 | ` 51 | -------------------------------------------------------------------------------- /packages/creator/templates/readme.ts: -------------------------------------------------------------------------------- 1 | export default (publisher: string, identifier: string, displayName: string) => `# ${displayName} 2 | 3 | [![Version](https://img.shields.io/visual-studio-marketplace/v/${publisher}.${identifier})](https://marketplace.visualstudio.com/items?itemName=${publisher}.${identifier}) [![Installs](https://img.shields.io/visual-studio-marketplace/i/${publisher}.${identifier}](https://marketplace.visualstudio.com/items?itemName=${publisher}.${identifier}) [![Reactive VSCode](https://img.shields.io/badge/Reactive-VSCode-%23007ACC?style=flat&labelColor=%23229863)](https://kermanx.github.io/reactive-vscode/) 4 | 5 | A VS Code extension created with [Reactive VS Code](https://kermanx.github.io/reactive-vscode/). 6 | 7 | ## Directory Structure 8 | 9 | * \`package.json\` - this is the manifest file in which you declare your extension and command. 10 | * \`src/extension.ts\` - this is the main file where you write your extension. 11 | 12 | ## Get started 13 | 14 | * Open this repository in VS Code. 15 | * Run \`pnpm install\` to install the dependencies. 16 | * Run \`pnpm dev\` to compile the extension and watch for changes. 17 | * Press \`F5\` to open a new window with your extension loaded. 18 | * Run your command from the command palette by pressing (\`Ctrl+Shift+P\` or \`Cmd+Shift+P\` on Mac) and typing \`Hello World\`. 19 | * Set breakpoints in your code inside \`src/extension.ts\` to debug your extension. 20 | * Find output from your extension in the debug console. 21 | 22 | ## Make changes 23 | 24 | * You can relaunch the extension from the debug toolbar after changing code in \`src/extension.ts\`. 25 | * You can also reload (\`Ctrl+R\` or \`Cmd+R\` on Mac) the VS Code window with your extension to load your changes. 26 | 27 | ## Go further 28 | 29 | * [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns. 30 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 31 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 32 | ` 33 | -------------------------------------------------------------------------------- /packages/creator/templates/src.ts: -------------------------------------------------------------------------------- 1 | export default (identifier: string, displayName: string) => ({ 2 | extension: `import { defineExtension, useCommand, useIsDarkTheme, watchEffect } from 'reactive-vscode' 3 | import { window } from 'vscode' 4 | import { message } from './configs' 5 | import { logger } from './utils' 6 | 7 | export = defineExtension(() => { 8 | logger.info('Extension Activated') 9 | 10 | useCommand('${identifier}.helloWorld', () => { 11 | window.showInformationMessage(message.value) 12 | }) 13 | 14 | const isDark = useIsDarkTheme() 15 | watchEffect(() => { 16 | logger.info('Is Dark Theme:', isDark.value) 17 | }) 18 | }) 19 | `, 20 | configs: `import { defineConfigs } from 'reactive-vscode' 21 | 22 | export const { message } = defineConfigs('${identifier}', { 23 | message: 'string', 24 | }) 25 | `, 26 | utils: `import { defineLogger } from 'reactive-vscode' 27 | 28 | export const logger = defineLogger('${displayName}') 29 | `, 30 | }) 31 | -------------------------------------------------------------------------------- /packages/creator/templates/tsconfig.ts: -------------------------------------------------------------------------------- 1 | export default `{ 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2022"], 5 | "module": "Preserve", 6 | "moduleResolution": "Bundler", 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "esModuleInterop": true 11 | } 12 | } 13 | ` 14 | -------------------------------------------------------------------------------- /packages/creator/templates/tsupConfig.ts: -------------------------------------------------------------------------------- 1 | export default `import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/extension.ts'], 5 | format: ['cjs'], 6 | target: 'node18', 7 | clean: true, 8 | minify: true, 9 | external: [ 10 | 'vscode', 11 | ], 12 | }) 13 | ` 14 | -------------------------------------------------------------------------------- /packages/creator/templates/vscodeignore.ts: -------------------------------------------------------------------------------- 1 | export default `.vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | **/tsconfig.json 7 | **/.eslintrc.json 8 | **/*.map 9 | **/*.ts 10 | **/.vscode-test.* 11 | ` 12 | -------------------------------------------------------------------------------- /packages/creator/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['./index.ts'], 5 | outDir: './dist', 6 | format: ['cjs'], 7 | target: 'node18', 8 | clean: true, 9 | minify: true, 10 | }) 11 | -------------------------------------------------------------------------------- /packages/creator/utils/banner.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | function getColoredText(text: string, startColor: number[], endColor: number[]) { 4 | function getColor(index: number) { 5 | const percent = index / text.length 6 | const color = startColor.map((start, i) => { 7 | const end = endColor[i] 8 | const value = Math.round(start + (end - start) * percent) 9 | return value 10 | }) 11 | return color as [number, number, number] 12 | } 13 | 14 | return text 15 | .split('') 16 | .map((char, index) => { 17 | const color = getColor(index) 18 | return chalk.rgb(...color)(char) 19 | }) 20 | .join('') 21 | } 22 | 23 | const color1 = [34, 152, 99] 24 | const color2 = [31, 156, 240] 25 | 26 | export const banner = [ 27 | getColoredText(`Reactive VSCode`, color1, color2), 28 | chalk.gray('-'), 29 | getColoredText(`Develop VSCode Extension with Vue Composition API`, color2, color1), 30 | ].join(' ') 31 | -------------------------------------------------------------------------------- /packages/creator/utils/pkgManager.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | const userAgent = process.env.npm_config_user_agent ?? '' 4 | export const packageManager = /pnpm/.test(userAgent) 5 | ? 'pnpm' 6 | : /yarn/.test(userAgent) 7 | ? 'yarn' 8 | : 'npm' 9 | 10 | export const runCommand = packageManager === 'npm' ? 'npm run' : packageManager 11 | -------------------------------------------------------------------------------- /packages/creator/utils/pkgName.ts: -------------------------------------------------------------------------------- 1 | // Copied from: https://github.com/vuejs/create-vue/blob/main/index.ts 2 | 3 | export function isValidPackageName(projectName: string) { 4 | return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test( 5 | projectName, 6 | ) 7 | } 8 | 9 | export function toValidPackageName(projectName: string) { 10 | return projectName 11 | .trim() 12 | .toLowerCase() 13 | .replace(/\s+/g, '-') 14 | .replace(/^[._]/, '') 15 | .replace(/[^a-z0-9-~]+/g, '-') 16 | } 17 | -------------------------------------------------------------------------------- /packages/metadata/README.md: -------------------------------------------------------------------------------- 1 | # @reactive-vscode/metadata 2 | 3 | Metadata for [reactive-vscode](https://github.com/KermanX/reactive-vscode). 4 | -------------------------------------------------------------------------------- /packages/metadata/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface FunctionMetadata { 2 | name: string 3 | category?: string 4 | original?: string 5 | lastUpdated?: number 6 | external?: string 7 | description?: string 8 | deprecated?: boolean 9 | internal?: boolean 10 | isComposable?: boolean 11 | } 12 | 13 | export interface Metadata { 14 | categories: string[] 15 | functions: FunctionMetadata[] 16 | } 17 | 18 | export const metadata: Metadata 19 | -------------------------------------------------------------------------------- /packages/metadata/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactive-vscode/metadata", 3 | "type": "module", 4 | "version": "0.2.0-beta.3", 5 | "description": "Metadata for reactive-vscode", 6 | "author": "_Kerman ", 7 | "license": "MIT", 8 | "homepage": "https://github.com/KermanX/reactive-vscode#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/KermanX/reactive-vscode.git" 12 | }, 13 | "bugs": "https://github.com/KermanX/reactive-vscode/issues", 14 | "keywords": [ 15 | "reactive-vscode", 16 | "metadata" 17 | ], 18 | "sideEffects": false, 19 | "exports": { 20 | ".": { 21 | "types": "./index.d.ts", 22 | "import": "./index.js" 23 | }, 24 | "./*": "./*" 25 | }, 26 | "main": "./index.js", 27 | "types": "./index.d.ts", 28 | "files": [ 29 | "README.md", 30 | "index.d.ts", 31 | "index.js", 32 | "metadata.json" 33 | ], 34 | "scripts": { 35 | "typecheck": "nr update && tsc --noEmit", 36 | "update": "tsx scripts/update.ts", 37 | "build": "nr update", 38 | "prepublishOnly": "nr update", 39 | "dev": "nodemon -w ../core/src/** -w ./scripts/** -e ts --exec \"nr update\"" 40 | }, 41 | "devDependencies": { 42 | "@antfu/ni": "^0.21.12", 43 | "@types/node": "18.x", 44 | "fast-glob": "^3.3.2", 45 | "nodemon": "^3.1.0", 46 | "simple-git": "^3.24.0", 47 | "tsx": "^4.10.5", 48 | "typescript": "^5.4.5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/metadata/scripts/update.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import { basename, resolve } from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | import { existsSync } from 'node:fs' 5 | import fg from 'fast-glob' 6 | import Git from 'simple-git' 7 | import type { FunctionMetadata, Metadata } from '../index.d.ts' 8 | 9 | const __dirname = fileURLToPath(new URL('.', import.meta.url)) 10 | 11 | const JSON_OUT = resolve(__dirname, '../metadata.json') 12 | const JS_OUT = resolve(__dirname, '../index.js') 13 | const DIR_ROOT = resolve(__dirname, '../../..') 14 | const DIR_SRC = resolve(DIR_ROOT, 'packages/core/src') 15 | const DIR_COMPOSABLES = resolve(DIR_SRC, 'composables') 16 | const DIR_UTILS = resolve(DIR_SRC, 'utils') 17 | 18 | const git = Git(DIR_ROOT) 19 | 20 | function getOriginalAPI(ts: string) { 21 | return ts.match(/^ \* @reactive `(\S+?)`/m)?.[1] 22 | } 23 | 24 | function getCategory(ts: string) { 25 | return ts.match(/^ \* @category (\S+)/m)?.[1] 26 | } 27 | 28 | function getDescription(ts: string) { 29 | const commentBlock = ts.slice(ts.lastIndexOf('\n/**\n') + 1) 30 | const lines = commentBlock.split('\n') 31 | .filter(line => line.startsWith(' * ')) 32 | .map(line => line.slice(3)) 33 | .filter(line => !line.startsWith('@reactive') && !line.startsWith('@category')) 34 | return lines.join('\n') 35 | } 36 | 37 | function toCategoryName(api: string) { 38 | return api.split('.')[0] 39 | } 40 | 41 | async function getComposableMetadata(filename: string): Promise { 42 | const tsPath = resolve(DIR_COMPOSABLES, filename) 43 | const name = basename(filename, '.ts') 44 | const ts = await fs.readFile(tsPath, 'utf-8') 45 | const mdPath = `${name}.md` 46 | const _md = existsSync(mdPath) ? await fs.readFile(mdPath, 'utf-8') : undefined // TODO: md 47 | const original = getOriginalAPI(ts) 48 | const category = getCategory(ts) ?? (original ? toCategoryName(original) : undefined) 49 | const description = getDescription(ts) || (original ? `Reactive API for \`vscode::${original}\`.` : undefined) 50 | return { 51 | name, 52 | category, 53 | original, 54 | lastUpdated: +await git.raw(['log', '-1', '--format=%at', tsPath]) * 1000, 55 | description, 56 | deprecated: ts.includes('@deprecated') || (description?.includes('DEPRECATED') ?? false), 57 | isComposable: true, 58 | } 59 | } 60 | 61 | async function getUtilMetadata(filename: string): Promise { 62 | const tsPath = resolve(DIR_UTILS, filename) 63 | const name = basename(filename, '.ts') 64 | const ts = await fs.readFile(tsPath, 'utf-8') 65 | const mdPath = `${name}.md` 66 | const _md = existsSync(mdPath) ? await fs.readFile(mdPath, 'utf-8') : undefined // TODO: md 67 | const category = getCategory(ts) 68 | const description = getDescription(ts) 69 | return { 70 | name, 71 | category, 72 | lastUpdated: +await git.raw(['log', '-1', '--format=%at', tsPath]) * 1000, 73 | description, 74 | deprecated: ts.includes('@deprecated') || (description?.includes('DEPRECATED') ?? false), 75 | isComposable: false, 76 | } 77 | } 78 | 79 | async function run() { 80 | const composables = await fg('*.ts', { cwd: DIR_COMPOSABLES, ignore: ['index.ts'] }) 81 | const utils = await fg('*.ts', { cwd: DIR_UTILS, ignore: ['index.ts', 'types.ts'] }) 82 | const functions = await Promise.all([ 83 | ...composables.map(getComposableMetadata), 84 | ...utils.map(getUtilMetadata), 85 | ]) 86 | const metadata: Metadata = { 87 | functions, 88 | categories: Array.from(new Set(functions.map(f => f.category!).filter(Boolean))).sort(), 89 | } 90 | await fs.writeFile(JSON_OUT, JSON.stringify(metadata, null, 2)) 91 | await fs.writeFile(JS_OUT, `export const metadata = ${JSON.stringify(metadata, null, 2)}`) 92 | // eslint-disable-next-line no-console 93 | console.log('Metadata updated') 94 | } 95 | 96 | run() 97 | -------------------------------------------------------------------------------- /packages/metadata/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "node" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/metadata/types.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/reactive-vscode/a3a7941118bcb1724ce8ecdaea8958f806d10971/packages/metadata/types.ts -------------------------------------------------------------------------------- /packages/reactivity/README.md: -------------------------------------------------------------------------------- 1 | # @reactive-vscode/reactivity 2 | 3 | Source code in this package is ported from [`@vue/runtime-core`](https://github.com/vuejs/core/blob/main/packages/runtime-core). Licensed under a [MIT License](https://github.com/vueuse/vueuse/blob/main/LICENSE). 4 | -------------------------------------------------------------------------------- /packages/reactivity/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactive-vscode/reactivity", 3 | "type": "module", 4 | "version": "0.2.0-beta.3", 5 | "description": "Full Vue Reactivity API without DOM", 6 | "author": "_Kerman ", 7 | "license": "MIT", 8 | "homepage": "https://github.com/KermanX/reactive-vscode#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/KermanX/reactive-vscode.git" 12 | }, 13 | "bugs": "https://github.com/KermanX/reactive-vscode/issues", 14 | "keywords": [ 15 | "vue", 16 | "reactivity" 17 | ], 18 | "sideEffects": false, 19 | "exports": { 20 | ".": { 21 | "types": "./src/index.ts", 22 | "import": "./src/index.ts" 23 | } 24 | }, 25 | "main": "./src/index.ts", 26 | "types": "./src/index.ts", 27 | "files": [ 28 | "README.md", 29 | "dist" 30 | ], 31 | "scripts": { 32 | "typecheck": "tsc --noEmit", 33 | "build": "vite build", 34 | "prepublishOnly": "pnpm typecheck && pnpm build" 35 | }, 36 | "devDependencies": { 37 | "@vue/reactivity": "^3.4.27", 38 | "@vue/shared": "^3.4.27", 39 | "typescript": "^5.4.5", 40 | "vite": "^5.2.12", 41 | "vite-plugin-dts": "^3.9.1" 42 | }, 43 | "publishConfig": { 44 | "exports": { 45 | ".": { 46 | "types": "./dist/index.d.ts", 47 | "import": "./dist/index.js" 48 | } 49 | }, 50 | "main": "./dist/index.js", 51 | "types": "./dist/index.d.ts" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/reactivity/shim.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | declare global { 3 | const __DEV__: boolean 4 | } 5 | -------------------------------------------------------------------------------- /packages/reactivity/src/enums.ts: -------------------------------------------------------------------------------- 1 | export enum LifecycleHooks { 2 | ACTIVATED = 'a', 3 | DEACTIVATED = 'da', 4 | } 5 | -------------------------------------------------------------------------------- /packages/reactivity/src/errorHandling.ts: -------------------------------------------------------------------------------- 1 | import { warn } from './warning' 2 | import { isArray, isFunction, isPromise } from '@vue/shared' 3 | import { LifecycleHooks } from './enums' 4 | 5 | // contexts where user provided function may be executed, in addition to 6 | // lifecycle hooks. 7 | export enum ErrorCodes { 8 | WATCH_GETTER, 9 | WATCH_CALLBACK, 10 | WATCH_CLEANUP, 11 | APP_ERROR_HANDLER, 12 | SCHEDULER, 13 | } 14 | 15 | export const ErrorTypeStrings: Record = { 16 | [LifecycleHooks.ACTIVATED]: 'activated hook', 17 | [LifecycleHooks.DEACTIVATED]: 'deactivated hook', 18 | [ErrorCodes.WATCH_GETTER]: 'watcher getter', 19 | [ErrorCodes.WATCH_CALLBACK]: 'watcher callback', 20 | [ErrorCodes.WATCH_CLEANUP]: 'watcher cleanup function', 21 | [ErrorCodes.APP_ERROR_HANDLER]: 'app errorHandler', 22 | [ErrorCodes.SCHEDULER]: 'scheduler flush', 23 | } 24 | 25 | export type ErrorTypes = LifecycleHooks | ErrorCodes 26 | 27 | export function callWithErrorHandling( 28 | fn: Function, 29 | instance: null, 30 | type: ErrorTypes, 31 | args?: unknown[], 32 | ) { 33 | try { 34 | return args ? fn(...args) : fn() 35 | } catch (err) { 36 | handleError(err, instance, type) 37 | } 38 | } 39 | 40 | export function callWithAsyncErrorHandling( 41 | fn: Function | Function[], 42 | instance: null, 43 | type: ErrorTypes, 44 | args?: unknown[], 45 | ): any { 46 | if (isFunction(fn)) { 47 | const res = callWithErrorHandling(fn, instance, type, args) 48 | if (res && isPromise(res)) { 49 | res.catch(err => { 50 | handleError(err, instance, type) 51 | }) 52 | } 53 | return res 54 | } 55 | 56 | if (isArray(fn)) { 57 | const values = [] 58 | for (let i = 0; i < fn.length; i++) { 59 | values.push(callWithAsyncErrorHandling(fn[i], instance, type, args)) 60 | } 61 | return values 62 | } else if (__DEV__) { 63 | warn( 64 | `Invalid value type passed to callWithAsyncErrorHandling(): ${typeof fn}`, 65 | ) 66 | } 67 | } 68 | 69 | export function handleError( 70 | err: unknown, 71 | instance: null, 72 | type: ErrorTypes, 73 | throwInDev = true, 74 | ) { 75 | const contextVNode = null 76 | logError(err, type, contextVNode, throwInDev) 77 | } 78 | 79 | function logError( 80 | err: unknown, 81 | type: ErrorTypes, 82 | contextVNode: null, 83 | throwInDev = true, 84 | ) { 85 | if (__DEV__) { 86 | const info = ErrorTypeStrings[type] 87 | warn(`Unhandled error${info ? ` during execution of ${info}` : ``}`) 88 | // crash in dev by default so it's more noticeable 89 | if (throwInDev) { 90 | throw err 91 | } else { 92 | console.error(err) 93 | } 94 | } else { 95 | // recover in prod to reduce the impact on end-user 96 | console.error(err) 97 | } 98 | } -------------------------------------------------------------------------------- /packages/reactivity/src/index.ts: -------------------------------------------------------------------------------- 1 | export { nextTick } from './scheduler' 2 | export type { WatchEffect, WatchSource, WatchCallback, WatchOptionsBase, WatchOptions, WatchStopHandle } from './apiWatch' 3 | export { watchEffect, watchSyncEffect, watch } from './apiWatch' 4 | 5 | export * from '@vue/reactivity' 6 | -------------------------------------------------------------------------------- /packages/reactivity/src/scheduler.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling' 2 | import { type Awaited, NOOP, isArray } from '@vue/shared' 3 | 4 | export interface SchedulerJob extends Function { 5 | id?: number 6 | pre?: boolean 7 | active?: boolean 8 | computed?: boolean 9 | /** 10 | * Indicates whether the effect is allowed to recursively trigger itself 11 | * when managed by the scheduler. 12 | * 13 | * By default, a job cannot trigger itself because some built-in method calls, 14 | * e.g. Array.prototype.push actually performs reads as well (#1740) which 15 | * can lead to confusing infinite loops. 16 | * The allowed cases are component update functions and watch callbacks. 17 | * Component update functions may update child component props, which in turn 18 | * trigger flush: "pre" watch callbacks that mutates state that the parent 19 | * relies on (#1801). Watch callbacks doesn't track its dependencies so if it 20 | * triggers itself again, it's likely intentional and it is the user's 21 | * responsibility to perform recursive state mutation that eventually 22 | * stabilizes (#1727). 23 | */ 24 | allowRecurse?: boolean 25 | } 26 | 27 | export type SchedulerJobs = SchedulerJob | SchedulerJob[] 28 | 29 | let isFlushing = false 30 | let isFlushPending = false 31 | 32 | const queue: SchedulerJob[] = [] 33 | let flushIndex = 0 34 | 35 | const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise 36 | let currentFlushPromise: Promise | null = null 37 | 38 | const RECURSION_LIMIT = 100 39 | type CountMap = Map 40 | 41 | export function nextTick( 42 | this: T, 43 | fn?: (this: T) => R, 44 | ): Promise> { 45 | const p = currentFlushPromise || resolvedPromise 46 | return fn ? p.then(this ? fn.bind(this) : fn) : p 47 | } 48 | 49 | // #2768 50 | // Use binary-search to find a suitable position in the queue, 51 | // so that the queue maintains the increasing order of job's id, 52 | // which can prevent the job from being skipped and also can avoid repeated patching. 53 | function findInsertionIndex(id: number) { 54 | // the start index should be `flushIndex + 1` 55 | let start = flushIndex + 1 56 | let end = queue.length 57 | 58 | while (start < end) { 59 | const middle = (start + end) >>> 1 60 | const middleJob = queue[middle] 61 | const middleJobId = getId(middleJob) 62 | if (middleJobId < id || (middleJobId === id && middleJob.pre)) { 63 | start = middle + 1 64 | } else { 65 | end = middle 66 | } 67 | } 68 | 69 | return start 70 | } 71 | 72 | export function queueJob(job: SchedulerJob) { 73 | // the dedupe search uses the startIndex argument of Array.includes() 74 | // by default the search index includes the current job that is being run 75 | // so it cannot recursively trigger itself again. 76 | // if the job is a watch() callback, the search will start with a +1 index to 77 | // allow it recursively trigger itself - it is the user's responsibility to 78 | // ensure it doesn't end up in an infinite loop. 79 | if ( 80 | !queue.length || 81 | !queue.includes( 82 | job, 83 | isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex, 84 | ) 85 | ) { 86 | if (job.id == null) { 87 | queue.push(job) 88 | } else { 89 | queue.splice(findInsertionIndex(job.id), 0, job) 90 | } 91 | queueFlush() 92 | } 93 | } 94 | 95 | function queueFlush() { 96 | if (!isFlushing && !isFlushPending) { 97 | isFlushPending = true 98 | currentFlushPromise = resolvedPromise.then(flushJobs) 99 | } 100 | } 101 | 102 | export function invalidateJob(job: SchedulerJob) { 103 | const i = queue.indexOf(job) 104 | if (i > flushIndex) { 105 | queue.splice(i, 1) 106 | } 107 | } 108 | 109 | export function flushPreFlushCbs( 110 | seen?: CountMap, 111 | // if currently flushing, skip the current job itself 112 | i = isFlushing ? flushIndex + 1 : 0, 113 | ) { 114 | if (__DEV__) { 115 | seen = seen || new Map() 116 | } 117 | for (; i < queue.length; i++) { 118 | const cb = queue[i] 119 | if (cb && cb.pre) { 120 | if (__DEV__ && checkRecursiveUpdates(seen!, cb)) { 121 | continue 122 | } 123 | queue.splice(i, 1) 124 | i-- 125 | cb() 126 | } 127 | } 128 | } 129 | 130 | const getId = (job: SchedulerJob): number => 131 | job.id == null ? Infinity : job.id 132 | 133 | const comparator = (a: SchedulerJob, b: SchedulerJob): number => { 134 | const diff = getId(a) - getId(b) 135 | if (diff === 0) { 136 | if (a.pre && !b.pre) return -1 137 | if (b.pre && !a.pre) return 1 138 | } 139 | return diff 140 | } 141 | 142 | function flushJobs(seen?: CountMap) { 143 | isFlushPending = false 144 | isFlushing = true 145 | if (__DEV__) { 146 | seen = seen || new Map() 147 | } 148 | 149 | // Sort queue before flush. 150 | // This ensures that: 151 | // 1. Components are updated from parent to child. (because parent is always 152 | // created before the child so its render effect will have smaller 153 | // priority number) 154 | // 2. If a component is unmounted during a parent component's update, 155 | // its update can be skipped. 156 | queue.sort(comparator) 157 | 158 | // conditional usage of checkRecursiveUpdate must be determined out of 159 | // try ... catch block since Rollup by default de-optimizes treeshaking 160 | // inside try-catch. This can leave all warning code unshaked. Although 161 | // they would get eventually shaken by a minifier like terser, some minifiers 162 | // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610) 163 | const check = __DEV__ 164 | ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job) 165 | : NOOP 166 | 167 | try { 168 | for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { 169 | const job = queue[flushIndex] 170 | if (job && job.active !== false) { 171 | if (__DEV__ && check(job)) { 172 | continue 173 | } 174 | callWithErrorHandling(job, null, ErrorCodes.SCHEDULER) 175 | } 176 | } 177 | } finally { 178 | flushIndex = 0 179 | queue.length = 0 180 | 181 | isFlushing = false 182 | currentFlushPromise = null 183 | // some postFlushCb queued jobs! 184 | // keep flushing until it drains. 185 | if (queue.length) { 186 | flushJobs(seen) 187 | } 188 | } 189 | } 190 | 191 | function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) { 192 | if (!seen.has(fn)) { 193 | seen.set(fn, 1) 194 | } else { 195 | const count = seen.get(fn)! 196 | if (count > RECURSION_LIMIT) { 197 | handleError( 198 | `Maximum recursive updates exceeded. ` + 199 | `This means you have a reactive effect that is mutating its own ` + 200 | `dependencies and thus recursively triggering itself. Possible sources ` + 201 | `include component template, render function, updated hook or ` + 202 | `watcher source function.`, 203 | null, 204 | ErrorCodes.APP_ERROR_HANDLER, 205 | ) 206 | return true 207 | } else { 208 | seen.set(fn, count + 1) 209 | } 210 | } 211 | } -------------------------------------------------------------------------------- /packages/reactivity/src/warning.ts: -------------------------------------------------------------------------------- 1 | import { pauseTracking, resetTracking } from '@vue/reactivity' 2 | 3 | export function warn(msg: string, ...args: any[]) { 4 | // avoid props formatting or warn handler tracking deps that might be mutated 5 | // during patch, leading to infinite recursion. 6 | pauseTracking() 7 | 8 | const warnArgs = [`[Vue warn]: ${msg}`, ...args] 9 | /* istanbul ignore if */ 10 | console.warn(...warnArgs) 11 | 12 | resetTracking() 13 | } 14 | -------------------------------------------------------------------------------- /packages/reactivity/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/reactivity/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { defineConfig } from 'vite' 3 | import Dts from 'vite-plugin-dts' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | { 8 | name: 'replace dev flag', 9 | enforce: 'pre', 10 | transform: { 11 | order: 'pre', 12 | handler(code, id) { 13 | if (id.endsWith('.ts')) 14 | return code.replaceAll(`__DEV__`, `!!(process.env.NODE_ENV !== "production")`) 15 | }, 16 | }, 17 | }, 18 | Dts({ 19 | include: [ 20 | './src/**/*.ts', 21 | './tsconfig.json', 22 | './shim.d.ts', 23 | ], 24 | rollupTypes: true, 25 | bundledPackages: ['@vue/reactivity', '@vue/shared'], 26 | }), 27 | ], 28 | build: { 29 | lib: { 30 | entry: 'src/index.ts', 31 | formats: ['es'], 32 | fileName: 'index', 33 | }, 34 | minify: false, 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /packages/vueuse/README.md: -------------------------------------------------------------------------------- 1 | # `@reactive-vscode/vueuse` 2 | 3 | A subset of [VueUse](https://vueuse.org/) for Node.js environment. 4 | -------------------------------------------------------------------------------- /packages/vueuse/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactive-vscode/vueuse", 3 | "type": "module", 4 | "version": "0.2.0-beta.3", 5 | "description": "Useful VueUse utilities for VSCode extension development", 6 | "author": "_Kerman ", 7 | "license": "MIT", 8 | "homepage": "https://github.com/KermanX/reactive-vscode#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/KermanX/reactive-vscode.git" 12 | }, 13 | "bugs": "https://github.com/KermanX/reactive-vscode/issues", 14 | "keywords": [ 15 | "vueuse", 16 | "vscode", 17 | "extension", 18 | "vue" 19 | ], 20 | "sideEffects": false, 21 | "exports": { 22 | ".": { 23 | "types": "./src/index.ts", 24 | "import": "./src/index.ts" 25 | } 26 | }, 27 | "main": "./src/index.ts", 28 | "types": "./src/index.ts", 29 | "files": [ 30 | "README.md", 31 | "dist", 32 | "tsconfig.json" 33 | ], 34 | "scripts": { 35 | "typecheck": "tsc --noEmit", 36 | "build": "vite build", 37 | "prepublishOnly": "pnpm typecheck && pnpm build" 38 | }, 39 | "dependencies": { 40 | "@reactive-vscode/reactivity": "workspace:*" 41 | }, 42 | "devDependencies": { 43 | "@types/node": "^20.14.2", 44 | "@vueuse/core": "^10.10.0", 45 | "typescript": "^5.4.5", 46 | "vite": "^5.2.12", 47 | "vite-plugin-dts": "^3.9.1" 48 | }, 49 | "publishConfig": { 50 | "exports": { 51 | ".": { 52 | "types": "./dist/index.d.ts", 53 | "import": "./dist/index.js" 54 | } 55 | }, 56 | "main": "./dist/index.js", 57 | "types": "./dist/index.d.ts" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/vueuse/shim.d.ts: -------------------------------------------------------------------------------- 1 | export { } 2 | declare global { 3 | const __DEV__: boolean 4 | const window: typeof window 5 | const HTMLCanvasElement: any 6 | const HTMLImageElement: any 7 | const HTMLTextAreaElement: any 8 | const HTMLElement: any 9 | const Window: any 10 | const WindowEventMap: any 11 | const MediaSource: any 12 | const WebSocket: globalThis.WebSocket 13 | const Worker: globalThis.Worker 14 | const CloseEvent: any 15 | const MessageEvent: any 16 | } 17 | -------------------------------------------------------------------------------- /packages/vueuse/src/vue-demi.ts: -------------------------------------------------------------------------------- 1 | export * from '@reactive-vscode/reactivity' 2 | 3 | export const isVue2 = false 4 | export const isVue3 = true 5 | 6 | export function getCurrentInstance() { 7 | return null 8 | } 9 | export function onMounted() { 10 | } 11 | -------------------------------------------------------------------------------- /packages/vueuse/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/vueuse/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { defineConfig } from 'vite' 3 | import Dts from 'vite-plugin-dts' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | { 8 | name: 'replace constants', 9 | enforce: 'pre', 10 | transform: { 11 | order: 'pre', 12 | handler(code, id) { 13 | if (id.endsWith('.mjs')) { 14 | return code 15 | .replaceAll(`getCurrentInstance()`, `null`) 16 | .replaceAll(`getLifeCycleTarget()`, `null`) 17 | } 18 | }, 19 | }, 20 | }, 21 | Dts({ 22 | include: [ 23 | './src/**/*.ts', 24 | './tsconfig.json', 25 | './shim.d.ts', 26 | ], 27 | rollupTypes: true, 28 | bundledPackages: ['@vueuse/core', '@vueuse/shared'], 29 | beforeWriteFile(_, content) { 30 | return { 31 | content: content.replaceAll('\'vue-demi\'', '\'@reactive-vscode/reactivity\''), 32 | } 33 | }, 34 | }), 35 | ], 36 | resolve: { 37 | alias: { 38 | 'vue-demi': path.resolve(__dirname, './src/vue-demi.ts'), 39 | }, 40 | }, 41 | build: { 42 | lib: { 43 | entry: 'src/index.ts', 44 | formats: ['es'], 45 | fileName: 'index', 46 | }, 47 | rollupOptions: { 48 | external: ['@reactive-vscode/reactivity'], 49 | }, 50 | minify: false, 51 | }, 52 | }) 53 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/reactive-vscode/a3a7941118bcb1724ce8ecdaea8958f806d10971/pnpm-workspace.yaml -------------------------------------------------------------------------------- /scripts/publish.js: -------------------------------------------------------------------------------- 1 | import { $, fs } from 'zx' 2 | 3 | await fs.copyFile('README.md', 'packages/core/README.md') 4 | await $`pnpm -r --filter "./packages/**" publish --access public --no-git-checks` 5 | -------------------------------------------------------------------------------- /test/composables/useVscodeContext.test.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef, Ref, WritableComputedRef } from 'reactive-vscode' 2 | import { computed, ref, useVscodeContext } from 'reactive-vscode' 3 | import { describe, expectTypeOf, it } from 'vitest' 4 | 5 | describe('useVscodeContext', () => { 6 | it.skip('should have correct types', () => { 7 | expectTypeOf(useVscodeContext('myContext', 'a')).toEqualTypeOf>() 8 | expectTypeOf(useVscodeContext('myContext', ref('a'))).toEqualTypeOf>() 9 | expectTypeOf(useVscodeContext('myContext', () => 'a')).toEqualTypeOf>() 10 | expectTypeOf(useVscodeContext('myContext', computed(() => 'a'))).toEqualTypeOf>() 11 | expectTypeOf(useVscodeContext('myContext', computed({ 12 | get() { 13 | return 'a' 14 | }, 15 | set() { }, 16 | }))).toEqualTypeOf>() 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/mock/vscode.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "scripts": { 4 | "test": "vitest", 5 | "typecheck": "tsc --noEmit" 6 | }, 7 | "devDependencies": { 8 | "reactive-vscode": "workspace:*", 9 | "typescript": "^5.4.5", 10 | "vite": "^5.2.12", 11 | "vitest": "^1.6.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/shim.d.ts: -------------------------------------------------------------------------------- 1 | export { } 2 | declare global { 3 | const __DEV__: boolean 4 | } 5 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "vitest/importMeta" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/utils/defineConfigs.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expectTypeOf, it } from 'vitest' 2 | import type { ConfigRef, ConfigType } from 'reactive-vscode' 3 | import { defineConfigs } from 'reactive-vscode' 4 | 5 | describe('keyedComposable', () => { 6 | it.skip('should have correct type', () => { 7 | expectTypeOf( 8 | defineConfigs('test', { 9 | str1: 'string', 10 | str2: String, 11 | num1: 'number', 12 | num2: Number, 13 | bool1: 'boolean', 14 | bool2: Boolean, 15 | null1: 'null', 16 | null2: null, 17 | int1: 'integer', 18 | arr1: 'array', 19 | arr2: Array, 20 | obj1: 'object', 21 | obj3: Object as ConfigType<{ a: number }>, 22 | union1: ['string', 'number'], 23 | union2: [String, Number], 24 | union3: [String, Number, 'boolean'], 25 | }), 26 | ).toMatchTypeOf<{ 27 | str1: ConfigRef 28 | str2: ConfigRef 29 | num1: ConfigRef 30 | num2: ConfigRef 31 | bool1: ConfigRef 32 | bool2: ConfigRef 33 | null1: ConfigRef 34 | null2: ConfigRef 35 | int1: ConfigRef 36 | arr1: ConfigRef 37 | arr2: ConfigRef 38 | obj1: ConfigRef> 39 | obj3: ConfigRef<{ a: number }> 40 | union1: ConfigRef 41 | union2: ConfigRef 42 | union3: ConfigRef 43 | }>() 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/utils/keyedComposable.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | import { createKeyedComposable, effectScope } from 'reactive-vscode' 3 | 4 | const fnImpl = (a: number) => ({ a }) 5 | 6 | describe('keyedComposable', () => { 7 | it('should cache correctly', () => { 8 | const fn = vi.fn(fnImpl) 9 | const composable = createKeyedComposable(fn, a => a) 10 | 11 | const result1 = composable(1) 12 | const result2 = composable(1) 13 | 14 | expect(result1.a).toBe(1) 15 | expect(result1).toBe(result2) 16 | expect(fn).toBeCalledTimes(1) 17 | 18 | const result3 = composable(2) 19 | 20 | expect(result3.a).toBe(2) 21 | expect(fn).toBeCalledTimes(2) 22 | }) 23 | 24 | it('should release data when all scopes disposed', () => { 25 | const fn = vi.fn(fnImpl) 26 | const composable = createKeyedComposable(fn, a => a) 27 | 28 | const scope1 = effectScope() 29 | scope1.run(() => { 30 | composable(1) 31 | }) 32 | scope1.stop() 33 | 34 | const scope2 = effectScope() 35 | scope2.run(() => { 36 | composable(1) 37 | }) 38 | scope2.stop() 39 | 40 | expect(fn).toBeCalledTimes(2) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /test/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { resolve } from 'node:path' 3 | import { defineConfig } from 'vite' 4 | 5 | export default defineConfig({ 6 | test: { 7 | include: ['**/*.test.ts'], 8 | alias: { 9 | vscode: resolve('./mock/vscode.ts'), 10 | }, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext"], 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "esModuleInterop": true, 11 | "skipDefaultLibCheck": true, 12 | "skipLibCheck": true 13 | } 14 | } 15 | --------------------------------------------------------------------------------