├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md ├── dependabot.yml ├── labels.yml ├── pull_request_template.md └── workflows │ ├── add-issues-to-devx-project.yml │ ├── ci.yml │ ├── release.yml │ └── sync-labels.yml ├── .gitignore ├── .metadata ├── README.md └── notifications.json ├── .vscode ├── launch.json └── settings.json ├── .vscodeignore ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── OWNERSHIP_PROOF.md ├── README.md ├── cypress.json ├── docker-compose.yml ├── docs ├── code_gen.gif └── deploy_contracts.gif ├── extension ├── index.d.ts ├── language │ ├── language-configuration.json │ └── syntaxes │ │ ├── cadence.tmGrammar.json │ │ └── codeblock.json ├── src │ ├── commands │ │ ├── command-constants.ts │ │ └── command-controller.ts │ ├── crypto-polyfill.ts │ ├── dependency-installer │ │ ├── dependency-installer.ts │ │ ├── installer.ts │ │ └── installers │ │ │ ├── flow-cli-installer.ts │ │ │ └── homebrew-installer.ts │ ├── extension.ts │ ├── flow-cli │ │ ├── cli-provider.ts │ │ ├── cli-selection-provider.ts │ │ └── cli-versions-provider.ts │ ├── json-schema-provider.ts │ ├── main.ts │ ├── server │ │ ├── flow-config.ts │ │ └── language-server.ts │ ├── settings │ │ └── settings.ts │ ├── storage │ │ └── storage-provider.ts │ ├── telemetry │ │ ├── mixpanel-wrapper.ts │ │ ├── playground.ts │ │ ├── sentry-wrapper.ts │ │ └── telemetry.ts │ ├── test-provider │ │ ├── constants.ts │ │ ├── test-provider.ts │ │ ├── test-resolver.ts │ │ ├── test-runner.ts │ │ ├── test-trie.ts │ │ └── utils.ts │ ├── ui │ │ ├── notification-provider.ts │ │ └── prompts.ts │ └── utils │ │ ├── semaphore.ts │ │ ├── shell │ │ ├── default-shell.ts │ │ ├── env-vars.ts │ │ └── exec.ts │ │ ├── state-cache.ts │ │ └── utils.ts └── test │ ├── fixtures │ └── workspace │ │ ├── Error.cdc │ │ ├── FooContract.cdc │ │ ├── NonFungibleToken.cdc │ │ ├── Script.cdc │ │ ├── Tx.cdc │ │ ├── bar │ │ └── flow.json │ │ ├── flow.json │ │ ├── foo │ │ └── flow.json │ │ └── test │ │ ├── bar │ │ ├── test2.cdc │ │ └── test3.cdc │ │ └── test1.cdc │ ├── globals.ts │ ├── index.ts │ ├── integration │ ├── 0 - dependencies.test.ts │ ├── 1 - language-server.test.ts │ ├── 2 - commands.test.ts │ ├── 3 - schema.test.ts │ ├── 4 - flow-config.test.ts │ ├── 5 - test-trie.test.ts │ └── 6 - test-provider.test.ts │ ├── mock │ └── mockSettings.ts │ ├── run-tests.ts │ └── unit │ ├── parser.test.ts │ ├── state-cache.test.ts │ └── test-trie.test.ts ├── flow-schema.json ├── images ├── icon.png ├── vscode-banner.png └── vscode-banner.svg ├── package-lock.json ├── package.json └── tsconfig.json /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Reporting a Problem/Bug 3 | about: Reporting a Problem/Bug 4 | title: '' 5 | labels: bug, Feedback 6 | assignees: DylanTinianov 7 | 8 | --- 9 | ### Instructions 10 | 11 | Please fill out the template below to the best of your ability and include a label indicating which tool/service you were working with when you encountered the problem. 12 | 13 | ### Problem 14 | 15 | 16 | 17 | ### Steps to Reproduce 18 | 19 | 20 | 21 | ### Acceptance Criteria 22 | 23 | 24 | 25 | ### Context 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Requesting a Feature or Improvement 3 | about: "For feature requests. Please search for existing issues first. Also see CONTRIBUTING.md" 4 | title: '' 5 | labels: Feedback, Feature 6 | assignees: DylanTinianov 7 | 8 | --- 9 | 10 | ## Instructions 11 | 12 | Please fill out the template below to the best of your ability. 13 | 14 | ### Issue To Be Solved 15 | 16 | (Replace this text: 17 | Please present a concise description of the problem to be addressed by this feature request. 18 | Please be clear what parts of the problem are considered to be in-scope and out-of-scope.) 19 | 20 | ### (Optional): Suggest A Solution 21 | 22 | (Replace this text: A concise description of your preferred solution. Things to address include: 23 | 24 | * Details of the technical implementation 25 | * Tradeoffs made in design decisions 26 | * Caveats and considerations for the future 27 | 28 | If there are multiple solutions, please present each one separately. Save comparisons for the very end.) 29 | 30 | ### (Optional): Context 31 | 32 | (Replace this text: 33 | What are you currently working on that this is blocking?) 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | 14 | - package-ecosystem: "npm" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | ignore: 19 | - dependency-name: "node-fetch" 20 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | - color: fbca04 2 | description: "" 3 | name: Breaking Change 4 | - color: 3E4B9E 5 | description: "" 6 | name: Epic 7 | - color: 0e8a16 8 | description: "" 9 | name: Feature 10 | - color: d4c5f9 11 | description: "" 12 | name: Feedback 13 | - color: 1d76db 14 | description: "" 15 | name: Improvement 16 | - color: efbd7f 17 | description: "" 18 | name: Needs Definition 19 | - color: f99875 20 | description: "" 21 | name: Needs Estimation 22 | - color: efa497 23 | description: "" 24 | name: Needs Test Cases 25 | - color: fcadab 26 | description: "" 27 | name: P-High 28 | - color: bfd4f2 29 | description: "" 30 | name: P-Low 31 | - color: ddcd3e 32 | description: "" 33 | name: P-Medium 34 | - color: CCCCCC 35 | description: "" 36 | name: Technical Debt 37 | - color: d73a4a 38 | description: Something isn't working 39 | name: Bug 40 | - color: c2e0c6 41 | description: "" 42 | name: Bugfix 43 | - color: cfd3d7 44 | description: This issue or pull request already exists 45 | name: Duplicate 46 | - color: 7057ff 47 | description: Good for newcomers 48 | name: Good First Issue 49 | - color: d876e3 50 | description: Further information is requested 51 | name: Question 52 | - color: 0075ca 53 | description: Improvements or additions to documentation 54 | name: Documentation 55 | - color: 5319e7 56 | description: "" 57 | name: S-Playground 58 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Closes #??? 2 | 3 | ## Description 4 | 5 | 9 | 10 | ______ 11 | 12 | For contributor use: 13 | 14 | - [ ] Targeted PR against `master` branch 15 | - [ ] Linked to Github issue with discussion and accepted design OR link to spec that describes this work 16 | - [ ] Code follows the [standards mentioned here](https://github.com/onflow/cadence/blob/master/CONTRIBUTING.md#styleguides) 17 | - [ ] Updated relevant documentation 18 | - [ ] Re-reviewed `Files changed` in the Github PR explorer 19 | - [ ] Added appropriate labels 20 | -------------------------------------------------------------------------------- /.github/workflows/add-issues-to-devx-project.yml: -------------------------------------------------------------------------------- 1 | name: Adds all issues to the DevEx project board. 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add issue to project 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/add-to-project@v1.0.2 14 | with: 15 | project-url: https://github.com/orgs/onflow/projects/13 16 | github-token: ${{ secrets.GH_ACTION_FOR_PROJECTS }} 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | linter: 11 | name: "Code Style Standard" 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | 22 | - run: npm ci 23 | - run: npm run lint 24 | 25 | build: 26 | name: Build 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: Use Node.js ${{ matrix.node-version }} 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | 37 | - name: Install dependencies 38 | run: npm ci 39 | 40 | - name: Package 41 | run: npm run package 42 | 43 | windows-build: 44 | name: Windows Build 45 | runs-on: windows-latest 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@v4 49 | 50 | - name: Use Node.js ${{ matrix.node-version }} 51 | uses: actions/setup-node@v4 52 | with: 53 | node-version: ${{ matrix.node-version }} 54 | 55 | - name: Install dependencies 56 | run: npm ci 57 | 58 | - name: Package 59 | run: npm run package 60 | 61 | test: 62 | name: Tests 63 | strategy: 64 | matrix: 65 | os: [macos-latest, ubuntu-latest, windows-latest] 66 | runs-on: ${{ matrix.os }} 67 | steps: 68 | - name: Checkout 69 | uses: actions/checkout@v4 70 | 71 | - name: Use Node.js ${{ matrix.node-version }} 72 | uses: actions/setup-node@v4 73 | with: 74 | node-version: ${{ matrix.node-version }} 75 | 76 | - name: Install dependencies 77 | run: npm ci 78 | 79 | - name: Setup Xvfb (Linux) 80 | if: matrix.os == 'ubuntu-latest' 81 | run: | 82 | nohup Xvfb :99 -screen 0 1024x768x16 > /dev/null 2>&1 & 83 | echo "XVFB_PID=$!" >> "$GITHUB_ENV" 84 | echo "DISPLAY=:99.0" >> "$GITHUB_ENV" 85 | while [ ! -e /tmp/.X11-unix/X99 ]; do sleep 0.1; done 86 | 87 | - name: Integration Tests 88 | run: npm run test 89 | env: 90 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 91 | 92 | - name: Cleanup Xvfb (Linux) 93 | if: matrix.os == 'ubuntu-latest' 94 | run: kill $XVFB_PID 95 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Extension 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | jobs: 8 | publish-extension: 9 | name: "Publish Extension" 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | 20 | - name: Install dependencies 21 | run: npm ci 22 | 23 | - name: Publish to Open VSX 24 | uses: HaaLeo/publish-vscode-extension@v1 25 | id: publishToOpenVSX 26 | with: 27 | pat: ${{ secrets.OPEN_VSX_TOKEN }} 28 | 29 | - name: Publish to Visual Studio Marketplace 30 | uses: HaaLeo/publish-vscode-extension@v1 31 | with: 32 | pat: ${{ secrets.VSCODE_MARKETPLACE_TOKEN }} 33 | registryUrl: https://marketplace.visualstudio.com 34 | extensionFile: ${{ steps.publishToOpenVSX.outputs.vsixPath }} 35 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yml: -------------------------------------------------------------------------------- 1 | name: Sync Labels 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - .github/labels.yml 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: micnncim/action-label-syncer@v1 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | with: 19 | manifest: .github/labels.yml 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | out 4 | .history 5 | .cache 6 | .vscode-test 7 | *.vsix 8 | cypress 9 | .idea -------------------------------------------------------------------------------- /.metadata/README.md: -------------------------------------------------------------------------------- 1 | # Extension metadata 2 | 3 | **DO NOT DELETE THIS FOLDER UNLESS YOU KNOW WHAT YOU ARE DOING** 4 | 5 | This folder contains remotely-updated metadata to provide updates to the Cadence VSCode Extension without requiring a new release of the extension itself. When consuming this metadata, the latest commit to the default repository branch should be assumed to be the latest version of the extension metadata. 6 | 7 | Currently, it is only used by the Cadence VSCode Extension to fetch any notifications that should be displayed to the user. 8 | 9 | ## Notfications schema 10 | 11 | ```ts 12 | interface Notification { 13 | _type: 'Notification' 14 | id: string 15 | type: 'error' | 'info' | 'warning' 16 | text: string 17 | buttons?: Array<{ 18 | label: string 19 | link: string 20 | }> 21 | suppressable?: boolean 22 | } 23 | ``` 24 | 25 | ### Fields 26 | 27 | - `_type`: The type of the object. Should always be `"Notification"`. 28 | - `id`: A unique identifier for the notification. This is used to determine if the notification has already been displayed to the user. 29 | - `type`: The type of notification. Can be `"info"`, `"warning"`, or `"error"`. 30 | - `text`: The text to display to the user. 31 | - `buttons`: An array of buttons to display to the user. Each button should have a `label` field and a `link` field. The `link` field should be a URL to open when the button is clicked. 32 | - `suppressable`: Whether or not the user should be able to suppress the notification. (defaults to `true`) -------------------------------------------------------------------------------- /.metadata/notifications.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "extensionHost", 6 | "request": "launch", 7 | "name": "Launch Extension", 8 | "runtimeExecutable": "${execPath}", 9 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 10 | "outFiles": ["${workspaceFolder}/out/**/*.js"] 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false 3 | } 4 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/tslint.json 9 | **/*.map 10 | node_modules -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v1.0.0 (2022-08-16) 2 | 3 | ## 🛠 Improvements 4 | - Complete system architecture overhaul to improve extendability, scalability, and maintainability @DylanTinianov (#99) 5 | - Improved testing suite @DylanTinianov (#108) 6 | - Added telemetry using Sentry to track errors @DylanTinianov (#103) 7 | - Removed Snyk from CI configuration @DylanTinianov (#95) 8 | - Setup End To End Tests @DylanTinianov (#132) 9 | - Add integration tests to CI @DylanTinianov (#140) 10 | - Add usage statistics to Sentry @DylanTinianov (#144) 11 | - Add activation analytics with Mixpanel @DylanTinianov (#145) 12 | 13 | ## 🐞 Bug Fixes 14 | - Fixed path issues on Windows @DylanTinianov (#112) 15 | 16 | ## 💥 Breaking Changes 17 | - Integrated Cadence Language Server hosted emulator @DylanTinianov (#109) 18 | 19 | ## ⭐ Features 20 | - Enabled users to enter a custom path to their flow.json file @DylanTinianov (#102) 21 | - Added a dependency installer to install missing dependencies such as flow-cli @DylanTinianov (#124) 22 | - Added a command to copy the active account to clipboard @DylanTinianov (#131) 23 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | @turbolent @jribbink @nialexsan -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at . 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, available at 118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | 123 | [homepage]: https://www.contributor-covenant.org 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | https://www.contributor-covenant.org/faq. Translations are available at 127 | https://www.contributor-covenant.org/translations. 128 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to flow-vscode 2 | 3 | The following is a set of guidelines for contributing to FCL and the Flow JS SDK 4 | These are mostly guidelines, not rules. 5 | Use your best judgment, and feel free to propose changes to this document in a pull request. 6 | 7 | ## Table Of Contents 8 | 9 | [Getting Started](#project-overview) 10 | 11 | [How Can I Contribute?](#how-can-i-contribute) 12 | 13 | - [Reporting Bugs](#reporting-bugs) 14 | - [Suggesting Enhancements](#suggesting-enhancements) 15 | - [Your First Code Contribution](#your-first-code-contribution) 16 | - [Pull Requests](#pull-requests) 17 | 18 | [Styleguides](#styleguides) 19 | 20 | - [Git Commit Messages](#git-commit-messages) 21 | - [TypeScript Styleguide](#typescript-styleguide) 22 | 23 | [Additional Notes](#additional-notes) 24 | 25 | # Developing the Extension 26 | 27 | Note that most editing features (type checking, code completion, etc.) are implemented 28 | in the [Cadence Language Server](https://github.com/onflow/cadence-tools/tree/master/languageserver). 29 | 30 | ## Pre-requisites 31 | 32 | - Must have Typescript installed globally: `npm i -g typescript` 33 | 34 | ## Getting Started 35 | - Run the Typescript watcher: `tsc -watch -p ./` 36 | - Launch the extension by pressing `F5` in VSCode 37 | - Manually reload the extension host when you make changes to TypeScript code 38 | 39 | ## Configuration for Extension Host if Missing (`launch.json`): 40 | 41 | ``` 42 | { 43 | "version": "0.2.0", 44 | "configurations": [ 45 | { 46 | "type": "extensionHost", 47 | "request": "launch", 48 | "name": "Launch Extension", 49 | "runtimeExecutable": "${execPath}", 50 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 51 | "outFiles": ["${workspaceFolder}/out/**/*.js"] 52 | } 53 | ] 54 | } 55 | ``` 56 | 57 | ## Building 58 | 59 | If you haven't already, install the dependencies: 60 | 61 | ```shell script 62 | npm install 63 | ``` 64 | 65 | Next, build and package the extension: 66 | 67 | ```shell script 68 | npm run package 69 | ``` 70 | 71 | This will result in a `.vsix` file containing the packaged extension. 72 | 73 | Install the packaged extension. 74 | 75 | ```shell script 76 | code --install-extension cadence-*.vsix 77 | ``` 78 | 79 | You can also run compile-install.sh instead. 80 | 81 | Restart VS Code and the extension should be installed! 82 | 83 | ## FAQ 84 | 85 | ### How can I debug the Language Server? 86 | 87 | It is possible to trace of the communication between the Visual Studio code extension and the Cadence language server. 88 | 89 | - Set the setting `Cadence > Trace: Server` to `Verbose` 90 | - In the bottom output view: 91 | - Select the "Output" tab 92 | - Select "Cadence" from the drop-down on the right 93 | 94 | If you don't see the output view, run the command `View: Toggle Output`. 95 | 96 | 97 | Make sure to re-select the lowest "Cadence" entry in the drop-down when the language server is restarted. 98 | 99 | 100 | ## How Can I Contribute? 101 | 102 | ### Reporting Bugs 103 | 104 | #### Before Submitting A Bug Report 105 | 106 | - **Search existing issues** to see if the problem has already been reported. 107 | If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one. 108 | 109 | #### How Do I Submit A (Good) Bug Report? 110 | 111 | Explain the problem and include additional details to help maintainers reproduce the problem: 112 | 113 | - **Use a clear and descriptive title** for the issue to identify the problem. 114 | - **Describe the exact steps which reproduce the problem** in as many details as possible. 115 | When listing steps, **don't just say what you did, but explain how you did it**. 116 | - **Provide specific examples to demonstrate the steps**. 117 | Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. 118 | If you're providing snippets in the issue, 119 | use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). 120 | - **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. 121 | - **Explain which behavior you expected to see instead and why.** 122 | - **Include error messages and stack traces** which show the output / crash and clearly demonstrate the problem. 123 | 124 | Provide more context by answering these questions: 125 | 126 | - **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens 127 | and under which conditions it normally happens. 128 | 129 | Include details about your configuration and environment: 130 | 131 | - **What is the version of the Cadence you're using**? 132 | - **What's the name and version of the Operating System you're using**? 133 | 134 | ### Suggesting Enhancements 135 | 136 | #### Before Submitting An Enhancement Suggestion 137 | 138 | - **Perform a cursory search** to see if the enhancement has already been suggested. 139 | If it has, add a comment to the existing issue instead of opening a new one. 140 | 141 | #### How Do I Submit A (Good) Enhancement Suggestion? 142 | 143 | Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). 144 | Create an issue and provide the following information: 145 | 146 | - **Use a clear and descriptive title** for the issue to identify the suggestion. 147 | - **Provide a step-by-step description of the suggested enhancement** in as many details as possible. 148 | - **Provide specific examples to demonstrate the steps**. 149 | Include copy/pasteable snippets which you use in those examples, 150 | as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). 151 | - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. 152 | - **Explain why this enhancement would be useful** to Cadence users. 153 | 154 | ### Your First Code Contribution 155 | 156 | Unsure where to begin contributing to Cadence? 157 | You can start by looking through these `good-first-issue` and `help-wanted` issues: 158 | 159 | - [Good first issues](https://github.com/onflow/cadence/labels/good%20first%20issue): 160 | issues which should only require a few lines of code, and a test or two. 161 | - [Help wanted issues](https://github.com/onflow/cadence/labels/help%20wanted): 162 | issues which should be a bit more involved than `good-first-issue` issues. 163 | 164 | Both issue lists are sorted by total number of comments. 165 | While not perfect, number of comments is a reasonable proxy for impact a given change will have. 166 | 167 | ### Pull Requests 168 | 169 | The process described here has several goals: 170 | 171 | - Maintain code quality 172 | - Fix problems that are important to users 173 | - Engage the community in working toward the best possible Developer/User Experience 174 | - Enable a sustainable system for the Cadence's maintainers to review contributions 175 | 176 | Please follow the [styleguides](#styleguides) to have your contribution considered by the maintainers. 177 | Reviewer(s) may ask you to complete additional design work, tests, 178 | or other changes before your pull request can be ultimately accepted. 179 | 180 | ## Styleguides 181 | 182 | ### Git Commit Messages 183 | 184 | - Use the present tense ("Add feature" not "Added feature") 185 | - Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 186 | - Limit the first line to 72 characters or less 187 | - Reference issues and pull requests liberally after the first line 188 | 189 | ### TypeScript Styleguide 190 | 191 | Use tslint with the setting in the root `.tslint.json` file 192 | 193 | ## Additional Notes 194 | 195 | Thank you for your interest in contributing to flow-vscode! 196 | -------------------------------------------------------------------------------- /OWNERSHIP_PROOF.md: -------------------------------------------------------------------------------- 1 | # Ownership Proof for VSCode Cadence Extension 2 | 3 | Hello! I am the author of the Cadence extension for Visual Studio Code. This is proof for my correspondance with Visual Studio Marketplace Support. 4 | 5 | This relates to the following ticket: 6 | `80c2ec86` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Logo 4 | 5 | 6 |

7 | Bringing Cadence, the resource-oriented smart contract language of Flow, to your VSCode Editor. 8 |
9 |

10 |

11 |
12 | 13 | [![CI](https://github.com/onflow/vscode-cadence/actions/workflows/ci.yml/badge.svg)](https://github.com/onflow/vscode-cadence/actions/workflows/ci.yml) 14 | [![Docs](https://img.shields.io/badge/Read%20The-Docs-blue)](https://developers.flow.com/tools/vscode-extension) 15 | [![Report Bug](https://img.shields.io/badge/-Report%20Bug-orange)](https://github.com/onflow/vscode-cadence/issues) 16 | [![Contribute](https://img.shields.io/badge/-Contribute-purple)](https://github.com/onflow/vscode-cadence/blob/master/CONTRIBUTING.md) 17 | 18 | ## Installation 19 | #### Install the Cadence extension from the **[Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=onflow.cadence)** 20 | #### The extension is also available on the **[Open VSX Registry](https://open-vsx.org/extension/onflow/cadence)** 21 | 22 | Once installed, the extension will help you install other dependencies such as the [Flow CLI](https://docs.onflow.org/flow-cli/install/). 23 | 24 | ## Features 25 | 26 | #### Cadence Language Server 27 | 28 | The Cadence extension provides a language server for Cadence. The language server is responsible for providing language features like code completion, diagnostics, and more. It is packaged within the Flow CLI and is managed by the extension. 29 | 30 | ### Debugging 31 | Use the debugger build into VSCode on Cadence files by creating a launch.json file. Make sure to have an emulator connected to enable debugging. 32 | 33 | ##### Example launch.json 34 | ``` 35 | { 36 | "version": "0.2.0", 37 | "configurations": [ 38 | { 39 | "type": "cadence", 40 | "request": "launch", 41 | "name": "Curent file", 42 | "program": "${file}", 43 | "stopOnEntry": true 44 | } 45 | ] 46 | } 47 | ``` 48 | 49 | #### But wait, there's much more than meets the eye. VSCode Cadence extension also offers: 50 | 51 | - Syntax highlighting (including in Markdown code fences) 52 | - Diagnostics (errors and warnings) 53 | - Code completion, including documentation 54 | - Type information on hover 55 | - Go to declaration 56 | - Go to symbol 57 | - Document outline 58 | - Renaming 59 | - Signature help 60 | - Symbol highlighting 61 | - Code actions 62 | - Declare constants, variables, functions, fields, and methods 63 | - Add missing members when implementing an interface 64 | - Apply removal suggestion 65 | - Apply replacement suggestion 66 | - Flow.json schema validation -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectId": "3ei47t", 3 | "integrationFolder": "extension/test/e2e/", 4 | "video": true, 5 | "screenshotOnRunFailure": true 6 | } 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | test: 3 | image: codercom/code-server:latest 4 | command: /source --auth none --disable-telemetry --disable-update-check --port 8888 5 | container_name: vscode 6 | ports: 7 | - "8888:8888" 8 | volumes: 9 | - ./extension/test/fixtures/workspace:/source 10 | - ./extension/test/fixtures/workspace/sbin:/usr/local/sbin 11 | - ./extension/test/fixtures/workspace/bin:/usr/local/bin 12 | -------------------------------------------------------------------------------- /docs/code_gen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onflow/vscode-cadence/4351a8051a99d8df69030a68d513b43a7f77222c/docs/code_gen.gif -------------------------------------------------------------------------------- /docs/deploy_contracts.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onflow/vscode-cadence/4351a8051a99d8df69030a68d513b43a7f77222c/docs/deploy_contracts.gif -------------------------------------------------------------------------------- /extension/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'portscanner-sync' 2 | declare module 'elliptic' 3 | declare module 'node-fetch' 4 | -------------------------------------------------------------------------------- /extension/language/language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | // symbol used for single line comment. Remove this entry if your language does not support line comments 4 | "lineComment": "//", 5 | // symbols used for start and end a block comment. Remove this entry if your language does not support block comments 6 | "blockComment": [ "/*", "*/" ] 7 | }, 8 | // symbols used as brackets 9 | "brackets": [ 10 | ["{", "}"], 11 | ["[", "]"], 12 | ["(", ")"] 13 | ], 14 | "autoClosingPairs": [ 15 | { "open": "{", "close": "}" }, 16 | { "open": "[", "close": "]" }, 17 | { "open": "(", "close": ")" }, 18 | { "open": "/*", "close": " */", "notIn": ["string"] } 19 | ], 20 | "surroundingPairs": [ 21 | ["{", "}"], 22 | ["[", "]"], 23 | ["(", ")"] 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /extension/language/syntaxes/codeblock.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [], 3 | "injectionSelector": "L:text.html.markdown", 4 | "patterns": [ 5 | { 6 | "include": "#superjs-code-block" 7 | } 8 | ], 9 | "repository": { 10 | "superjs-code-block": { 11 | "name": "markup.fenced_code.block.markdown", 12 | "begin": "(^|\\G)(\\s*)(\\`{3,}|~{3,})\\s*(?i:(cadence)\\b.*$)", 13 | "beginCaptures": { 14 | "3": { 15 | "name": "punctuation.definition.markdown" 16 | }, 17 | "5": { 18 | "name": "fenced_code.block.language" 19 | }, 20 | "6": { 21 | "name": "fenced_code.block.language.attributes" 22 | } 23 | }, 24 | "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", 25 | "endCaptures": { 26 | "3": { 27 | "name": "punctuation.definition.markdown" 28 | } 29 | }, 30 | "patterns": [ 31 | { 32 | "begin": "(^|\\G)(\\s*)(.*)", 33 | "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", 34 | "contentName": "meta.embedded.block.cadence", 35 | "patterns": [ 36 | { 37 | "include": "source.cadence" 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | }, 44 | "scopeName": "markdown.cadence.codeblock" 45 | } -------------------------------------------------------------------------------- /extension/src/commands/command-constants.ts: -------------------------------------------------------------------------------- 1 | // Command identifiers for locally handled commands 2 | export const RESTART_SERVER = 'cadence.restartServer' 3 | 4 | // Command identifires for dependencies 5 | export const CHECK_DEPENDENCIES = 'cadence.checkDepencencies' 6 | -------------------------------------------------------------------------------- /extension/src/commands/command-controller.ts: -------------------------------------------------------------------------------- 1 | /* CommandController is responsible for registering possible commands */ 2 | import { commands, Disposable, window } from 'vscode' 3 | import { ext } from '../main' 4 | import * as commandID from './command-constants' 5 | import * as Telemetry from '../telemetry/telemetry' 6 | import { DependencyInstaller } from '../dependency-installer/dependency-installer' 7 | 8 | export class CommandController { 9 | #cmds: Disposable[] // Hold onto commands 10 | #mappings = new Map void | Promise>() 11 | 12 | #dependencyInstaller: DependencyInstaller 13 | 14 | constructor (dependencyInstaller: DependencyInstaller) { 15 | this.#dependencyInstaller = dependencyInstaller 16 | this.#cmds = [] 17 | Telemetry.withTelemetry(this.#registerCommands.bind(this)) 18 | } 19 | 20 | async executeCommand (command: string): Promise { 21 | const cmd = this.#mappings.get(command) 22 | if (cmd !== undefined) { 23 | await cmd() 24 | return true 25 | } 26 | return false 27 | } 28 | 29 | // Registers a command with VS Code so it can be invoked by the user. 30 | #registerCommand (command: string, callback: () => void | Promise): void { 31 | const commandCallback = (): void | Promise => Telemetry.withTelemetry(callback) 32 | const cmd = commands.registerCommand(command, commandCallback) 33 | this.#cmds.push(cmd) 34 | this.#mappings.set(command, commandCallback) 35 | } 36 | 37 | // Registers all commands that are handled by the extension (as opposed to 38 | // those handled by the Language Server). 39 | #registerCommands (): void { 40 | this.#registerCommand(commandID.RESTART_SERVER, this.#restartServer.bind(this)) 41 | this.#registerCommand(commandID.CHECK_DEPENDENCIES, this.#checkDependencies.bind(this)) 42 | } 43 | 44 | async #restartServer (): Promise { 45 | await ext?.languageServer.restart() 46 | } 47 | 48 | async #checkDependencies (): Promise { 49 | await this.#dependencyInstaller.checkDependencies() 50 | 51 | // Show message if all dependencies are already installed 52 | const missingDependencies = await this.#dependencyInstaller.missingDependencies.getValue() 53 | if (missingDependencies.length === 0) { 54 | void window.showInformationMessage('All dependencies are already installed') 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /extension/src/crypto-polyfill.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto' 2 | 3 | if (globalThis.crypto == null) { 4 | Object.defineProperty(globalThis, 'crypto', { 5 | value: crypto 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /extension/src/dependency-installer/dependency-installer.ts: -------------------------------------------------------------------------------- 1 | import { window } from 'vscode' 2 | import { InstallFlowCLI } from './installers/flow-cli-installer' 3 | import { Installer, InstallerConstructor, InstallerContext, InstallError } from './installer' 4 | import { promptUserErrorMessage } from '../ui/prompts' 5 | import { StateCache } from '../utils/state-cache' 6 | import { LanguageServerAPI } from '../server/language-server' 7 | import { CliProvider } from '../flow-cli/cli-provider' 8 | 9 | const INSTALLERS: InstallerConstructor[] = [ 10 | InstallFlowCLI 11 | ] 12 | 13 | export class DependencyInstaller { 14 | registeredInstallers: Installer[] = [] 15 | missingDependencies: StateCache 16 | #installerContext: InstallerContext 17 | 18 | constructor (languageServerApi: LanguageServerAPI, cliProvider: CliProvider) { 19 | this.#installerContext = { 20 | refreshDependencies: this.checkDependencies.bind(this), 21 | languageServerApi, 22 | cliProvider 23 | } 24 | 25 | // Register installers 26 | this.#registerInstallers() 27 | 28 | // Create state cache for missing dependencies 29 | this.missingDependencies = new StateCache(async () => { 30 | const missing: Installer[] = [] 31 | for (const installer of this.registeredInstallers) { 32 | if (!(await installer.verifyInstall())) { 33 | missing.push(installer) 34 | } 35 | } 36 | return missing 37 | }) 38 | 39 | // Display error message if dependencies are missing 40 | this.missingDependencies.subscribe((missing: Installer[]) => { 41 | if (missing.length !== 0) { 42 | // Prompt user to install missing dependencies 43 | promptUserErrorMessage( 44 | 'Not all dependencies are installed: ' + missing.map(x => x.getName()).join(', '), 45 | [ 46 | { 47 | label: 'Install Missing Dependencies', 48 | callback: () => { void this.#installMissingDependencies() } 49 | } 50 | ] 51 | ) 52 | } 53 | }) 54 | } 55 | 56 | async checkDependencies (): Promise { 57 | // Invalidate and wait for state to update 58 | // This will trigger the missingDependencies subscriptions 59 | this.missingDependencies.invalidate() 60 | await this.missingDependencies.getValue() 61 | } 62 | 63 | async installMissing (): Promise { 64 | await this.#installMissingDependencies() 65 | } 66 | 67 | #registerInstallers (): void { 68 | // Recursively register installers and their dependencies in the correct order 69 | (function registerInstallers (this: DependencyInstaller, installers: InstallerConstructor[]) { 70 | installers.forEach((_installer) => { 71 | // Check if installer is already registered 72 | if (this.registeredInstallers.find(x => x instanceof _installer) != null) { return } 73 | 74 | // Register installer and dependencies 75 | const installer = new _installer(this.#installerContext) 76 | registerInstallers.bind(this)(installer.dependencies) 77 | this.registeredInstallers.push(installer) 78 | }) 79 | }).bind(this)(INSTALLERS) 80 | } 81 | 82 | async #installMissingDependencies (): Promise { 83 | const missing = await this.missingDependencies.getValue() 84 | const installed: Installer[] = this.registeredInstallers.filter(x => !missing.includes(x)) 85 | 86 | await new Promise((resolve) => { 87 | setTimeout(() => { resolve() }, 2000) 88 | }) 89 | 90 | for (const installer of missing) { 91 | try { 92 | // Check if dependencies are installed 93 | const missingDeps = installer.dependencies 94 | .filter(x => installed.find(y => y instanceof x) == null) 95 | .map(x => this.registeredInstallers.find(y => y instanceof x)) 96 | 97 | // Show error if dependencies are missing 98 | if (missingDeps.length !== 0) { 99 | throw new InstallError('Cannot install ' + installer.getName() + '. Missing depdenencies: ' + missingDeps.map(x => x?.getName()).join(', ')) 100 | } 101 | 102 | await installer.runInstall() 103 | installed.push(installer) 104 | } catch (err) { 105 | if (err instanceof InstallError) { 106 | void window.showErrorMessage(err.message) 107 | } 108 | } 109 | } 110 | 111 | // Refresh missing dependencies 112 | this.missingDependencies.invalidate() 113 | const failed = await this.missingDependencies.getValue() 114 | 115 | if (failed.length !== 0) { 116 | // Find all failed installations 117 | void window.showErrorMessage('Failed to install all dependencies. The following may need to be installed manually: ' + failed.map(x => x.getName()).join(', ')) 118 | } else { 119 | if (process.platform === 'win32') { 120 | // Windows requires a restart of VSCode to refresh environment variables 121 | void window.showInformationMessage('All dependencies installed successfully. Newly installed dependencies will not be available in terminals until VSCode is restarted.') 122 | } else if (process.platform !== 'darwin') { 123 | // Linux requires a fresh login to refresh environment variables for new terminals 124 | void window.showInformationMessage('All dependencies installed successfully. Newly installed dependencies will not be available in terminals until you log out and back in.') 125 | } else { 126 | // MacOS requires a restart of active terminals to refresh environment variables 127 | void window.showInformationMessage('All dependencies installed successfully. You may need to restart active terminals.') 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /extension/src/dependency-installer/installer.ts: -------------------------------------------------------------------------------- 1 | /* Abstract Installer class */ 2 | import { window } from 'vscode' 3 | import { envVars } from '../utils/shell/env-vars' 4 | import { LanguageServerAPI } from '../server/language-server' 5 | import { CliProvider } from '../flow-cli/cli-provider' 6 | 7 | // InstallError is thrown if install fails 8 | export class InstallError extends Error {} 9 | 10 | export interface InstallerContext { 11 | refreshDependencies: () => Promise 12 | languageServerApi: LanguageServerAPI 13 | cliProvider: CliProvider 14 | } 15 | 16 | export type InstallerConstructor = new (context: InstallerContext) => Installer 17 | 18 | export abstract class Installer { 19 | dependencies: InstallerConstructor[] 20 | #installerName: string 21 | 22 | constructor (name: string, dependencies: InstallerConstructor[]) { 23 | this.dependencies = dependencies 24 | this.#installerName = name 25 | } 26 | 27 | getName (): string { 28 | return this.#installerName 29 | } 30 | 31 | async runInstall (): Promise { 32 | void window.showInformationMessage('Running ' + this.#installerName + ' installer, please wait...') 33 | 34 | try { 35 | await this.install() 36 | } catch { 37 | throw new InstallError('Failed to install: ' + this.#installerName) 38 | } 39 | 40 | // Refresh env vars 41 | envVars.invalidate() 42 | 43 | // Check if install was successful 44 | if (!(await this.verifyInstall())) { 45 | throw new InstallError('Failed to install: ' + this.#installerName) 46 | } 47 | 48 | void window.showInformationMessage(this.#installerName + ' installed sucessfully.') 49 | } 50 | 51 | // Installation logic 52 | protected abstract install (): Promise | void 53 | 54 | // Logic to check if dependency is installed 55 | abstract verifyInstall (): Promise 56 | } 57 | -------------------------------------------------------------------------------- /extension/src/dependency-installer/installers/flow-cli-installer.ts: -------------------------------------------------------------------------------- 1 | /* Installer for Flow CLI */ 2 | import { window } from 'vscode' 3 | import { execVscodeTerminal, tryExecPowerShell, tryExecUnixDefault } from '../../utils/shell/exec' 4 | import { promptUserInfoMessage, promptUserErrorMessage } from '../../ui/prompts' 5 | import { Installer, InstallerConstructor, InstallerContext } from '../installer' 6 | import * as semver from 'semver' 7 | import fetch from 'node-fetch' 8 | import { HomebrewInstaller } from './homebrew-installer' 9 | import { KNOWN_FLOW_COMMANDS } from '../../flow-cli/cli-versions-provider' 10 | 11 | // Relevant subset of Homebrew formulae JSON 12 | interface HomebrewVersionInfo { 13 | versions: { 14 | stable: string 15 | } 16 | } 17 | 18 | // Command to check flow-cli 19 | const COMPATIBLE_FLOW_CLI_VERSIONS = '>=2.0.0' 20 | 21 | // Shell install commands 22 | const BREW_INSTALL_FLOW_CLI = 'brew update && brew install flow-cli' 23 | const POWERSHELL_INSTALL_CMD = (githubToken?: string): string => 24 | `iex "& { $(irm 'https://raw.githubusercontent.com/onflow/flow-cli/master/install.ps1') } ${ 25 | githubToken != null ? `-GitHubToken ${githubToken} ` : '' 26 | }"` 27 | const BASH_INSTALL_FLOW_CLI = (githubToken?: string): string => 28 | `${ 29 | githubToken != null ? `GITHUB_TOKEN=${githubToken} ` : '' 30 | }sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)"` 31 | const VERSION_INFO_URL = 'https://formulae.brew.sh/api/formula/flow-cli.json' 32 | 33 | export class InstallFlowCLI extends Installer { 34 | #githubToken: string | undefined 35 | #context: InstallerContext 36 | 37 | constructor (context: InstallerContext) { 38 | // Homebrew is a dependency for macos and linux 39 | const dependencies: InstallerConstructor[] = [] 40 | if (process.platform === 'darwin') { 41 | dependencies.push(HomebrewInstaller) 42 | } 43 | 44 | super('Flow CLI', dependencies) 45 | this.#githubToken = process.env.GITHUB_TOKEN 46 | this.#context = context 47 | } 48 | 49 | async install (): Promise { 50 | const isActive = this.#context.languageServerApi.isActive ?? false 51 | if (isActive) await this.#context.languageServerApi.deactivate() 52 | const OS_TYPE = process.platform 53 | 54 | try { 55 | switch (OS_TYPE) { 56 | case 'darwin': 57 | await this.#install_macos() 58 | break 59 | case 'win32': 60 | await this.#install_windows() 61 | break 62 | default: 63 | await this.#install_bash_cmd() 64 | break 65 | } 66 | } catch { 67 | void window.showErrorMessage('Failed to install Flow CLI') 68 | } 69 | if (isActive) await this.#context.languageServerApi.activate() 70 | } 71 | 72 | async #install_macos (): Promise { 73 | // Install Flow CLI using homebrew 74 | await execVscodeTerminal('Install Flow CLI', BREW_INSTALL_FLOW_CLI) 75 | } 76 | 77 | async #install_windows (): Promise { 78 | // Retry if bad GH token 79 | if (this.#githubToken != null && await tryExecPowerShell(POWERSHELL_INSTALL_CMD(this.#githubToken))) { return } 80 | await execVscodeTerminal('Install Flow CLI', POWERSHELL_INSTALL_CMD(this.#githubToken)) 81 | } 82 | 83 | async #install_bash_cmd (): Promise { 84 | // Retry if bad GH token 85 | if (this.#githubToken != null && await tryExecUnixDefault(BASH_INSTALL_FLOW_CLI(this.#githubToken))) { return } 86 | await execVscodeTerminal('Install Flow CLI', BASH_INSTALL_FLOW_CLI()) 87 | } 88 | 89 | async maybeNotifyNewerVersion (currentVersion: semver.SemVer): Promise { 90 | try { 91 | const response = await fetch(VERSION_INFO_URL) 92 | const { versions: { stable: latestStr } }: HomebrewVersionInfo = await response.json() 93 | const latest: semver.SemVer | null = semver.parse(latestStr) 94 | 95 | // Check if latest version > current version 96 | if (latest != null && latestStr != null && semver.compare(latest, currentVersion) === 1) { 97 | promptUserInfoMessage( 98 | 'There is a new Flow CLI version available: ' + latest.format(), 99 | [{ 100 | label: 'Install latest Flow CLI', 101 | callback: async () => { 102 | await this.runInstall() 103 | await this.#context.refreshDependencies() 104 | } 105 | }] 106 | ) 107 | } 108 | } catch (e) {} 109 | } 110 | 111 | async checkVersion (version: semver.SemVer): Promise { 112 | // Get user's version informaton 113 | this.#context.cliProvider.refresh() 114 | if (version == null) return false 115 | 116 | if (!semver.satisfies(version, COMPATIBLE_FLOW_CLI_VERSIONS, { 117 | includePrerelease: true 118 | })) { 119 | promptUserErrorMessage( 120 | 'Incompatible Flow CLI version: ' + version.format(), 121 | [{ 122 | label: 'Install latest Flow CLI', 123 | callback: async () => { 124 | await this.runInstall() 125 | await this.#context.refreshDependencies() 126 | } 127 | }] 128 | ) 129 | return false 130 | } 131 | 132 | // Maybe notify user of newer version, non-blocking 133 | void this.maybeNotifyNewerVersion(version) 134 | 135 | return true 136 | } 137 | 138 | async verifyInstall (): Promise { 139 | // Check if flow version is valid to verify install 140 | this.#context.cliProvider.refresh() 141 | const installedVersions = await this.#context.cliProvider.getBinaryVersions().catch((e) => { 142 | void window.showErrorMessage(`Failed to check CLI version: ${String(e.message)}`) 143 | return [] 144 | }) 145 | const version = installedVersions.find(y => y.command === KNOWN_FLOW_COMMANDS.DEFAULT)?.version 146 | if (version == null) return false 147 | 148 | // Check flow-cli version number 149 | return await this.checkVersion(version) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /extension/src/dependency-installer/installers/homebrew-installer.ts: -------------------------------------------------------------------------------- 1 | /* Installer for Flow CLI */ 2 | import { execVscodeTerminal, tryExecDefault } from '../../utils/shell/exec' 3 | import { Installer } from '../installer' 4 | 5 | // Flow CLI with homebrew 6 | const CHECK_HOMEBREW_CMD = 'brew help help' // Run this to check if brew is executable 7 | const BASH_INSTALL_HOMEBREW = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' 8 | 9 | export class HomebrewInstaller extends Installer { 10 | constructor () { 11 | super('Homebrew', []) 12 | } 13 | 14 | async install (): Promise { 15 | await execVscodeTerminal('Install Homebrew', BASH_INSTALL_HOMEBREW) 16 | } 17 | 18 | async verifyInstall (): Promise { 19 | return await tryExecDefault(CHECK_HOMEBREW_CMD) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /extension/src/extension.ts: -------------------------------------------------------------------------------- 1 | import './crypto-polyfill' 2 | 3 | import { CommandController } from './commands/command-controller' 4 | import { ExtensionContext } from 'vscode' 5 | import { DependencyInstaller } from './dependency-installer/dependency-installer' 6 | import { Settings } from './settings/settings' 7 | import { CliProvider } from './flow-cli/cli-provider' 8 | import { JSONSchemaProvider } from './json-schema-provider' 9 | import { LanguageServerAPI } from './server/language-server' 10 | import { FlowConfig } from './server/flow-config' 11 | import { TestProvider } from './test-provider/test-provider' 12 | import { StorageProvider } from './storage/storage-provider' 13 | import * as path from 'path' 14 | import { NotificationProvider } from './ui/notification-provider' 15 | import { CliSelectionProvider } from './flow-cli/cli-selection-provider' 16 | 17 | // The container for all data relevant to the extension. 18 | export class Extension { 19 | // The extension singleton 20 | static #instance: Extension 21 | static initialized = false 22 | 23 | static initialize (settings: Settings, ctx: ExtensionContext): Extension { 24 | Extension.#instance = new Extension(settings, ctx) 25 | Extension.initialized = true 26 | return Extension.#instance 27 | } 28 | 29 | ctx: ExtensionContext 30 | languageServer: LanguageServerAPI 31 | #dependencyInstaller: DependencyInstaller 32 | #commands: CommandController 33 | #testProvider: TestProvider 34 | #schemaProvider: JSONSchemaProvider 35 | #cliSelectionProvider: CliSelectionProvider 36 | 37 | private constructor (settings: Settings, ctx: ExtensionContext) { 38 | this.ctx = ctx 39 | 40 | // Initialize Storage Provider 41 | const storageProvider = new StorageProvider(ctx?.globalState) 42 | 43 | // Display any notifications from remote server 44 | const notificationProvider = new NotificationProvider(storageProvider) 45 | notificationProvider.activate() 46 | 47 | // Register CliProvider 48 | const cliProvider = new CliProvider(settings) 49 | 50 | // Register CliSelectionProvider 51 | this.#cliSelectionProvider = new CliSelectionProvider(cliProvider) 52 | 53 | // Register JSON schema provider 54 | this.#schemaProvider = new JSONSchemaProvider(ctx.extensionPath, cliProvider) 55 | 56 | // Initialize Flow Config 57 | const flowConfig = new FlowConfig(settings) 58 | void flowConfig.activate() 59 | 60 | // Initialize Language Server 61 | this.languageServer = new LanguageServerAPI(settings, cliProvider, flowConfig) 62 | 63 | // Check for any missing dependencies 64 | // The language server will start if all dependencies are installed 65 | // Otherwise, the language server will not start and will start after 66 | // the user installs the missing dependencies 67 | this.#dependencyInstaller = new DependencyInstaller(this.languageServer, cliProvider) 68 | this.#dependencyInstaller.missingDependencies.subscribe((missing) => { 69 | if (missing.length === 0) { 70 | void this.languageServer.activate() 71 | } else { 72 | void this.languageServer.deactivate() 73 | } 74 | }) 75 | 76 | // Initialize ExtensionCommands 77 | this.#commands = new CommandController(this.#dependencyInstaller) 78 | 79 | // Initialize TestProvider 80 | const extensionPath = ctx.extensionPath ?? '' 81 | const parserLocation = path.resolve(extensionPath, 'out/extension/cadence-parser.wasm') 82 | this.#testProvider = new TestProvider(parserLocation, settings, flowConfig) 83 | } 84 | 85 | // Called on exit 86 | async deactivate (): Promise { 87 | await this.languageServer.deactivate() 88 | this.#testProvider.dispose() 89 | this.#schemaProvider.dispose() 90 | this.#cliSelectionProvider.dispose() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /extension/src/flow-cli/cli-provider.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject, Observable, distinctUntilChanged, pairwise, startWith } from 'rxjs' 2 | import { StateCache } from '../utils/state-cache' 3 | import * as vscode from 'vscode' 4 | import { Settings } from '../settings/settings' 5 | import { isEqual } from 'lodash' 6 | import { CliBinary, CliVersionsProvider, KNOWN_FLOW_COMMANDS } from './cli-versions-provider' 7 | 8 | export class CliProvider { 9 | #selectedBinaryName: BehaviorSubject 10 | #currentBinary$: StateCache 11 | #cliVersionsProvider: CliVersionsProvider 12 | #settings: Settings 13 | 14 | constructor (settings: Settings) { 15 | const initialBinaryPath = settings.getSettings().flowCommand 16 | 17 | this.#settings = settings 18 | this.#cliVersionsProvider = new CliVersionsProvider([initialBinaryPath]) 19 | this.#selectedBinaryName = new BehaviorSubject(initialBinaryPath) 20 | this.#currentBinary$ = new StateCache(async () => { 21 | const name: string = this.#selectedBinaryName.getValue() 22 | const versionCache = this.#cliVersionsProvider.get(name) 23 | if (versionCache == null) return null 24 | return await versionCache.getValue() 25 | }) 26 | 27 | // Bind the selected binary to the settings 28 | this.#settings.watch$(config => config.flowCommand).subscribe((flowCommand) => { 29 | this.#selectedBinaryName.next(flowCommand) 30 | }) 31 | 32 | // Display warning to user if binary doesn't exist (only if not using the default binary) 33 | this.currentBinary$.subscribe((binary) => { 34 | if (binary === null && this.#selectedBinaryName.getValue() !== KNOWN_FLOW_COMMANDS.DEFAULT) { 35 | void vscode.window.showErrorMessage(`The configured Flow CLI binary "${this.#selectedBinaryName.getValue()}" does not exist. Please check your settings.`) 36 | } 37 | }) 38 | 39 | this.#watchForBinaryChanges() 40 | } 41 | 42 | #watchForBinaryChanges (): void { 43 | // Subscribe to changes in the selected binary to update the caches 44 | this.#selectedBinaryName.pipe(distinctUntilChanged(), startWith(null), pairwise()).subscribe(([prev, curr]) => { 45 | // Remove the previous binary from the cache 46 | if (prev != null) this.#cliVersionsProvider.remove(prev) 47 | 48 | // Add the current binary to the cache 49 | if (curr != null) this.#cliVersionsProvider.add(curr) 50 | 51 | // Invalidate the current binary cache 52 | this.#currentBinary$.invalidate() 53 | }) 54 | } 55 | 56 | async getCurrentBinary (): Promise { 57 | return await this.#currentBinary$.getValue() 58 | } 59 | 60 | async setCurrentBinary (name: string): Promise { 61 | if (vscode.workspace.workspaceFolders == null) { 62 | await this.#settings.updateSettings({ flowCommand: name }, vscode.ConfigurationTarget.Global) 63 | } else { 64 | await this.#settings.updateSettings({ flowCommand: name }) 65 | } 66 | } 67 | 68 | get currentBinary$ (): Observable { 69 | return this.#currentBinary$.pipe(distinctUntilChanged(isEqual)) 70 | } 71 | 72 | async getBinaryVersions (): Promise { 73 | return await this.#cliVersionsProvider.getVersions() 74 | } 75 | 76 | get binaryVersions$ (): Observable { 77 | return this.#cliVersionsProvider.versions$.pipe(distinctUntilChanged(isEqual)) 78 | } 79 | 80 | // Refresh all cached binary versions 81 | refresh (): void { 82 | this.#cliVersionsProvider.refresh() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /extension/src/flow-cli/cli-selection-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { zip } from 'rxjs' 3 | import { CliProvider } from './cli-provider' 4 | import { SemVer } from 'semver' 5 | import { CliBinary } from './cli-versions-provider' 6 | 7 | const CHANGE_CLI_BINARY = 'cadence.changeFlowCliBinary' 8 | const GET_BINARY_LABEL = (version: SemVer): string => `Flow CLI v${version.format()}` 9 | 10 | export class CliSelectionProvider { 11 | #statusBarItem: vscode.StatusBarItem | undefined 12 | #cliProvider: CliProvider 13 | #showSelector: boolean = false 14 | #versionSelector: vscode.QuickPick | undefined 15 | #disposables: vscode.Disposable[] = [] 16 | 17 | constructor (cliProvider: CliProvider) { 18 | this.#cliProvider = cliProvider 19 | 20 | // Register the command to toggle the version 21 | this.#disposables.push(vscode.commands.registerCommand(CHANGE_CLI_BINARY, async () => { 22 | this.#cliProvider.refresh() 23 | await this.#toggleSelector(true) 24 | })) 25 | 26 | // Register UI components 27 | zip(this.#cliProvider.currentBinary$, this.#cliProvider.binaryVersions$).subscribe(() => { 28 | void this.#refreshSelector() 29 | }) 30 | this.#cliProvider.currentBinary$.subscribe((binary) => { 31 | this.#statusBarItem?.dispose() 32 | this.#statusBarItem = this.#createStatusBarItem(binary?.version ?? null) 33 | this.#statusBarItem.show() 34 | }) 35 | } 36 | 37 | #createStatusBarItem (version: SemVer | null): vscode.StatusBarItem { 38 | const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1) 39 | statusBarItem.command = CHANGE_CLI_BINARY 40 | statusBarItem.color = new vscode.ThemeColor('statusBar.foreground') 41 | statusBarItem.tooltip = 'Click to change the Flow CLI version' 42 | 43 | if (version != null) { 44 | statusBarItem.text = GET_BINARY_LABEL(version) 45 | } else { 46 | statusBarItem.text = '$(error) Flow CLI not found' 47 | statusBarItem.color = new vscode.ThemeColor('errorForeground') 48 | } 49 | 50 | return statusBarItem 51 | } 52 | 53 | #createVersionSelector (currentBinary: CliBinary | null, availableBinaries: CliBinary[]): vscode.QuickPick { 54 | const versionSelector = vscode.window.createQuickPick() 55 | versionSelector.title = 'Select a Flow CLI version' 56 | 57 | // Update selected binary when the user selects a version 58 | this.#disposables.push(versionSelector.onDidAccept(async () => { 59 | if (versionSelector.selectedItems.length === 0) return 60 | await this.#toggleSelector(false) 61 | 62 | const selected = versionSelector.selectedItems[0] 63 | 64 | if (selected instanceof CustomBinaryItem) { 65 | void vscode.window.showOpenDialog({ 66 | canSelectFiles: true, 67 | canSelectFolders: false, 68 | canSelectMany: false, 69 | openLabel: 'Choose a Flow CLI binary' 70 | }).then((uri) => { 71 | if (uri != null) { 72 | void this.#cliProvider.setCurrentBinary(uri[0].fsPath) 73 | } 74 | }) 75 | } else if (selected instanceof AvailableBinaryItem) { 76 | void this.#cliProvider.setCurrentBinary(selected.command) 77 | } 78 | })) 79 | 80 | this.#disposables.push(versionSelector.onDidHide(() => { 81 | void this.#toggleSelector(false) 82 | })) 83 | 84 | // Update available versions 85 | const items: Array = availableBinaries.map(binary => new AvailableBinaryItem(binary)) 86 | items.push(new CustomBinaryItem()) 87 | 88 | // Hoist the current binary to the top of the list 89 | const currentBinaryIndex = items.findIndex(item => 90 | item instanceof AvailableBinaryItem && 91 | currentBinary != null && 92 | item.command === currentBinary.command 93 | ) 94 | if (currentBinaryIndex !== -1) { 95 | const currentBinaryItem = items[currentBinaryIndex] 96 | items.splice(currentBinaryIndex, 1) 97 | items.unshift(currentBinaryItem) 98 | } 99 | 100 | versionSelector.items = items 101 | return versionSelector 102 | } 103 | 104 | async #toggleSelector (show: boolean): Promise { 105 | this.#showSelector = show 106 | await this.#refreshSelector() 107 | } 108 | 109 | async #refreshSelector (): Promise { 110 | if (this.#showSelector) { 111 | this.#versionSelector?.dispose() 112 | const currentBinary = await this.#cliProvider.getCurrentBinary() 113 | const availableBinaries = await this.#cliProvider.getBinaryVersions() 114 | this.#versionSelector = this.#createVersionSelector(currentBinary, availableBinaries) 115 | this.#disposables.push(this.#versionSelector) 116 | this.#versionSelector.show() 117 | } else { 118 | this.#versionSelector?.dispose() 119 | } 120 | } 121 | 122 | dispose (): void { 123 | this.#disposables.forEach(disposable => disposable.dispose()) 124 | this.#statusBarItem?.dispose() 125 | this.#versionSelector?.dispose() 126 | } 127 | } 128 | 129 | class AvailableBinaryItem implements vscode.QuickPickItem { 130 | detail?: string 131 | picked?: boolean 132 | alwaysShow?: boolean 133 | #binary: CliBinary 134 | 135 | constructor (binary: CliBinary) { 136 | this.#binary = binary 137 | } 138 | 139 | get label (): string { 140 | return GET_BINARY_LABEL(this.#binary.version) 141 | } 142 | 143 | get description (): string { 144 | return `(${this.#binary.command})` 145 | } 146 | 147 | get command (): string { 148 | return this.#binary.command 149 | } 150 | } 151 | 152 | class CustomBinaryItem implements vscode.QuickPickItem { 153 | label: string 154 | 155 | constructor () { 156 | this.label = 'Choose a custom version...' 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /extension/src/flow-cli/cli-versions-provider.ts: -------------------------------------------------------------------------------- 1 | import * as semver from 'semver' 2 | import { StateCache } from '../utils/state-cache' 3 | import { execDefault } from '../utils/shell/exec' 4 | import { Observable, distinctUntilChanged } from 'rxjs' 5 | import { isEqual } from 'lodash' 6 | 7 | const CHECK_FLOW_CLI_CMD = (flowCommand: string): string => `${flowCommand} version --output=json` 8 | const CHECK_FLOW_CLI_CMD_NO_JSON = (flowCommand: string): string => `${flowCommand} version` 9 | 10 | export enum KNOWN_FLOW_COMMANDS { 11 | DEFAULT = 'flow', 12 | } 13 | 14 | // Matches the version number from the output of the Flow CLI 15 | const LEGACY_VERSION_REGEXP = /Version:\s*v(.*)(?:\s|$)/m 16 | 17 | export interface CliBinary { 18 | command: string 19 | version: semver.SemVer 20 | } 21 | 22 | interface FlowVersionOutput { 23 | version: string 24 | } 25 | 26 | export class CliVersionsProvider { 27 | #rootCache: StateCache 28 | #caches: { [key: string]: StateCache } = {} 29 | 30 | constructor (seedBinaries: string[] = []) { 31 | // Seed the caches with the known binaries 32 | Object.values(KNOWN_FLOW_COMMANDS).forEach((bin) => { 33 | this.add(bin) 34 | }) 35 | 36 | // Seed the caches with any additional binaries 37 | seedBinaries.forEach((bin) => { 38 | this.add(bin) 39 | }) 40 | 41 | // Create the root cache. This cache will hold all the binary information 42 | // and is a combination of all the individual caches for each binary 43 | this.#rootCache = new StateCache(async () => { 44 | const binaries = await Promise.all( 45 | Object.keys(this.#caches).map(async (bin) => { 46 | return await this.#caches[bin].getValue().catch(() => null) 47 | }) 48 | ) 49 | 50 | // Filter out missing binaries 51 | return binaries.filter((bin) => bin != null) as CliBinary[] 52 | }) 53 | } 54 | 55 | add (command: string): void { 56 | if (this.#caches[command] != null) return 57 | this.#caches[command] = new StateCache(async () => await this.#fetchBinaryInformation(command)) 58 | this.#caches[command].subscribe(() => { 59 | this.#rootCache?.invalidate() 60 | }) 61 | this.#rootCache?.invalidate() 62 | } 63 | 64 | remove (command: string): void { 65 | // Known binaries cannot be removed 66 | if (this.#caches[command] == null || (Object.values(KNOWN_FLOW_COMMANDS) as string[]).includes(command)) return 67 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 68 | delete this.#caches[command] 69 | this.#rootCache?.invalidate() 70 | } 71 | 72 | get (name: string): StateCache | null { 73 | return this.#caches[name] ?? null 74 | } 75 | 76 | // Fetches the binary information for the given binary 77 | async #fetchBinaryInformation (bin: string): Promise { 78 | try { 79 | // Get user's version informaton 80 | const buffer: string = (await execDefault(CHECK_FLOW_CLI_CMD( 81 | bin 82 | ))).stdout 83 | 84 | // Format version string from output 85 | const versionInfo: FlowVersionOutput = JSON.parse(buffer) 86 | 87 | return cliBinaryFromVersion(bin, versionInfo.version) 88 | } catch { 89 | // Fallback to old method if JSON is not supported/fails 90 | return await this.#fetchBinaryInformationOld(bin) 91 | } 92 | } 93 | 94 | // Old version of fetchBinaryInformation (before JSON was supported) 95 | // Used as fallback for old CLI versions 96 | async #fetchBinaryInformationOld (bin: string): Promise { 97 | try { 98 | // Get user's version informaton 99 | const output = (await execDefault(CHECK_FLOW_CLI_CMD_NO_JSON( 100 | bin 101 | ))) 102 | 103 | let versionStr: string | null = parseFlowCliVersion(output.stdout) 104 | if (versionStr === null) { 105 | // Try to fallback to stderr as patch for bugged version 106 | versionStr = parseFlowCliVersion(output.stderr) 107 | } 108 | 109 | if (versionStr == null) return null 110 | 111 | return cliBinaryFromVersion(bin, versionStr) 112 | } catch { 113 | return null 114 | } 115 | } 116 | 117 | refresh (): void { 118 | Object.keys(this.#caches).forEach((bin) => { 119 | this.#caches[bin].invalidate() 120 | }) 121 | this.#rootCache.invalidate() 122 | } 123 | 124 | async getVersions (): Promise { 125 | return await this.#rootCache.getValue() 126 | } 127 | 128 | get versions$ (): Observable { 129 | return this.#rootCache.pipe(distinctUntilChanged(isEqual)) 130 | } 131 | } 132 | 133 | export function parseFlowCliVersion (buffer: Buffer | string): string | null { 134 | const rawMatch = buffer.toString().match(LEGACY_VERSION_REGEXP)?.[1] ?? null 135 | if (rawMatch == null) return null 136 | return semver.clean(rawMatch) 137 | } 138 | 139 | function cliBinaryFromVersion (bin: string, versionStr: string): CliBinary | null { 140 | // Ensure user has a compatible version number installed 141 | const version: semver.SemVer | null = semver.parse(versionStr) 142 | if (version === null) return null 143 | 144 | return { command: bin, version } 145 | } 146 | -------------------------------------------------------------------------------- /extension/src/json-schema-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { readFile } from 'fs' 3 | import { promisify } from 'util' 4 | import { resolve } from 'path' 5 | import fetch from 'node-fetch' 6 | import { CliProvider } from './flow-cli/cli-provider' 7 | 8 | const CADENCE_SCHEMA_URI = 'cadence-schema' 9 | const GET_FLOW_SCHEMA_URL = (version: string): string => `https://raw.githubusercontent.com/onflow/flow-cli/v${version}/flowkit/schema.json` 10 | 11 | // This class provides the JSON schema for the flow.json file 12 | // It is accessible via the URI scheme "cadence-schema:///flow.json" 13 | export class JSONSchemaProvider implements vscode.FileSystemProvider, vscode.Disposable { 14 | #contentProviderDisposable: vscode.Disposable | undefined 15 | #extensionPath: string 16 | #cliProvider: CliProvider 17 | #schemaCache: { [version: string]: Promise } = {} 18 | 19 | constructor ( 20 | extensionPath: string, 21 | cliProvider: CliProvider 22 | ) { 23 | this.#extensionPath = extensionPath 24 | this.#cliProvider = cliProvider 25 | 26 | // Register the schema provider 27 | this.#contentProviderDisposable = vscode.workspace.registerFileSystemProvider( 28 | CADENCE_SCHEMA_URI, 29 | this 30 | ) 31 | } 32 | 33 | async #getFlowSchema (): Promise { 34 | const cliBinary = await this.#cliProvider.getCurrentBinary() 35 | if (cliBinary == null) { 36 | void vscode.window.showWarningMessage('Cannot get flow-cli version, using local schema instead. Please install flow-cli to get the latest schema.') 37 | return await this.getLocalSchema() 38 | } 39 | 40 | const version = cliBinary.version.format() 41 | if (this.#schemaCache[version] == null) { 42 | // Try to get schema from flow-cli repo based on the flow-cli version 43 | this.#schemaCache[version] = fetch(GET_FLOW_SCHEMA_URL(version)).then(async (response: Response) => { 44 | if (!response.ok) { 45 | throw new Error(`Failed to fetch schema for flow-cli version ${version}`) 46 | } 47 | return await response.text() 48 | }).catch(async () => { 49 | void vscode.window.showWarningMessage('Failed to fetch flow.json schema from flow-cli repo, using local schema instead. Please update flow-cli to the latest version to get the latest schema.') 50 | return await this.getLocalSchema() 51 | }) 52 | } 53 | 54 | return await this.#schemaCache[version] 55 | } 56 | 57 | async getLocalSchema (): Promise { 58 | const schemaUrl = resolve(this.#extensionPath, 'flow-schema.json') 59 | return await promisify(readFile)(schemaUrl).then(x => x.toString()) 60 | } 61 | 62 | async readFile (uri: vscode.Uri): Promise { 63 | if (uri.path === '/flow.json') { 64 | const schema = await this.#getFlowSchema() 65 | return Buffer.from(schema) 66 | } else { 67 | throw new Error('Unknown schema') 68 | } 69 | } 70 | 71 | async stat (uri: vscode.Uri): Promise { 72 | if (uri.path === '/flow.json') { 73 | // Mocked values 74 | return { 75 | type: vscode.FileType.File, 76 | ctime: 0, 77 | mtime: 0, 78 | size: await this.#getFlowSchema().then(x => x.length) 79 | } 80 | } else { 81 | throw new Error('Unknown schema') 82 | } 83 | } 84 | 85 | dispose (): void { 86 | if (this.#contentProviderDisposable != null) { 87 | this.#contentProviderDisposable.dispose() 88 | } 89 | } 90 | 91 | // Unsupported file system provider methods 92 | // These methods are required to implement the vscode.FileSystemProvider interface 93 | onDidChangeFile: vscode.Event = new vscode.EventEmitter().event 94 | watch (uri: vscode.Uri, options: { readonly recursive: boolean, readonly excludes: readonly string[] }): vscode.Disposable { 95 | throw new Error('Method not implemented.') 96 | } 97 | 98 | readDirectory (uri: vscode.Uri): Array<[string, vscode.FileType]> | Thenable> { 99 | throw new Error('Method not implemented.') 100 | } 101 | 102 | createDirectory (uri: vscode.Uri): void | Thenable { 103 | throw new Error('Method not implemented.') 104 | } 105 | 106 | writeFile (uri: vscode.Uri, content: Uint8Array, options: { readonly create: boolean, readonly overwrite: boolean }): void | Thenable { 107 | throw new Error('Method not implemented.') 108 | } 109 | 110 | delete (uri: vscode.Uri, options: { readonly recursive: boolean }): void | Thenable { 111 | throw new Error('Method not implemented.') 112 | } 113 | 114 | rename (oldUri: vscode.Uri, newUri: vscode.Uri, options: { readonly overwrite: boolean }): void | Thenable { 115 | throw new Error('Method not implemented.') 116 | } 117 | 118 | copy? (source: vscode.Uri, destination: vscode.Uri, options: { readonly overwrite: boolean }): void | Thenable { 119 | throw new Error('Method not implemented.') 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /extension/src/main.ts: -------------------------------------------------------------------------------- 1 | /* VS Code Cadence Extension entry point */ 2 | import { ExtensionContext, debug, DebugAdapterServer } from 'vscode' 3 | import { Extension } from './extension' 4 | import * as Telemetry from './telemetry/telemetry' 5 | import { Settings } from './settings/settings' 6 | 7 | // Global extension variable to update UI 8 | export let ext: Extension | null = null 9 | 10 | // Called by VS Code when the extension starts up 11 | export async function activate (ctx: ExtensionContext): Promise { 12 | await Telemetry.initialize(ctx) 13 | 14 | debug.registerDebugAdapterDescriptorFactory('cadence', { 15 | createDebugAdapterDescriptor: (_session) => { 16 | return new DebugAdapterServer(2345) 17 | } 18 | }) 19 | 20 | // Initialize the extension 21 | Telemetry.withTelemetry(() => { 22 | const settings = new Settings() 23 | ext = Extension.initialize(settings, ctx) 24 | }) 25 | 26 | return ext 27 | } 28 | 29 | // Called by VS Code when the extension terminates 30 | export function deactivate (): Thenable | undefined { 31 | void Telemetry.deactivate() 32 | return (ext === undefined ? undefined : ext?.deactivate()) 33 | } 34 | -------------------------------------------------------------------------------- /extension/src/server/flow-config.ts: -------------------------------------------------------------------------------- 1 | /* Handle flow.json config file */ 2 | import { window, workspace, Uri, FileSystemWatcher } from 'vscode' 3 | import { Settings } from '../settings/settings' 4 | import * as os from 'os' 5 | import * as fs from 'fs' 6 | import * as path from 'path' 7 | import { Disposable } from 'vscode-languageclient' 8 | import { tryExecDefault } from '../utils/shell/exec' 9 | import { BehaviorSubject, Observable, Subject, Subscription, connectable, distinctUntilChanged, map, Connectable } from 'rxjs' 10 | import { findFilesInAnyWorkspace, pathsAreEqual } from '../utils/utils' 11 | 12 | export interface FlowConfigFile { 13 | path: string | null 14 | isCustom: boolean 15 | exists: boolean 16 | } 17 | 18 | export class FlowConfig implements Disposable { 19 | #configPath$ = new BehaviorSubject({ 20 | path: null, 21 | isCustom: false, 22 | exists: false 23 | }) 24 | 25 | #fileModified$ = new Subject() 26 | #pathChanged$: Connectable 27 | 28 | #pathChangedConnection$: Subscription | null = null 29 | #workspaceSettingsSubscriber: Subscription | null = null 30 | #configChangeWatcher: Disposable | null = null 31 | #workspaceFolderWatcher: Disposable | null = null 32 | 33 | #settings: Settings 34 | 35 | constructor (settings: Settings) { 36 | this.#settings = settings 37 | 38 | this.#pathChanged$ = connectable(this.#configPath$.pipe( 39 | map(({ path, exists }) => (path != null && exists) ? path : null), 40 | distinctUntilChanged(), 41 | map(() => {}) 42 | )) 43 | this.#pathChangedConnection$ = this.#pathChanged$.connect() 44 | } 45 | 46 | async activate (): Promise { 47 | // Load initial config path 48 | await this.reloadConfigPath() 49 | 50 | // Watch for config changes 51 | this.#configChangeWatcher = this.#watchForConfigChanges() 52 | 53 | // Watch for workspace settings changes 54 | this.#workspaceSettingsSubscriber = this.#watchWorkspaceConfiguration() 55 | 56 | // Watch for workspace folder changes (may affect config path) 57 | this.#workspaceFolderWatcher = workspace.onDidChangeWorkspaceFolders(() => { 58 | void this.reloadConfigPath() 59 | }) 60 | } 61 | 62 | get configPath (): string | null { 63 | const { path, exists } = this.#configPath$.value 64 | return path != null && exists ? path : null 65 | } 66 | 67 | get pathChanged$ (): Observable { 68 | return this.#pathChanged$ 69 | } 70 | 71 | get fileModified$ (): Observable { 72 | return this.#fileModified$.asObservable() 73 | } 74 | 75 | dispose (): void { 76 | this.#pathChangedConnection$?.unsubscribe() 77 | this.#workspaceSettingsSubscriber?.unsubscribe() 78 | this.#configChangeWatcher?.dispose() 79 | this.#workspaceFolderWatcher?.dispose() 80 | this.#configPath$.complete() 81 | } 82 | 83 | async reloadConfigPath (): Promise { 84 | const configPath = this.#resolveCustomConfigPath() ?? await this.#resolveDefaultConfigPath() 85 | this.#configPath$.next(configPath ?? { path: null, isCustom: false, exists: false }) 86 | } 87 | 88 | // Search for config file in workspace 89 | async #resolveDefaultConfigPath (): Promise { 90 | // Default config search for flow.json in workspace 91 | const files = findFilesInAnyWorkspace('./flow.json') 92 | if (files.length === 0) { 93 | // Couldn't find config file, prompt user 94 | void this.promptInitializeConfig() 95 | } else if (files.length > 1) { 96 | void window.showErrorMessage(`Multiple flow.json files found: ${files.join(', ')}. Please specify an absolute path to the desired flow.json file in your workspace settings.`) 97 | } else { 98 | return { path: files[0], isCustom: false, exists: true } 99 | } 100 | 101 | return null 102 | } 103 | 104 | #resolveCustomConfigPath (): FlowConfigFile | null { 105 | const customConfigPath = this.#settings.getSettings().customConfigPath 106 | if (customConfigPath === null || customConfigPath === '') { 107 | return null 108 | } 109 | 110 | let resolvedPath: string 111 | const fileNotFoundMessage = `File specified at ${customConfigPath} not found. Please verify the file exists.` 112 | 113 | if (customConfigPath[0] === '~') { 114 | resolvedPath = path.join( 115 | os.homedir(), 116 | customConfigPath.slice(1) 117 | ) 118 | } else if (path.isAbsolute(customConfigPath)) { 119 | resolvedPath = customConfigPath 120 | } else if (workspace.workspaceFolders != null) { 121 | // Find all files matching relative path in workspace 122 | const files = findFilesInAnyWorkspace(customConfigPath) 123 | 124 | // Check that only one file was found (could be in multiple workspaces) 125 | if (files.length === 1) { 126 | resolvedPath = files[0] 127 | } else if (files.length === 0) { 128 | void window.showErrorMessage(fileNotFoundMessage) 129 | return { path: customConfigPath, isCustom: true, exists: false } 130 | } else { 131 | void window.showErrorMessage(`Multiple flow.json files found: ${files.join(', ')}. Please specify an absolute path to the desired flow.json file in your workspace settings.`) 132 | return { path: customConfigPath, isCustom: true, exists: false } 133 | } 134 | } else { 135 | return null 136 | } 137 | 138 | // Verify that the path exists if it was resolved 139 | if (!fs.existsSync(resolvedPath)) { 140 | void window.showErrorMessage(fileNotFoundMessage) 141 | return { path: customConfigPath, isCustom: true, exists: false } 142 | } 143 | 144 | return { path: resolvedPath, isCustom: true, exists: true } 145 | } 146 | 147 | // Prompt the user to create a new config file 148 | async promptInitializeConfig (): Promise { 149 | const rootPath = workspace.workspaceFolders?.[0]?.uri?.fsPath 150 | 151 | if (rootPath == null) { 152 | void window.showErrorMessage('No workspace folder found. Please open a workspace folder and try again.') 153 | } 154 | 155 | const continueMessage = 'Continue' 156 | const selection = await window.showInformationMessage( 157 | 'Missing Flow CLI configuration. Create a new one?', 158 | continueMessage 159 | ) 160 | if (selection !== continueMessage) { 161 | return 162 | } 163 | 164 | const didInit = await tryExecDefault('flow', ['init', '--config-only'], { cwd: rootPath }) 165 | 166 | if (!didInit) { 167 | void window.showErrorMessage('Failed to initialize Flow CLI configuration.') 168 | } else { 169 | void window.showInformationMessage('Flow CLI configuration created.') 170 | } 171 | } 172 | 173 | // Watch and reload flow configuration when changed. 174 | #watchWorkspaceConfiguration (): Subscription { 175 | return this.#settings.watch$(config => config.customConfigPath).subscribe(() => { 176 | void this.reloadConfigPath() 177 | }) 178 | } 179 | 180 | #watchForConfigChanges (): Disposable { 181 | let configWatcher: FileSystemWatcher 182 | 183 | // Recursively bind watcher every time config path changes 184 | const bindWatcher = (): void => { 185 | configWatcher?.dispose() 186 | 187 | // If custom config path is set, watch that file 188 | // Otherwise watch for flow.json in workspace 189 | const relativeWatchPath = this.#configPath$.value.isCustom && this.#configPath$.value.path != null ? this.#configPath$.value.path : './flow.json' 190 | const watchPaths = new Set(workspace.workspaceFolders?.map(folder => path.resolve(folder.uri.fsPath, relativeWatchPath)) ?? []) 191 | 192 | watchPaths.forEach(watchPath => { 193 | // Watch for changes to config file 194 | // If it does not exist, wait for flow.json to be created 195 | configWatcher = workspace.createFileSystemWatcher(watchPath) 196 | 197 | const configPathChangeHandler = (): void => { 198 | void this.reloadConfigPath() 199 | } 200 | const configModifyHandler = (file: Uri): void => { 201 | if (this.configPath != null && pathsAreEqual(file.fsPath, this.configPath)) { 202 | this.#fileModified$.next() 203 | } 204 | } 205 | 206 | configWatcher.onDidCreate(configPathChangeHandler) 207 | configWatcher.onDidDelete(configPathChangeHandler) 208 | configWatcher.onDidChange(configModifyHandler) 209 | }) 210 | } 211 | 212 | // Bind initial watcher 213 | bindWatcher() 214 | 215 | // If config path changes, dispose of current watcher and bind a new one to bind to new path 216 | const configSubscription = this.pathChanged$.subscribe(() => { 217 | configWatcher.dispose() 218 | bindWatcher() 219 | }) 220 | 221 | return { 222 | dispose: () => { 223 | configWatcher.dispose() 224 | configSubscription.unsubscribe() 225 | } 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /extension/src/server/language-server.ts: -------------------------------------------------------------------------------- 1 | import { LanguageClient, State } from 'vscode-languageclient/node' 2 | import { window } from 'vscode' 3 | import { Settings } from '../settings/settings' 4 | import { exec } from 'child_process' 5 | import { ExecuteCommandRequest } from 'vscode-languageclient' 6 | import { BehaviorSubject, Subscription, filter, firstValueFrom, skip } from 'rxjs' 7 | import { envVars } from '../utils/shell/env-vars' 8 | import { FlowConfig } from './flow-config' 9 | import { CliProvider } from '../flow-cli/cli-provider' 10 | import { KNOWN_FLOW_COMMANDS } from '../flow-cli/cli-versions-provider' 11 | 12 | // Identities for commands handled by the Language server 13 | const RELOAD_CONFIGURATION = 'cadence.server.flow.reloadConfiguration' 14 | 15 | export class LanguageServerAPI { 16 | #settings: Settings 17 | #config: FlowConfig 18 | #cliProvider: CliProvider 19 | client: LanguageClient | null = null 20 | 21 | clientState$ = new BehaviorSubject(State.Stopped) 22 | #subscriptions: Subscription[] = [] 23 | 24 | #isActive = false 25 | 26 | constructor (settings: Settings, cliProvider: CliProvider, config: FlowConfig) { 27 | this.#settings = settings 28 | this.#cliProvider = cliProvider 29 | this.#config = config 30 | } 31 | 32 | // Activates the language server manager 33 | // This will control the lifecycle of the language server 34 | // & restart it when necessary 35 | async activate (): Promise { 36 | if (this.isActive) return 37 | await this.deactivate() 38 | 39 | this.#isActive = true 40 | 41 | this.#subscribeToConfigChanges() 42 | this.#subscribeToSettingsChanges() 43 | this.#subscribeToBinaryChanges() 44 | 45 | // Report error, but an error starting is non-terminal 46 | // The server will be restarted if conditions change which make it possible 47 | // (e.g. a new binary is selected, or the config file is created) 48 | await this.startClient().catch((e) => { 49 | console.error(e) 50 | }) 51 | } 52 | 53 | async deactivate (): Promise { 54 | this.#isActive = false 55 | this.#subscriptions.forEach((sub) => sub.unsubscribe()) 56 | await this.stopClient() 57 | } 58 | 59 | get isActive (): boolean { 60 | return this.#isActive 61 | } 62 | 63 | async startClient (): Promise { 64 | try { 65 | // Prevent starting multiple times 66 | if (this.clientState$.getValue() === State.Starting) { 67 | const newState = await firstValueFrom(this.clientState$.pipe(filter(state => state !== State.Starting))) 68 | if (newState === State.Running) { return } 69 | } else if (this.clientState$.getValue() === State.Running) { 70 | return 71 | } 72 | 73 | // Set client state to starting 74 | this.clientState$.next(State.Starting) 75 | 76 | const accessCheckMode: string = this.#settings.getSettings().accessCheckMode 77 | const configPath: string | null = this.#config.configPath 78 | 79 | const binaryPath = (await this.#cliProvider.getCurrentBinary())?.command 80 | if (binaryPath == null) { 81 | throw new Error('No flow binary found') 82 | } 83 | 84 | if (binaryPath !== KNOWN_FLOW_COMMANDS.DEFAULT) { 85 | try { 86 | exec('killall dlv') // Required when running language server locally on mac 87 | } catch (err) { void err } 88 | } 89 | 90 | const env = await envVars.getValue() 91 | this.client = new LanguageClient( 92 | 'cadence', 93 | 'Cadence', 94 | { 95 | command: binaryPath, 96 | args: ['cadence', 'language-server', '--enable-flow-client=false'], 97 | options: { 98 | env 99 | } 100 | }, 101 | { 102 | documentSelector: [{ scheme: 'file', language: 'cadence' }], 103 | synchronize: { 104 | configurationSection: 'cadence' 105 | }, 106 | initializationOptions: { 107 | configPath, 108 | accessCheckMode 109 | } 110 | } 111 | ) 112 | 113 | this.client.onDidChangeState((event) => { 114 | this.clientState$.next(event.newState) 115 | }) 116 | 117 | await this.client.start() 118 | .catch((err: Error) => { 119 | void window.showErrorMessage(`Cadence language server failed to start: ${err.message}`) 120 | }) 121 | } catch (e) { 122 | await this.stopClient() 123 | throw e 124 | } 125 | } 126 | 127 | async stopClient (): Promise { 128 | // Set emulator state to disconnected 129 | this.clientState$.next(State.Stopped) 130 | 131 | await this.client?.stop() 132 | await this.client?.dispose() 133 | this.client = null 134 | } 135 | 136 | async restart (): Promise { 137 | await this.stopClient() 138 | await this.startClient() 139 | } 140 | 141 | #subscribeToConfigChanges (): void { 142 | const tryReloadConfig = (): void => { 143 | void this.#sendRequest(RELOAD_CONFIGURATION).catch((e: any) => { 144 | void window.showErrorMessage(`Failed to reload configuration: ${String(e)}`) 145 | }) 146 | } 147 | 148 | this.#subscriptions.push(this.#config.fileModified$.subscribe(function notify (this: LanguageServerAPI): void { 149 | // Reload configuration 150 | if (this.clientState$.getValue() === State.Running) { 151 | tryReloadConfig() 152 | } else if (this.clientState$.getValue() === State.Starting) { 153 | // Wait for client to connect 154 | void firstValueFrom(this.clientState$.pipe(filter((state) => state === State.Running))).then(() => { 155 | notify.call(this) 156 | }) 157 | } else { 158 | // Start client 159 | void this.startClient() 160 | } 161 | }.bind(this))) 162 | 163 | this.#subscriptions.push(this.#config.pathChanged$.subscribe(() => { 164 | // Restart client 165 | void this.restart() 166 | })) 167 | } 168 | 169 | #subscribeToSettingsChanges (): void { 170 | // Subscribe to changes in the flowCommand setting to restart the client 171 | // Skip the first value since we don't want to restart the client when it's first initialized 172 | this.#settings.watch$((config) => config.flowCommand).pipe(skip(1)).subscribe(() => { 173 | // Restart client 174 | void this.restart() 175 | }) 176 | } 177 | 178 | #subscribeToBinaryChanges (): void { 179 | // Subscribe to changes in the selected binary to restart the client 180 | // Skip the first value since we don't want to restart the client when it's first initialized 181 | const subscription = this.#cliProvider.currentBinary$.pipe(skip(1)).subscribe(() => { 182 | // Restart client 183 | void this.restart() 184 | }) 185 | this.#subscriptions.push(subscription) 186 | } 187 | 188 | async #sendRequest (cmd: string, args: any[] = []): Promise { 189 | return await this.client?.sendRequest(ExecuteCommandRequest.type, { 190 | command: cmd, 191 | arguments: args 192 | }) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /extension/src/settings/settings.ts: -------------------------------------------------------------------------------- 1 | /* Workspace Settings */ 2 | import { BehaviorSubject, Observable, distinctUntilChanged, map } from 'rxjs' 3 | import { workspace, Disposable, ConfigurationTarget } from 'vscode' 4 | import { isEqual } from 'lodash' 5 | 6 | const CONFIGURATION_KEY = 'cadence' 7 | 8 | // Schema for the cadence configuration 9 | export interface CadenceConfiguration { 10 | flowCommand: string 11 | accessCheckMode: string 12 | customConfigPath: string 13 | test: { 14 | maxConcurrency: number 15 | } 16 | } 17 | 18 | export class Settings implements Disposable { 19 | #configuration$: BehaviorSubject = new BehaviorSubject(this.#getConfiguration()) 20 | #disposables: Disposable[] = [] 21 | 22 | constructor () { 23 | // Watch for configuration changes 24 | this.#disposables.push(workspace.onDidChangeConfiguration((e) => { 25 | if (e.affectsConfiguration(CONFIGURATION_KEY)) { 26 | this.#configuration$.next(this.#getConfiguration()) 27 | } 28 | })) 29 | } 30 | 31 | /** 32 | * Returns an observable that emits whenever the configuration changes. If a selector is provided, the observable 33 | * will only emit when the selected value changes. 34 | * 35 | * @param selector A function that selects a value from the configuration 36 | * @returns An observable that emits whenever the configuration changes 37 | * @template T The type of the selected value 38 | * @example 39 | * // Emit whenever the flow command changes 40 | * settings.watch$(config => config.flowCommand) 41 | */ 42 | watch$ (selector: (config: CadenceConfiguration) => T = (config) => config as unknown as T): Observable { 43 | return this.#configuration$.pipe( 44 | map(selector), 45 | distinctUntilChanged(isEqual) 46 | ) 47 | } 48 | 49 | /** 50 | * Get the current configuration 51 | * @returns The current configuration 52 | */ 53 | getSettings (): CadenceConfiguration { 54 | return this.#configuration$.value 55 | } 56 | 57 | async updateSettings (config: Partial, target?: ConfigurationTarget): Promise { 58 | // Recursively update all keys in the configuration 59 | async function update (section: string, obj: any): Promise { 60 | await Promise.all(Object.entries(obj).map(async ([key, value]) => { 61 | const newKey = `${section}.${key}` 62 | if (typeof value === 'object' && !Array.isArray(value)) { 63 | await update(newKey, value) 64 | } else { 65 | await workspace.getConfiguration().update(newKey, value, target) 66 | } 67 | })) 68 | } 69 | 70 | await update(CONFIGURATION_KEY, config) 71 | } 72 | 73 | dispose (): void { 74 | this.#configuration$.complete() 75 | } 76 | 77 | #getConfiguration (): CadenceConfiguration { 78 | return workspace.getConfiguration(CONFIGURATION_KEY) as unknown as CadenceConfiguration 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /extension/src/storage/storage-provider.ts: -------------------------------------------------------------------------------- 1 | import { Memento } from 'vscode' 2 | 3 | interface State { 4 | dismissedNotifications: string[] 5 | } 6 | 7 | export class StorageProvider { 8 | #globalState: Memento 9 | 10 | constructor (globalState: Memento) { 11 | this.#globalState = globalState 12 | } 13 | 14 | get(key: T, fallback: State[T]): State[T] { 15 | return this.#globalState.get(key, fallback) 16 | } 17 | 18 | async set(key: T, value: State[T]): Promise { 19 | return await (this.#globalState.update(key, value) as Promise) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /extension/src/telemetry/mixpanel-wrapper.ts: -------------------------------------------------------------------------------- 1 | /* Wrapper functions for Mixpanel analytics */ 2 | import * as mixpanel from 'mixpanel' 3 | 4 | // Mixpanel vscode-cadence 5 | const MIXPANEL_TOKEN: string = '69e592a84fef909bee58668b5c764ae4' 6 | const DEV_FUNNEL_TOKEN: string = '776159d170484f49f19c3c2f7339f297' 7 | 8 | export enum MixpanelSelector { 9 | VSCODE = 0, 10 | DEV_FUNNEL = 1 11 | } 12 | 13 | // True when mixpanel telemetry is active 14 | let mixpanelActivated: boolean = false 15 | 16 | // Mixpanel instance 17 | let mixPanel: mixpanel.Mixpanel 18 | 19 | // Dev funnel Mixpanel 20 | let devFunnel: mixpanel.Mixpanel 21 | 22 | // User information 23 | let userInfo: { 24 | vscode_cadence_version: string 25 | distinct_id: string 26 | operating_system: string 27 | } 28 | 29 | // Events to capture 30 | export enum Events { 31 | ExtensionActivated = 'Extension Activated', 32 | UnhandledException = 'Unhandled Exception', 33 | PlaygroundProjectOpened = 'Playground Project Opened', 34 | PlaygroundProjectDeployed = 'Playground Project Deployed' 35 | } 36 | 37 | export async function mixpanelInit (activate: boolean, uid: string, version: string): Promise { 38 | const osName = await import('os-name') 39 | mixpanelActivated = activate 40 | if (!mixpanelActivated) return 41 | 42 | mixPanel = mixpanel.init(MIXPANEL_TOKEN) 43 | devFunnel = mixpanel.init(DEV_FUNNEL_TOKEN) 44 | 45 | userInfo = { 46 | vscode_cadence_version: version, 47 | distinct_id: uid, 48 | operating_system: osName.default() 49 | } 50 | } 51 | 52 | export function captureException (err: any): void { 53 | if (!mixpanelActivated) return 54 | const errProperties = Object.getOwnPropertyNames(err) 55 | const mixpanelProperties: mixpanel.PropertyDict = {} 56 | 57 | // Extract properties from the error 58 | errProperties.forEach((elem) => { 59 | type ObjectKey = keyof typeof err 60 | mixpanelProperties[elem] = err[elem as ObjectKey] 61 | }) 62 | 63 | captureEvent(MixpanelSelector.VSCODE, Events.UnhandledException, mixpanelProperties) 64 | } 65 | 66 | export function captureEvent (selector: MixpanelSelector, eventName: string, properties: mixpanel.PropertyDict = {}): void { 67 | if (!mixpanelActivated) return 68 | 69 | // Add user information 70 | properties.vscode_cadence_version = userInfo.vscode_cadence_version 71 | properties.distinct_id = userInfo.distinct_id 72 | properties.$os = userInfo.operating_system 73 | 74 | // Track event data 75 | switch (selector) { 76 | case MixpanelSelector.VSCODE: 77 | mixPanel.track(eventName, properties) 78 | break 79 | case MixpanelSelector.DEV_FUNNEL: 80 | devFunnel.track(eventName, properties) 81 | break 82 | default: 83 | console.log('Unknown mixpanel selector type') 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /extension/src/telemetry/playground.ts: -------------------------------------------------------------------------------- 1 | /* Detect exported Flow Playground projects in order to track the developer funnel: 2 | 1. Conversion of Playground projects to VSCode 3 | 2. Deployment of Playground projects to emulator 4 | */ 5 | import { workspace } from 'vscode' 6 | import * as fs from 'fs' 7 | import * as objHash from 'object-hash' 8 | 9 | // State of projects converted from Flow Playground 10 | export enum ProjectState { 11 | OPENED = 'OPENED', 12 | DEPLOYED = 'DEPLOYED' 13 | } 14 | 15 | interface PlaygroundProject { 16 | id: string 17 | updatedAt: string 18 | } 19 | 20 | let projectHash: string | null = null 21 | 22 | export async function getPlaygroundProjectHash (): Promise { 23 | if (projectHash === null) { 24 | // Search for Playground file in workspace 25 | const file = await workspace.findFiles('.vscode/*.play') 26 | if (file.length !== 1) { 27 | return null 28 | } 29 | 30 | const proj: PlaygroundProject = JSON.parse(fs.readFileSync(file[0].fsPath).toString()) 31 | projectHash = 'Playground:' + String(objHash.sha1(proj)) 32 | } 33 | 34 | return projectHash 35 | } 36 | -------------------------------------------------------------------------------- /extension/src/telemetry/sentry-wrapper.ts: -------------------------------------------------------------------------------- 1 | /* Wrapper functions for error reporting using Sentry */ 2 | import * as Sentry from '@sentry/node' 3 | import * as Type from '@sentry/types' 4 | 5 | // Sentry vscode-cadence 6 | const SENTRY_DSN: string = 'https://4d98c4d4ac7e4892850f8e3d2e61c733@o114654.ingest.sentry.io/6568410' 7 | 8 | // True when sentry telemetry is active 9 | let sentryActivated: boolean = false 10 | 11 | export async function sentryInit (activate: boolean, uid: string, version: string): Promise { 12 | sentryActivated = activate 13 | if (!sentryActivated) return 14 | 15 | // Initialize Sentry 16 | Sentry.init({ 17 | dsn: SENTRY_DSN, 18 | tracesSampleRate: 1.0, 19 | attachStacktrace: true, 20 | defaultIntegrations: false 21 | }) 22 | 23 | // Set user information 24 | Sentry.setUser({ id: uid, vscode_cadence_version: version }) 25 | } 26 | 27 | export async function sentryClose (): Promise { 28 | if (!sentryActivated) return 29 | void await Sentry.close() 30 | } 31 | 32 | export function captureException (exception: any, captureContent?: Type.CaptureContext | undefined): void { 33 | if (!sentryActivated) return 34 | Sentry.captureException(exception, captureContent) 35 | } 36 | 37 | export function captureStatistics (message: string): void { 38 | if (!sentryActivated) return 39 | Sentry.captureMessage(message, 'info') 40 | } 41 | -------------------------------------------------------------------------------- /extension/src/telemetry/telemetry.ts: -------------------------------------------------------------------------------- 1 | /* Telemetry functions */ 2 | import * as sentry from './sentry-wrapper' 3 | import * as mixpanel from './mixpanel-wrapper' 4 | import { env, ExtensionContext } from 'vscode' 5 | import * as pkg from '../../../package.json' 6 | import * as uuid from 'uuid' 7 | import * as playground from './playground' 8 | 9 | let extensionContext: ExtensionContext 10 | 11 | export async function getUID (): Promise { 12 | let uid: string | undefined = extensionContext.globalState.get('uid') 13 | if (uid === undefined) { 14 | // Generate new uid and add it to global state 15 | uid = uuid.v4() 16 | await extensionContext.globalState.update('uid', uid) 17 | } 18 | return uid 19 | } 20 | 21 | // Called in main to setup telemetry 22 | export async function initialize (ctx: ExtensionContext): Promise { 23 | extensionContext = ctx 24 | 25 | // Check if user is allowing telemetry for vscode globally 26 | const telemetryEnabled: boolean = env.isTelemetryEnabled 27 | 28 | // Get unique UID 29 | const uid = await getUID() 30 | 31 | // Initialize Sentry 32 | await sentry.sentryInit(telemetryEnabled, uid, pkg.version) 33 | 34 | // Initialize Mixpanel 35 | await mixpanel.mixpanelInit(telemetryEnabled, uid, pkg.version) 36 | 37 | // Send initial statistics 38 | sendActivationStatistics() 39 | 40 | // Check if project was exported from Flow Playground 41 | const projectHash = await playground.getPlaygroundProjectHash() 42 | if (projectHash !== null) { 43 | void sendPlaygroundProjectOpened(projectHash) 44 | } 45 | } 46 | 47 | // Called in main to deactivate telemetry 48 | export async function deactivate (): Promise { 49 | await sentry.sentryClose() 50 | } 51 | 52 | function sendActivationStatistics (): void { 53 | mixpanel.captureEvent(mixpanel.MixpanelSelector.VSCODE, mixpanel.Events.ExtensionActivated) 54 | } 55 | 56 | // Wrap a function call with telemetry 57 | export function withTelemetry (callback: () => R): R { 58 | try { 59 | const returnValue = callback() 60 | if (returnValue instanceof Promise) { 61 | return returnValue.then((value) => { 62 | return value 63 | }).catch((err) => { 64 | sentry.captureException(err) 65 | mixpanel.captureException(err) 66 | throw err 67 | }) as R 68 | } else { 69 | return returnValue 70 | } 71 | } catch (err) { 72 | sentry.captureException(err) 73 | mixpanel.captureException(err) 74 | throw err 75 | } 76 | } 77 | 78 | export async function emulatorConnected (): Promise { 79 | const projectHash = await playground.getPlaygroundProjectHash() 80 | if (projectHash !== null) { 81 | void sendPlaygroundProjectDeployed(projectHash) 82 | } 83 | } 84 | 85 | async function sendPlaygroundProjectOpened (projectHash: string): Promise { 86 | const projectState: string | undefined = extensionContext.globalState.get(projectHash) 87 | if (projectState !== undefined) { 88 | // Project was already reported 89 | return 90 | } 91 | await extensionContext.globalState.update(projectHash, playground.ProjectState.OPENED) 92 | mixpanel.captureEvent( 93 | mixpanel.MixpanelSelector.DEV_FUNNEL, 94 | mixpanel.Events.PlaygroundProjectOpened) 95 | } 96 | 97 | async function sendPlaygroundProjectDeployed (projectHash: string): Promise { 98 | const projectState: string | undefined = extensionContext.globalState.get(projectHash) 99 | if (projectState === playground.ProjectState.DEPLOYED) { 100 | // Project deployment was already reported 101 | return 102 | } 103 | await extensionContext.globalState.update(projectHash, playground.ProjectState.DEPLOYED) 104 | mixpanel.captureEvent( 105 | mixpanel.MixpanelSelector.DEV_FUNNEL, 106 | mixpanel.Events.PlaygroundProjectDeployed) 107 | } 108 | -------------------------------------------------------------------------------- /extension/src/test-provider/constants.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | 3 | export const CADENCE_TEST_TAG = new vscode.TestTag('cadence') 4 | export const TEST_FUNCTION_PREFIX = ':' 5 | -------------------------------------------------------------------------------- /extension/src/test-provider/test-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { TestResolver } from './test-resolver' 3 | import { TestRunner } from './test-runner' 4 | import { Settings } from '../settings/settings' 5 | import { FlowConfig } from '../server/flow-config' 6 | import { QueuedMutator, TestTrie } from './test-trie' 7 | 8 | const testControllerId = 'cadence-test-controller' 9 | const testControllerLabel = 'Cadence Tests' 10 | 11 | export class TestProvider implements vscode.Disposable { 12 | #controller: vscode.TestController 13 | #testResolver: TestResolver 14 | #testRunner: TestRunner 15 | #testTrie: QueuedMutator 16 | 17 | constructor (parserLocation: string, settings: Settings, flowConfig: FlowConfig) { 18 | this.#controller = vscode.tests.createTestController(testControllerId, testControllerLabel) 19 | this.#testTrie = new QueuedMutator(new TestTrie(this.#controller), recoverTrieError.bind(this)) 20 | this.#testResolver = new TestResolver(parserLocation, this.#controller, this.#testTrie) 21 | this.#testRunner = new TestRunner(this.#controller, this.#testTrie, settings, flowConfig, this.#testResolver) 22 | 23 | // Recover from trie errors by rebuilding the test tree from scratch 24 | // It shouldn't happen, but if it does, this should catch tricky bugs 25 | // And leave the user with a seemingly normal experience 26 | function recoverTrieError (this: TestProvider, _: Error, abortMutations: () => void): void { 27 | abortMutations() 28 | void this.#testResolver.loadAllTests() 29 | } 30 | } 31 | 32 | dispose (): void { 33 | this.#controller.dispose() 34 | this.#testResolver.dispose() 35 | this.#testRunner.dispose() 36 | } 37 | 38 | async runAllTests (cancellationToken?: vscode.CancellationToken, hookTestRun?: (testRun: vscode.TestRun) => vscode.TestRun): Promise { 39 | const trie = await this.#testTrie.getState() 40 | 41 | const request = new vscode.TestRunRequest(trie.rootNodes) 42 | return await this.#testRunner.runTests(request, cancellationToken, hookTestRun) 43 | } 44 | 45 | async runIndividualTest (testPath: string, cancellationToken?: vscode.CancellationToken, hookTestRun?: (testRun: vscode.TestRun) => vscode.TestRun): Promise { 46 | const test = (await this.#testTrie.getState()).get(testPath) 47 | if (test == null) { 48 | return 49 | } 50 | 51 | const request = new vscode.TestRunRequest([test]) 52 | return await this.#testRunner.runTests(request, cancellationToken, hookTestRun) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /extension/src/test-provider/test-resolver.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import * as CadenceParser from '@onflow/cadence-parser' 3 | import { QueuedMutator, TestFunction, TestTrie } from './test-trie' 4 | import { isDirectory } from '../utils/utils' 5 | 6 | interface Ast { 7 | program: { 8 | Declarations: Declaration[] 9 | } 10 | } 11 | 12 | interface Declaration { 13 | Type: string 14 | Identifier: { 15 | Identifier: string 16 | } 17 | StartPos: { 18 | Line: number 19 | Column: number 20 | } 21 | EndPos: { 22 | Line: number 23 | Column: number 24 | } 25 | } 26 | 27 | export class TestResolver implements vscode.Disposable { 28 | #controller: vscode.TestController 29 | #parser: Thenable 30 | #testTrie: QueuedMutator 31 | #disposables: vscode.Disposable[] = [] 32 | 33 | constructor (parserLocation: string, controller: vscode.TestController, testTrie: QueuedMutator) { 34 | this.#controller = controller 35 | this.#parser = vscode.workspace.fs.readFile(vscode.Uri.file(parserLocation)).then(async buffer => await CadenceParser.CadenceParser.create(buffer)) 36 | this.#testTrie = testTrie 37 | 38 | void this.watchFiles() 39 | void this.loadAllTests() 40 | this.#controller.refreshHandler = async (): Promise => { 41 | await this.loadAllTests() 42 | } 43 | } 44 | 45 | dispose (): void { 46 | this.#disposables.forEach((disposable) => disposable.dispose()) 47 | } 48 | 49 | async watchFiles (): Promise { 50 | vscode.workspace.onDidChangeWorkspaceFolders(() => { 51 | void this.loadAllTests() 52 | }) 53 | 54 | const watcher = vscode.workspace.createFileSystemWatcher('**') 55 | 56 | watcher.onDidCreate(async (uri) => { 57 | void this.#testTrie.mutate(async (trie) => { 58 | if (await isDirectory(uri)) { 59 | const files = await vscode.workspace.findFiles(new vscode.RelativePattern(uri.fsPath, '**/*.cdc')) 60 | await Promise.all(files.map(async (file) => { 61 | await this.addTestsFromFile(file.fsPath, trie) 62 | })) 63 | } else if (uri.fsPath.endsWith('.cdc')) { 64 | await this.addTestsFromFile(uri.fsPath, trie) 65 | } 66 | }) 67 | }) 68 | watcher.onDidDelete((uri: vscode.Uri) => { 69 | void this.#testTrie.mutate(async (trie) => { 70 | trie.remove(uri.fsPath) 71 | }) 72 | }) 73 | watcher.onDidChange((uri: vscode.Uri) => { 74 | void this.#testTrie.mutate(async (trie) => { 75 | if (!(await isDirectory(uri))) { 76 | trie.remove(uri.fsPath) 77 | await this.addTestsFromFile(uri.fsPath, trie) 78 | } 79 | }) 80 | }) 81 | 82 | this.#disposables.push(watcher) 83 | } 84 | 85 | async loadAllTests (): Promise { 86 | const trieItemsPromise = (async () => { 87 | // Build test tree 88 | const testFilepaths = (await vscode.workspace.findFiles('**/*.cdc')).map((uri) => uri.fsPath) 89 | const items: Array<[string, TestFunction[]]> = [] 90 | 91 | await Promise.all(testFilepaths.map(async (filepath) => { 92 | const tests = await this.#findTestsFromPath(filepath) 93 | if (tests.length > 0) { 94 | items.push([filepath, tests]) 95 | } 96 | })) 97 | 98 | return items 99 | })() 100 | 101 | await this.#testTrie.mutate(async (trie) => { 102 | const items = await trieItemsPromise 103 | 104 | // Clear test tree 105 | trie.clear() 106 | for (const [filepath, tests] of items) { 107 | trie.add(filepath, tests) 108 | } 109 | }) 110 | } 111 | 112 | async addTestsFromFile (filepath: string, trie: TestTrie): Promise { 113 | const tests = await this.#findTestsFromPath(filepath) 114 | if (tests.length > 0) { 115 | trie.remove(filepath) 116 | trie.add(filepath, tests) 117 | } 118 | } 119 | 120 | async #findTestsFromPath (filepath: string): Promise { 121 | try { 122 | const parser = await this.#parser 123 | const uri = vscode.Uri.file(filepath) 124 | const fileContents = await vscode.workspace.fs.readFile(uri) 125 | const ast: Ast = await parser.parse(fileContents.toString()) 126 | const { program } = ast 127 | const { Declarations } = program 128 | const tests = Declarations.filter((declaration: any) => { 129 | return declaration.Type === 'FunctionDeclaration' && declaration.Identifier.Identifier.startsWith('test') 130 | }) 131 | 132 | const astTests: TestFunction[] = [] 133 | tests.forEach((test: any) => { 134 | const testFunction = this.#declarationToTestFunction(uri, test) 135 | if (testFunction != null) { 136 | astTests.push(testFunction) 137 | } 138 | }) 139 | 140 | return astTests 141 | } catch (e) { 142 | return [] 143 | } 144 | } 145 | 146 | #declarationToTestFunction (uri: vscode.Uri, declaration: Declaration): TestFunction | null { 147 | try { 148 | const { Identifier } = declaration 149 | const testId = Identifier.Identifier 150 | 151 | return { 152 | name: testId, 153 | range: new vscode.Range(declaration.StartPos.Line - 1, declaration.StartPos.Column, declaration.EndPos.Line, declaration.EndPos.Column) 154 | } 155 | } catch { 156 | return null 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /extension/src/test-provider/test-runner.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { CADENCE_TEST_TAG } from './constants' 3 | import { execDefault } from '../utils/shell/exec' 4 | import * as path from 'path' 5 | import { Settings } from '../settings/settings' 6 | import { FlowConfig } from '../server/flow-config' 7 | import { QueuedMutator, TestTrie } from './test-trie' 8 | import { TestResolver } from './test-resolver' 9 | import { decodeTestFunctionId } from './utils' 10 | import { semaphore } from '../utils/semaphore' 11 | 12 | const TEST_RESULT_PASS = 'PASS' 13 | 14 | interface TestResult { 15 | [filename: string]: { 16 | [testName: string]: string 17 | } 18 | } 19 | 20 | export class TestRunner implements vscode.Disposable { 21 | #controller: vscode.TestController 22 | #testTrie: QueuedMutator 23 | #settings: Settings 24 | #flowConfig: FlowConfig 25 | #testResolver: TestResolver 26 | #acquireLock: (fn: () => Promise) => Promise 27 | #disposibles: vscode.Disposable[] = [] 28 | 29 | constructor (controller: vscode.TestController, testTrie: QueuedMutator, settings: Settings, flowConfig: FlowConfig, testResolver: TestResolver) { 30 | this.#controller = controller 31 | this.#testTrie = testTrie 32 | this.#settings = settings 33 | this.#flowConfig = flowConfig 34 | this.#testResolver = testResolver 35 | this.#acquireLock = semaphore(settings.getSettings().test.maxConcurrency) 36 | 37 | this.#disposibles.push(this.#controller.createRunProfile('Cadence Tests', vscode.TestRunProfileKind.Run, this.runTests.bind(this), true, CADENCE_TEST_TAG)) 38 | } 39 | 40 | dispose (): void { 41 | this.#disposibles.forEach((disposable) => disposable.dispose()) 42 | } 43 | 44 | async runTests (request: vscode.TestRunRequest, cancellationToken?: vscode.CancellationToken, hookTestRun: (testRun: vscode.TestRun) => vscode.TestRun = run => run): Promise { 45 | // Flush the test trie to make sure that all tests are up to date 46 | await this.#testTrie.getState() 47 | 48 | // Allow the test run creation to be hooked into for testing purposes 49 | const run = hookTestRun(this.#controller.createTestRun(request)) 50 | 51 | await Promise.all(request.include?.map(async (test) => { 52 | await this.runTestItem(test, run, cancellationToken) 53 | }) ?? []) 54 | 55 | run.end() 56 | } 57 | 58 | private async runTestItem (test: vscode.TestItem, run: vscode.TestRun, cancellationToken?: vscode.CancellationToken): Promise { 59 | if (cancellationToken?.isCancellationRequested === true) { 60 | return 61 | } 62 | 63 | const testFunctionName = decodeTestFunctionId(test.id) 64 | 65 | if (testFunctionName != null) { 66 | await this.runIndividualTest(test, run, cancellationToken) 67 | } else { 68 | if (test.uri == null) { 69 | throw new Error('Test uri is null') 70 | } 71 | const fsStat = await vscode.workspace.fs.stat(test.uri) 72 | if (fsStat.type === vscode.FileType.Directory) { 73 | await this.runTestFolder(test, run, cancellationToken) 74 | } else { 75 | await this.runTestFile(test, run, cancellationToken) 76 | } 77 | } 78 | } 79 | 80 | async runTestFolder (test: vscode.TestItem, run: vscode.TestRun, cancellationToken?: vscode.CancellationToken): Promise { 81 | const promises: Array> = [] 82 | test.children.forEach((child) => { 83 | promises.push(this.runTestItem(child, run, cancellationToken)) 84 | }) 85 | await Promise.all(promises) 86 | } 87 | 88 | async runTestFile (test: vscode.TestItem, run: vscode.TestRun, cancellationToken?: vscode.CancellationToken): Promise { 89 | // Notify that all tests contained within the uri have started 90 | test.children.forEach((testItem) => { 91 | run.started(testItem) 92 | }) 93 | 94 | // If files are dirty they must be saved before running tests 95 | // The trie is updated with the new test items after the file is saved 96 | let resolvedTest: vscode.TestItem | null = test 97 | const openDocument = vscode.workspace.textDocuments.find((document) => test.uri != null && document.uri.fsPath === test.uri.fsPath) 98 | if (openDocument?.isDirty === true) { 99 | await openDocument.save() 100 | await this.#testTrie.mutate(async (trie) => { 101 | if (test.uri == null) { 102 | throw new Error('Test uri is null') 103 | } 104 | await this.#testResolver.addTestsFromFile(test.uri.fsPath, trie) 105 | }) 106 | 107 | if (test.uri == null) { 108 | throw new Error('Test uri is null') 109 | } 110 | const trie = await this.#testTrie.getState() 111 | resolvedTest = trie.get(test.uri.fsPath) 112 | 113 | if (resolvedTest == null) { 114 | throw new Error(`Failed to find test item for ${test.uri.fsPath} in test trie`) 115 | } 116 | 117 | // Make sure that new test items are notified as started 118 | resolvedTest.children.forEach((testItem) => { 119 | run.started(testItem) 120 | }) 121 | } 122 | 123 | // Execute the tests 124 | if (this.#flowConfig.configPath == null) { 125 | throw new Error('Flow config path is null') 126 | } 127 | if (resolvedTest.uri == null) { 128 | throw new Error('Test uri is null') 129 | } 130 | const testFilePath = path.resolve(this.#flowConfig.configPath, resolvedTest.uri.fsPath) 131 | let testResults: TestResult = {} 132 | try { 133 | testResults = await this.#executeTests(testFilePath, cancellationToken) 134 | } catch (error: any) { 135 | resolvedTest.children.forEach((testItem) => { 136 | run.errored(testItem, error) 137 | }) 138 | return 139 | } 140 | 141 | // Notify the results of all tests contained within the uri 142 | resolvedTest.children.forEach((testItem) => { 143 | const testId = decodeTestFunctionId(testItem.id) 144 | let result: string 145 | if (testId == null || testResults[testFilePath]?.[testId] == null) { 146 | result = 'ERROR - Test not found' 147 | } else { 148 | result = testResults[testFilePath][testId] 149 | } 150 | 151 | if (result === TEST_RESULT_PASS) { 152 | run.passed(testItem) 153 | } else { 154 | run.failed(testItem, { 155 | message: result 156 | }) 157 | } 158 | }) 159 | } 160 | 161 | async runIndividualTest (test: vscode.TestItem, run: vscode.TestRun, cancellationToken?: vscode.CancellationToken): Promise { 162 | // Run parent test item to run the individual test 163 | // In the future we may want to run the individual test directly 164 | if (test.parent != null) { 165 | await this.runTestItem(test.parent, run, cancellationToken) 166 | } 167 | } 168 | 169 | async #executeTests (globPattern: string, cancellationToken?: vscode.CancellationToken): Promise { 170 | if (cancellationToken?.isCancellationRequested === true) { 171 | return {} 172 | } 173 | 174 | return await this.#acquireLock(async () => { 175 | if (this.#flowConfig.configPath == null) { 176 | throw new Error('Flow config path is null') 177 | } 178 | const args = ['test', `'${globPattern}'`, '--output=json', '-f', `'${this.#flowConfig.configPath}'`] 179 | const { stdout, stderr } = await execDefault(this.#settings.getSettings().flowCommand, args, { cwd: path.dirname(this.#flowConfig.configPath) }, cancellationToken) 180 | 181 | if (stderr.length > 0) { 182 | throw new Error(stderr) 183 | } 184 | 185 | const testResults = JSON.parse(stdout) as TestResult 186 | return testResults 187 | }) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /extension/src/test-provider/test-trie.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import * as path from 'path' 3 | import { CADENCE_TEST_TAG, TEST_FUNCTION_PREFIX } from './constants' 4 | 5 | export interface TestFunction { 6 | name: string 7 | range: vscode.Range 8 | } 9 | 10 | export class TestTrie { 11 | #items: vscode.TestItemCollection 12 | #controller: vscode.TestController 13 | 14 | constructor (controller: vscode.TestController) { 15 | this.#items = controller.items 16 | this.#controller = controller 17 | } 18 | 19 | /** 20 | * Add a file to the test trie 21 | * @param filePath Path to the file 22 | * @param tests List of test functions in the file 23 | */ 24 | add (filePath: string, testFunctions: TestFunction[]): void { 25 | const workspaceNode: vscode.TestItem & { uri: vscode.Uri } | null = this.#getWorkspaceNode(filePath) 26 | if (workspaceNode == null) return 27 | let node: vscode.TestItem = workspaceNode 28 | 29 | const relativePath = path.relative(workspaceNode.uri.fsPath, filePath) 30 | const segments = path.normalize(relativePath).split(path.sep) 31 | for (const segment of segments) { 32 | if (node.uri == null) throw new Error('Node does not have a uri') 33 | const segmentPath = path.join(node.uri.fsPath, segment) 34 | let child = node.children.get(segment) 35 | if (child == null) { 36 | child = this.#createNode(segmentPath, false, true) 37 | node.children.add(child) 38 | } 39 | 40 | node = child 41 | } 42 | 43 | // Add all test functions for the file to the leaf node 44 | testFunctions.forEach((testFunction) => { 45 | const testItem = this.#controller.createTestItem(`${TEST_FUNCTION_PREFIX}${testFunction.name}`, testFunction.name, vscode.Uri.file(filePath)) 46 | testItem.range = testFunction.range 47 | testItem.tags = [CADENCE_TEST_TAG] 48 | node.children.add(testItem) 49 | }) 50 | } 51 | 52 | /** 53 | * Remove a file from the test trie 54 | * @param fsPath Path to the file or folder 55 | */ 56 | remove (fsPath: string): void { 57 | const node = this.get(fsPath) 58 | if (node == null) return 59 | 60 | // Remove node from parent 61 | if (node.parent == null) return 62 | node.parent.children.delete(node.id) 63 | 64 | // Remove any empty parent nodes 65 | let parent = node.parent 66 | while (parent?.parent != null && parent.children.size === 0) { 67 | parent.parent.children.delete(parent.id) 68 | parent = parent.parent 69 | } 70 | } 71 | 72 | /** 73 | * Get a test item from the test trie 74 | */ 75 | get (fsPath: string): vscode.TestItem | null { 76 | const workspaceNode: vscode.TestItem & { uri: vscode.Uri } | null = this.#getWorkspaceNode(fsPath) 77 | if (workspaceNode == null) return null 78 | let node: vscode.TestItem = workspaceNode 79 | 80 | const relativePath = path.relative(workspaceNode.uri.fsPath, fsPath) 81 | const segments = path.normalize(relativePath).split(path.sep) 82 | for (const segment of segments) { 83 | const child = node.children.get(segment) 84 | if (child == null) { 85 | return null 86 | } 87 | 88 | node = child 89 | } 90 | 91 | return node 92 | } 93 | 94 | /** 95 | * Clear all items from the test trie 96 | */ 97 | clear (): void { 98 | this.#items.forEach((item) => { 99 | this.#items.delete(item.id) 100 | }) 101 | } 102 | 103 | get rootNodes (): vscode.TestItem[] { 104 | const nodes: vscode.TestItem[] = [] 105 | this.#items.forEach((item) => { 106 | if (item.parent == null) { 107 | nodes.push(item) 108 | } 109 | }) 110 | return nodes 111 | } 112 | 113 | #createNode (filePath: string, isRoot: boolean = false, canResolveChildren: boolean = false): vscode.TestItem { 114 | const id = isRoot ? filePath : path.basename(filePath) 115 | const node = this.#controller.createTestItem(id, path.basename(filePath), vscode.Uri.file(filePath)) 116 | node.tags = [CADENCE_TEST_TAG] 117 | node.canResolveChildren = canResolveChildren 118 | return node 119 | } 120 | 121 | #getWorkspaceNode (filepath: string): vscode.TestItem & { uri: vscode.Uri } | null { 122 | const normalizedPath = path.normalize(filepath) 123 | let containingFolder: vscode.WorkspaceFolder | undefined 124 | for (const folder of vscode.workspace.workspaceFolders ?? []) { 125 | if (normalizedPath.startsWith(folder.uri.fsPath) && (containingFolder == null || folder.uri.fsPath.length > containingFolder.uri.fsPath.length)) { 126 | containingFolder = folder 127 | break 128 | } 129 | } 130 | if (containingFolder == null) return null 131 | 132 | const node: vscode.TestItem = this.#items.get(containingFolder.uri.fsPath) ?? this.#createNode(containingFolder.uri.fsPath, true, true) 133 | this.#items.add(node) 134 | return node as vscode.TestItem & { uri: vscode.Uri } 135 | } 136 | } 137 | 138 | /** 139 | * A class that allows mutations to be queued and executed sequentially 140 | */ 141 | export class QueuedMutator { 142 | #queue: Promise = Promise.resolve() 143 | #subject: T 144 | #recoverError: (error: Error, abortMutations: () => void) => Promise | void 145 | 146 | constructor (subject: T, recoverError: (error: Error, abortMutations: () => void) => Promise | void) { 147 | this.#subject = subject 148 | this.#recoverError = recoverError 149 | } 150 | 151 | async mutate (task: (subject: T) => Promise): Promise { 152 | const mutationPromise = this.#queue.then(async () => await task(this.#subject)) 153 | this.#queue = mutationPromise.catch(async (error) => { 154 | await this.#recoverError(error, () => { 155 | this.#queue = Promise.resolve() 156 | }) 157 | }) 158 | await mutationPromise 159 | } 160 | 161 | async getState (): Promise { 162 | let previousTask: Promise | null = null 163 | while (this.#queue !== previousTask) { 164 | await this.#queue 165 | previousTask = this.#queue 166 | } 167 | return this.#subject 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /extension/src/test-provider/utils.ts: -------------------------------------------------------------------------------- 1 | import { TEST_FUNCTION_PREFIX } from './constants' 2 | 3 | export function encodeTestFunctionId (testName: string): string { 4 | return `${TEST_FUNCTION_PREFIX}${testName}` 5 | } 6 | 7 | export function decodeTestFunctionId (id: string): string | null { 8 | return id.startsWith(TEST_FUNCTION_PREFIX) ? id.slice(TEST_FUNCTION_PREFIX.length) : null 9 | } 10 | -------------------------------------------------------------------------------- /extension/src/ui/notification-provider.ts: -------------------------------------------------------------------------------- 1 | import { StorageProvider } from '../storage/storage-provider' 2 | import { promptUserErrorMessage, promptUserInfoMessage, promptUserWarningMessage } from './prompts' 3 | import * as vscode from 'vscode' 4 | 5 | const NOTIFICATIONS_URL = 'https://raw.githubusercontent.com/onflow/vscode-cadence/master/.metadata/notifications.json' 6 | 7 | export interface Notification { 8 | _type: 'Notification' 9 | id: string 10 | type: 'error' | 'info' | 'warning' 11 | text: string 12 | buttons?: Array<{ 13 | label: string 14 | link: string 15 | }> 16 | suppressable?: boolean 17 | } 18 | 19 | export class NotificationProvider { 20 | #storageProvider: StorageProvider 21 | 22 | constructor ( 23 | storageProvider: StorageProvider 24 | ) { 25 | this.#storageProvider = storageProvider 26 | } 27 | 28 | activate (): void { 29 | void this.#fetchAndDisplayNotifications() 30 | } 31 | 32 | async #fetchAndDisplayNotifications (): Promise { 33 | // Fetch notifications 34 | const notifications = await this.#fetchNotifications() 35 | 36 | // Display all valid notifications 37 | notifications 38 | .filter(this.#notificationFilter.bind(this)) 39 | .forEach(this.#displayNotification.bind(this)) 40 | } 41 | 42 | #displayNotification (notification: Notification): void { 43 | const transformButton = (button: { label: string, link: string }): { label: string, callback: () => void } => { 44 | return { 45 | label: button.label, 46 | callback: () => { 47 | void vscode.env.openExternal(vscode.Uri.parse(button.link)) 48 | } 49 | } 50 | } 51 | 52 | // Transform buttons 53 | let buttons: Array<{ label: string, callback: () => void }> = [] 54 | if (notification.suppressable === true) { 55 | buttons = [{ 56 | label: 'Don\'t show again', 57 | callback: () => { 58 | this.#dismissNotification(notification) 59 | } 60 | }] 61 | } 62 | buttons = buttons?.concat(notification.buttons?.map(transformButton) ?? []) 63 | 64 | if (notification.type === 'error') { 65 | promptUserErrorMessage(notification.text, buttons) 66 | } else if (notification.type === 'info') { 67 | promptUserInfoMessage(notification.text, buttons) 68 | } else if (notification.type === 'warning') { 69 | promptUserWarningMessage(notification.text, buttons) 70 | } 71 | } 72 | 73 | #notificationFilter (notification: Notification): boolean { 74 | if (notification.suppressable === true && this.#isNotificationDismissed(notification)) { 75 | return false 76 | } 77 | 78 | return true 79 | } 80 | 81 | async #fetchNotifications (): Promise { 82 | return await fetch(NOTIFICATIONS_URL).then(async res => await res.json()).then((notifications: Notification[]) => { 83 | return notifications 84 | }).catch(() => { 85 | return [] 86 | }) 87 | } 88 | 89 | #dismissNotification (notification: Notification): void { 90 | const dismissedNotifications = this.#storageProvider.get('dismissedNotifications', []) 91 | void this.#storageProvider.set('dismissedNotifications', [...dismissedNotifications, notification.id]) 92 | } 93 | 94 | #isNotificationDismissed (notification: Notification): boolean { 95 | const dismissedNotifications = this.#storageProvider.get('dismissedNotifications', []) 96 | return dismissedNotifications.includes(notification.id) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /extension/src/ui/prompts.ts: -------------------------------------------------------------------------------- 1 | /* Information and error prompts */ 2 | import { window } from 'vscode' 3 | 4 | export interface PromptButton { 5 | label: string 6 | callback: Function 7 | } 8 | 9 | export function promptUserInfoMessage (message: string, buttons: PromptButton[] = []): void { 10 | window.showInformationMessage( 11 | message, 12 | ...buttons.map((button) => button.label) 13 | ).then((choice) => { 14 | const button = buttons.find((button) => button.label === choice) 15 | if (button != null) { 16 | button.callback() 17 | } 18 | }, () => {}) 19 | } 20 | 21 | export function promptUserErrorMessage (message: string, buttons: PromptButton[] = []): void { 22 | window.showErrorMessage( 23 | message, 24 | ...buttons.map((button) => button.label) 25 | ).then((choice) => { 26 | const button = buttons.find((button) => button.label === choice) 27 | if (button != null) { 28 | button.callback() 29 | } 30 | }, () => {}) 31 | } 32 | 33 | export function promptUserWarningMessage (message: string, buttons: PromptButton[] = []): void { 34 | window.showWarningMessage( 35 | message, 36 | ...buttons.map((button) => button.label) 37 | ).then((choice) => { 38 | const button = buttons.find((button) => button.label === choice) 39 | if (button != null) { 40 | button.callback() 41 | } 42 | }, () => {}) 43 | } 44 | -------------------------------------------------------------------------------- /extension/src/utils/semaphore.ts: -------------------------------------------------------------------------------- 1 | export function semaphore (concurrency: number): (fn: () => Promise) => Promise { 2 | let current = 0 3 | const queue: Array<() => Promise> = [] 4 | return async (fn: () => Promise): Promise => { 5 | return await new Promise((resolve, reject) => { 6 | const run = async (): Promise => { 7 | current++ 8 | try { 9 | resolve(await fn()) 10 | } catch (error) { 11 | reject(error) 12 | } finally { 13 | current-- 14 | if (queue.length > 0) { 15 | void queue.shift()?.() 16 | } 17 | } 18 | } 19 | if (current >= concurrency) { 20 | queue.push(run) 21 | } else { 22 | void run() 23 | } 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /extension/src/utils/shell/default-shell.ts: -------------------------------------------------------------------------------- 1 | export function getDefaultShell (): string { 2 | if (process.platform === 'win32') { 3 | return 'powershell' 4 | } else if (process.platform === 'darwin') { 5 | return 'zsh' 6 | } else { 7 | return 'bash' 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /extension/src/utils/shell/env-vars.ts: -------------------------------------------------------------------------------- 1 | 2 | import { StateCache } from '../state-cache' 3 | import { ChildProcessWithoutNullStreams, spawn } from 'child_process' 4 | import { getDefaultShell } from './default-shell' 5 | 6 | const PRINT_ENV_POWERSHELL = ` 7 | $machineEnv = [Environment]::GetEnvironmentVariables('Machine') 8 | $userEnv = [Environment]::GetEnvironmentVariables('User') 9 | 10 | $env = @{} 11 | $machineEnv.Keys | ForEach-Object { 12 | $env[$_] = $machineEnv[$_] 13 | } 14 | 15 | $userEnv.Keys | ForEach-Object { 16 | $env[$_] = $userEnv[$_] 17 | } 18 | 19 | # handle PATH special ase 20 | $machinePath = $machineEnv['Path'] 21 | $userPath = $userEnv['Path'] 22 | 23 | $env['Path'] = $machinePath + ';' + $userPath 24 | 25 | # Iterate over the dictionary and print key-value pairs 26 | foreach ($key in $env.Keys) { 27 | Write-Host "$key=$($env[$key])" 28 | }` 29 | 30 | export const envVars = new StateCache(async () => { 31 | const shell = getDefaultShell() 32 | return await getEnvVars(shell).catch(() => process.env) 33 | }) 34 | 35 | async function getEnvVars (shell: string): Promise<{ [key: string]: string | undefined }> { 36 | const OS_TYPE = process.platform 37 | let childProcess: ChildProcessWithoutNullStreams 38 | if (OS_TYPE === 'win32') { 39 | childProcess = spawn('powershell', [PRINT_ENV_POWERSHELL], { env: {} }) 40 | } else { 41 | childProcess = spawn(shell, ['-l', '-i', '-c', 'env']) 42 | } 43 | 44 | let stdout = '' 45 | let stderr = '' 46 | 47 | childProcess.stdout.on('data', (data) => { 48 | stdout += String(data) 49 | }) 50 | 51 | childProcess.stderr.on('data', (data) => { 52 | stderr += String(data) 53 | }) 54 | 55 | return await new Promise((resolve, reject) => { 56 | childProcess.on('close', (code) => { 57 | if (code === 0) { 58 | const env: { [key: string]: string | undefined } = process.env 59 | stdout.split('\n').forEach((line) => { 60 | const [key, value] = line.split('=') 61 | if (key !== undefined && value !== undefined) { 62 | env[key] = value 63 | } 64 | }) 65 | resolve(env) 66 | } else { 67 | reject(stderr) 68 | } 69 | }) 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /extension/src/utils/shell/exec.ts: -------------------------------------------------------------------------------- 1 | import { SpawnOptionsWithoutStdio, spawn } from 'child_process' 2 | import { envVars } from './env-vars' 3 | import * as vscode from 'vscode' 4 | import { getDefaultShell } from './default-shell' 5 | 6 | interface ExecResult { 7 | stdout: string 8 | stderr: string 9 | code: number | null 10 | } 11 | 12 | // Execute a command in default shell 13 | export async function execDefault (cmd: string, args?: readonly string[] | undefined, options?: SpawnOptionsWithoutStdio | undefined, cancellationToken?: vscode.CancellationToken): Promise { 14 | const OS_TYPE = process.platform 15 | if (OS_TYPE === 'win32') { 16 | return await execPowerShell(cmd, args, options, cancellationToken) 17 | } else { 18 | return await execUnixDefault(cmd, args, options, cancellationToken) 19 | } 20 | } 21 | 22 | // Execute a command in powershell 23 | export async function execPowerShell (cmd: string, args?: readonly string[] | undefined, options?: SpawnOptionsWithoutStdio | undefined, cancellationToken?: vscode.CancellationToken): Promise { 24 | const env = await envVars.getValue() 25 | return await abortableExec(cmd, args, { env, shell: 'powershell.exe', ...options }, cancellationToken) 26 | } 27 | 28 | // Execute command in default shell 29 | export async function execUnixDefault (cmd: string, args?: readonly string[] | undefined, options?: SpawnOptionsWithoutStdio | undefined, cancellationToken?: vscode.CancellationToken): Promise { 30 | const env = await envVars.getValue() 31 | return await abortableExec(cmd, args, { env, shell: getDefaultShell(), ...options }, cancellationToken) 32 | } 33 | 34 | async function abortableExec (cmd: string, args?: readonly string[] | undefined, options?: SpawnOptionsWithoutStdio | undefined, cancellationToken?: vscode.CancellationToken): Promise { 35 | let cancellationHandler: vscode.Disposable | undefined 36 | return await new Promise((resolve, reject) => { 37 | cancellationHandler = cancellationToken?.onCancellationRequested(() => { 38 | childProcess.kill() 39 | reject(new Error('Command execution cancelled')) 40 | }) 41 | 42 | const childProcess = spawn(cmd, args, { ...options }) 43 | let stdout = '' 44 | let stderr = '' 45 | 46 | childProcess.stdout.on('data', (data) => { 47 | stdout += String(data) 48 | }) 49 | 50 | childProcess.stderr.on('data', (data) => { 51 | stderr += String(data) 52 | }) 53 | 54 | childProcess.on('error', (err) => { 55 | reject(err) 56 | }) 57 | 58 | childProcess.on('close', (code) => { 59 | resolve({ stdout, stderr, code }) 60 | }) 61 | }).finally(() => { 62 | cancellationHandler?.dispose() 63 | }) 64 | } 65 | 66 | export async function tryExecDefault (cmd: string, args?: readonly string[] | undefined, options?: SpawnOptionsWithoutStdio | undefined, cancellationToken?: vscode.CancellationToken): Promise { 67 | return await execDefault(cmd, args, options, cancellationToken).then(({ code }) => code === 0).catch(() => false) 68 | } 69 | 70 | export async function tryExecPowerShell (cmd: string, args?: readonly string[] | undefined, options?: SpawnOptionsWithoutStdio | undefined, cancellationToken?: vscode.CancellationToken): Promise { 71 | return await execPowerShell(cmd, args, options, cancellationToken).then(({ code }) => code === 0).catch(() => false) 72 | } 73 | 74 | export async function tryExecUnixDefault (cmd: string, args?: readonly string[] | undefined, options?: SpawnOptionsWithoutStdio | undefined, cancellationToken?: vscode.CancellationToken): Promise { 75 | return await execUnixDefault(cmd, args, options, cancellationToken).then(({ code }) => code === 0).catch(() => false) 76 | } 77 | 78 | // Execute a command in vscode terminal 79 | export async function execVscodeTerminal (name: string, command: string, shellPath?: string): Promise { 80 | const OS_TYPE = process.platform 81 | if (shellPath == null) { shellPath = OS_TYPE === 'win32' ? 'powershell.exe' : getDefaultShell() } 82 | 83 | const term = vscode.window.createTerminal({ 84 | name, 85 | hideFromUser: false, 86 | shellPath, 87 | env: await envVars.getValue() 88 | }) 89 | 90 | // Add exit to command to close terminal 91 | command = OS_TYPE === 'win32' ? command + '; exit $LASTEXITCODE' : command + '; exit $?' 92 | 93 | term.sendText(command) 94 | term.show() 95 | 96 | // Wait for installation to complete 97 | await new Promise((resolve, reject) => { 98 | const disposeToken = vscode.window.onDidCloseTerminal( 99 | async (closedTerminal) => { 100 | if (closedTerminal === term) { 101 | disposeToken.dispose() 102 | if (term.exitStatus?.code === 0) { 103 | resolve() 104 | } else { 105 | reject(new Error('Terminal execution failed')) 106 | } 107 | } 108 | } 109 | ) 110 | }) 111 | } 112 | -------------------------------------------------------------------------------- /extension/src/utils/state-cache.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject, Observable, firstValueFrom, map, skip } from 'rxjs' 2 | 3 | enum ValidationState { 4 | Valid = 0, 5 | Fetching = 1, 6 | FetchingAndQueued = 2 7 | } 8 | 9 | /** 10 | * @template T 11 | * @class StateCache 12 | * @description 13 | * A class that caches a value and fetches it asynchronously. Comparable to SWR in React. 14 | * It provides the following guarantees: 15 | * - The value is always up to date 16 | * - The value is only fetched once per invalidation 17 | */ 18 | export class StateCache extends Observable { 19 | #validationState: ValidationState = ValidationState.Valid 20 | #value: BehaviorSubject<[T | null, Error | null] | undefined> = new BehaviorSubject<[T | null, Error | null] | undefined>(undefined) 21 | #fetcher: () => Promise 22 | 23 | // Observable to subscribe to in order to skip initial undefined value and clean up errors 24 | #observable: Observable = (this.#value as BehaviorSubject<[T | null, Error | null]>).pipe(skip(1), map(([value, error]) => { 25 | if (error !== null) { 26 | throw error 27 | } else { 28 | return value as T 29 | } 30 | })) 31 | 32 | constructor (fetcher: () => Promise) { 33 | super((...args) => this.#observable.subscribe(...args)) 34 | this.#fetcher = fetcher 35 | this.invalidate() 36 | } 37 | 38 | async getValue (refresh?: boolean): Promise { 39 | // If refresh flag is set, invalidate the cache 40 | if (refresh === true) this.invalidate() 41 | 42 | // Wait for up to date value 43 | let value: T | null, error: Error | null 44 | if (this.#validationState === ValidationState.Valid) { 45 | [value, error] = (this.#value as BehaviorSubject<[T | null, Error | null]>).getValue() 46 | } else { 47 | const queueNumber = this.#validationState - 1 48 | ;[value, error] = await (firstValueFrom((this.#value as BehaviorSubject<[T, Error]>).pipe(skip(queueNumber + 1)))) 49 | } 50 | 51 | // If error, rethrow with added stack trace 52 | if (error !== null) { 53 | // Create a new Error object 54 | const newError = new Error(error.message) 55 | // Append the original error's stack trace to the new error 56 | if (error.stack != null) { 57 | newError.stack = (newError.stack ?? '') + '\n\nOriginal Stack Trace:\n' + error.stack 58 | } 59 | // Throw the new error 60 | throw newError 61 | } else { 62 | return value as T 63 | } 64 | } 65 | 66 | async #fetch (): Promise { 67 | let value: T | null = null; let error: Error | null = null 68 | try { 69 | value = await this.#fetcher() 70 | } catch (e: any) { 71 | error = e 72 | } 73 | 74 | this.#validationState -= 1 75 | this.#value.next([value, error]) 76 | 77 | if (this.#validationState > 0) { 78 | void this.#fetch() 79 | } 80 | } 81 | 82 | invalidate (): void { 83 | this.#validationState = Math.min(this.#validationState + 1, 2) 84 | // If we're not already fetching, start fetching 85 | if (this.#validationState === ValidationState.Fetching) { 86 | void this.#fetch() 87 | } 88 | } 89 | 90 | setFetcher (fetcher: () => Promise): void { 91 | this.#fetcher = fetcher 92 | this.invalidate() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /extension/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs' 2 | import * as path from 'path' 3 | import { workspace } from 'vscode' 4 | import * as vscode from 'vscode' 5 | 6 | export const FILE_PATH_EMPTY = '' 7 | 8 | export async function delay (seconds: number): Promise { 9 | await new Promise((resolve, reject) => { 10 | setTimeout(() => resolve(''), seconds * 1000) 11 | }) 12 | } 13 | 14 | export function pathsAreEqual (path1: string, path2: string): boolean { 15 | path1 = path.resolve(path1) 16 | path2 = path.resolve(path2) 17 | if (process.platform === 'win32') { return path1.toLowerCase() === path2.toLowerCase() } 18 | return path1 === path2 19 | } 20 | 21 | export function findFilesInAnyWorkspace (filepath: string): string[] { 22 | return (workspace.workspaceFolders?.reduce( 23 | (res, folder) => { 24 | const filePath = path.resolve(folder.uri.fsPath, filepath) 25 | if (existsSync(filePath)) { 26 | res.push(filePath) 27 | } 28 | return res 29 | }, 30 | [] 31 | )) ?? [] 32 | } 33 | 34 | export async function isDirectory (uri: vscode.Uri): Promise { 35 | try { 36 | return (await vscode.workspace.fs.stat(uri)).type === vscode.FileType.Directory 37 | } catch { 38 | return false 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /extension/test/fixtures/workspace/Error.cdc: -------------------------------------------------------------------------------- 1 | /** 2 | Careful: this cadence code is purposely written with errors so we can test error marking 3 | */ 4 | access(all) contract interface Foo { 5 | 6 | access(all) var bar: UInt6 7 | 8 | fun zoo() { 9 | return 2 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /extension/test/fixtures/workspace/FooContract.cdc: -------------------------------------------------------------------------------- 1 | access(all) contract FooContract {} 2 | -------------------------------------------------------------------------------- /extension/test/fixtures/workspace/NonFungibleToken.cdc: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | ## The Flow Non-Fungible Token standard 4 | 5 | ## `NonFungibleToken` contract interface 6 | 7 | The interface that all non-fungible token contracts could conform to. 8 | If a user wants to deploy a new nft contract, their contract would need 9 | to implement the NonFungibleToken interface. 10 | 11 | Their contract would have to follow all the rules and naming 12 | that the interface specifies. 13 | 14 | ## `NFT` resource 15 | 16 | The core resource type that represents an NFT in the smart contract. 17 | 18 | ## `Collection` Resource 19 | 20 | The resource that stores a user's NFT collection. 21 | It includes a few functions to allow the owner to easily 22 | move tokens in and out of the collection. 23 | 24 | ## `Provider` and `Receiver` resource interfaces 25 | 26 | These interfaces declare functions with some pre and post conditions 27 | that require the Collection to follow certain naming and behavior standards. 28 | 29 | They are separate because it gives the user the ability to share a reference 30 | to their Collection that only exposes the fields and functions in one or more 31 | of the interfaces. It also gives users the ability to make custom resources 32 | that implement these interfaces to do various things with the tokens. 33 | 34 | By using resources and interfaces, users of NFT smart contracts can send 35 | and receive tokens peer-to-peer, without having to interact with a central ledger 36 | smart contract. 37 | 38 | To send an NFT to another user, a user would simply withdraw the NFT 39 | from their Collection, then call the deposit function on another user's 40 | Collection to complete the transfer. 41 | 42 | */ 43 | 44 | // The main NFT contract interface. Other NFT contracts will 45 | // import and implement this interface 46 | // 47 | pub contract interface NonFungibleToken { 48 | 49 | // The total number of tokens of this type in existence 50 | pub var totalSupply: UInt64 51 | 52 | // Event that emitted when the NFT contract is initialized 53 | // 54 | pub event ContractInitialized() 55 | 56 | // Event that is emitted when a token is withdrawn, 57 | // indicating the owner of the collection that it was withdrawn from. 58 | // 59 | // If the collection is not in an account's storage, `from` will be `nil`. 60 | // 61 | pub event Withdraw(id: UInt64, from: Address?) 62 | 63 | // Event that emitted when a token is deposited to a collection. 64 | // 65 | // It indicates the owner of the collection that it was deposited to. 66 | // 67 | pub event Deposit(id: UInt64, to: Address?) 68 | 69 | // Interface that the NFTs have to conform to 70 | // 71 | pub resource interface INFT { 72 | // The unique ID that each NFT has 73 | pub let id: UInt64 74 | } 75 | 76 | // Requirement that all conforming NFT smart contracts have 77 | // to define a resource called NFT that conforms to INFT 78 | pub resource NFT: INFT { 79 | pub let id: UInt64 80 | } 81 | 82 | // Interface to mediate withdraws from the Collection 83 | // 84 | pub resource interface Provider { 85 | // withdraw removes an NFT from the collection and moves it to the caller 86 | pub fun withdraw(withdrawID: UInt64): @NFT { 87 | post { 88 | result.id == withdrawID: "The ID of the withdrawn token must be the same as the requested ID" 89 | } 90 | } 91 | } 92 | 93 | // Interface to mediate deposits to the Collection 94 | // 95 | pub resource interface Receiver { 96 | 97 | // deposit takes an NFT as an argument and adds it to the Collection 98 | // 99 | pub fun deposit(token: @NFT) 100 | } 101 | 102 | // Interface that an account would commonly 103 | // publish for their collection 104 | pub resource interface CollectionPublic { 105 | pub fun deposit(token: @NFT) 106 | pub fun getIDs(): [UInt64] 107 | pub fun borrowNFT(id: UInt64): &NFT 108 | } 109 | 110 | // Requirement for the the concrete resource type 111 | // to be declared in the implementing contract 112 | // 113 | pub resource Collection: Provider, Receiver, CollectionPublic { 114 | 115 | // Dictionary to hold the NFTs in the Collection 116 | pub var ownedNFTs: @{UInt64: NFT} 117 | 118 | // withdraw removes an NFT from the collection and moves it to the caller 119 | pub fun withdraw(withdrawID: UInt64): @NFT 120 | 121 | // deposit takes a NFT and adds it to the collections dictionary 122 | // and adds the ID to the id array 123 | pub fun deposit(token: @NFT) 124 | 125 | // getIDs returns an array of the IDs that are in the collection 126 | pub fun getIDs(): [UInt64] 127 | 128 | // Returns a borrowed reference to an NFT in the collection 129 | // so that the caller can read data and call methods from it 130 | pub fun borrowNFT(id: UInt64): &NFT { 131 | pre { 132 | self.ownedNFTs[id] != nil: "NFT does not exist in the collection!" 133 | } 134 | } 135 | } 136 | 137 | // createEmptyCollection creates an empty Collection 138 | // and returns it to the caller so that they can own NFTs 139 | pub fun createEmptyCollection(): @Collection { 140 | post { 141 | result.getIDs().length == 0: "The created collection must be empty!" 142 | } 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /extension/test/fixtures/workspace/Script.cdc: -------------------------------------------------------------------------------- 1 | access(all) fun main(): UFix64 { 2 | return 42.0 3 | } 4 | -------------------------------------------------------------------------------- /extension/test/fixtures/workspace/Tx.cdc: -------------------------------------------------------------------------------- 1 | transaction() { 2 | let guest: Address 3 | 4 | prepare(authorizer: &Account) { 5 | self.guest = authorizer.address 6 | } 7 | 8 | execute { 9 | log("Hello ".concat(self.guest.toString())) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /extension/test/fixtures/workspace/bar/flow.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onflow/vscode-cadence/4351a8051a99d8df69030a68d513b43a7f77222c/extension/test/fixtures/workspace/bar/flow.json -------------------------------------------------------------------------------- /extension/test/fixtures/workspace/flow.json: -------------------------------------------------------------------------------- 1 | { 2 | "emulators": { 3 | "default": { 4 | "port": 3569, 5 | "serviceAccount": "emulator-account" 6 | } 7 | }, 8 | "contracts": {}, 9 | "networks": { 10 | "emulator": "127.0.0.1:3569", 11 | "mainnet": "access.mainnet.nodes.onflow.org:9000", 12 | "testnet": "access.devnet.nodes.onflow.org:9000" 13 | }, 14 | "accounts": { 15 | "emulator-account": { 16 | "address": "f8d6e0586b0a20c7", 17 | "key": "d2f9b3e122aa5289fb38edab611c8d1c2aa88d6fd8e3943a306a493361639812" 18 | } 19 | }, 20 | "deployments": {} 21 | } -------------------------------------------------------------------------------- /extension/test/fixtures/workspace/foo/flow.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onflow/vscode-cadence/4351a8051a99d8df69030a68d513b43a7f77222c/extension/test/fixtures/workspace/foo/flow.json -------------------------------------------------------------------------------- /extension/test/fixtures/workspace/test/bar/test2.cdc: -------------------------------------------------------------------------------- 1 | import Test 2 | 3 | access(all) fun testPassing() { 4 | Test.assert(true) 5 | } 6 | 7 | access(all) fun testFailing() { 8 | Test.assert(false) 9 | } -------------------------------------------------------------------------------- /extension/test/fixtures/workspace/test/bar/test3.cdc: -------------------------------------------------------------------------------- 1 | import Test 2 | 3 | access(all) fun testFailing() { 4 | Test.assert(false) 5 | } 6 | 7 | access(all) fun testPassing() { 8 | Test.assert(true) 9 | } -------------------------------------------------------------------------------- /extension/test/fixtures/workspace/test/test1.cdc: -------------------------------------------------------------------------------- 1 | import Test 2 | 3 | access(all) fun testPassing() { 4 | Test.assert(true) 5 | } -------------------------------------------------------------------------------- /extension/test/globals.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | export const MaxTimeout = 100000 4 | 5 | export function ASSERT_EQUAL (a: any, b: any): void { 6 | assert.strictEqual(a, b) 7 | } 8 | -------------------------------------------------------------------------------- /extension/test/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as Mocha from 'mocha' 3 | import { glob } from 'glob' 4 | 5 | export async function run (): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }) 11 | 12 | const testsRoot = path.resolve(__dirname, '..') 13 | 14 | // Add files to the test suite 15 | const files = (await glob('**/**.test.js', { cwd: testsRoot })).sort() 16 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))) 17 | 18 | // Run the mocha test 19 | await new Promise((resolve, reject) => { 20 | mocha.run(failures => { 21 | if (failures > 0) { 22 | reject(new Error(`${failures} tests failed.`)) 23 | } else { 24 | resolve() 25 | } 26 | }) 27 | }) 28 | } 29 | 30 | export async function delay (seconds: number): Promise { 31 | await new Promise((resolve, reject) => { 32 | setTimeout(() => resolve(''), seconds * 1000) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /extension/test/integration/0 - dependencies.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { DependencyInstaller } from '../../src/dependency-installer/dependency-installer' 3 | import { MaxTimeout } from '../globals' 4 | import { InstallFlowCLI } from '../../src/dependency-installer/installers/flow-cli-installer' 5 | import { stub } from 'sinon' 6 | import { before } from 'mocha' 7 | import { CliProvider } from '../../src/flow-cli/cli-provider' 8 | import { getMockSettings } from '../mock/mockSettings' 9 | 10 | // Note: Dependency installation must run before other integration tests 11 | suite('Dependency Installer', () => { 12 | let cliProvider: any 13 | 14 | before(async function () { 15 | cliProvider = new CliProvider(getMockSettings()) 16 | }) 17 | 18 | test('Install Missing Dependencies', async () => { 19 | const mockLanguageServerApi = { 20 | activate: stub(), 21 | deactivate: stub(), 22 | isActive: true 23 | } 24 | const dependencyManager = new DependencyInstaller(mockLanguageServerApi as any, cliProvider) 25 | await assert.doesNotReject(async () => { await dependencyManager.installMissing() }) 26 | 27 | // Check that all dependencies are installed 28 | await dependencyManager.checkDependencies() 29 | assert.deepStrictEqual(await dependencyManager.missingDependencies.getValue(), []) 30 | }).timeout(MaxTimeout) 31 | 32 | test('Flow CLI installer restarts langauge server if active', async () => { 33 | const mockLanguageServerApi = { 34 | activate: stub().callsFake(async () => { 35 | mockLanguageServerApi.isActive = true 36 | }), 37 | deactivate: stub().callsFake(async () => { 38 | mockLanguageServerApi.isActive = false 39 | }), 40 | isActive: true 41 | } 42 | const mockInstallerContext = { 43 | refreshDependencies: async () => {}, 44 | languageServerApi: mockLanguageServerApi as any, 45 | cliProvider 46 | } 47 | const flowCliInstaller = new InstallFlowCLI(mockInstallerContext) 48 | 49 | await assert.doesNotReject(async () => { await flowCliInstaller.install() }) 50 | assert(mockLanguageServerApi.deactivate.calledOnce) 51 | assert(mockLanguageServerApi.activate.calledOnce) 52 | assert(mockLanguageServerApi.deactivate.calledBefore(mockLanguageServerApi.activate)) 53 | }).timeout(MaxTimeout) 54 | 55 | test('Flow CLI installer does not restart langauge server if inactive', async () => { 56 | const mockLanguageServerApi = { 57 | activate: stub().callsFake(async () => { 58 | mockLanguageServerApi.isActive = true 59 | }), 60 | deactivate: stub().callsFake(async () => { 61 | mockLanguageServerApi.isActive = false 62 | }), 63 | isActive: false 64 | } 65 | const mockInstallerContext = { 66 | refreshDependencies: async () => {}, 67 | languageServerApi: mockLanguageServerApi as any, 68 | cliProvider 69 | } 70 | const flowCliInstaller = new InstallFlowCLI(mockInstallerContext) 71 | 72 | await assert.doesNotReject(async () => { await flowCliInstaller.install() }) 73 | assert(mockLanguageServerApi.activate.notCalled) 74 | assert(mockLanguageServerApi.deactivate.notCalled) 75 | }).timeout(MaxTimeout) 76 | }) 77 | -------------------------------------------------------------------------------- /extension/test/integration/1 - language-server.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { before, after } from 'mocha' 3 | import { getMockSettings } from '../mock/mockSettings' 4 | import { LanguageServerAPI } from '../../src/server/language-server' 5 | import { FlowConfig } from '../../src/server/flow-config' 6 | import { Settings } from '../../src/settings/settings' 7 | import { MaxTimeout } from '../globals' 8 | import { BehaviorSubject, Subject } from 'rxjs' 9 | import { State } from 'vscode-languageclient' 10 | import * as sinon from 'sinon' 11 | import { SemVer } from 'semver' 12 | import { CliBinary } from '../../src/flow-cli/cli-versions-provider' 13 | 14 | suite('Language Server & Emulator Integration', () => { 15 | let LS: LanguageServerAPI 16 | let settings: Settings 17 | let mockConfig: FlowConfig 18 | let fileModified$: Subject 19 | let pathChanged$: Subject 20 | let cliBinary$: BehaviorSubject 21 | 22 | before(async function () { 23 | this.timeout(MaxTimeout) 24 | // Initialize language server 25 | settings = getMockSettings() 26 | fileModified$ = new Subject() 27 | pathChanged$ = new Subject() 28 | mockConfig = { 29 | fileModified$, 30 | pathChanged$, 31 | configPath: null 32 | } as any 33 | 34 | // create a mock cli provider without invokign the constructor 35 | cliBinary$ = new BehaviorSubject({ 36 | command: 'flow', 37 | version: new SemVer('1.0.0') 38 | }) 39 | const mockCliProvider = { 40 | currentBinary$: cliBinary$, 41 | getCurrentBinary: sinon.stub().callsFake(async () => cliBinary$.getValue()) 42 | } as any 43 | 44 | LS = new LanguageServerAPI(settings, mockCliProvider, mockConfig) 45 | await LS.activate() 46 | }) 47 | 48 | after(async function () { 49 | this.timeout(MaxTimeout) 50 | await LS.deactivate() 51 | }) 52 | 53 | test('Language Server Client', async () => { 54 | assert.notStrictEqual(LS.client, undefined) 55 | assert.equal(LS.client?.state, State.Running) 56 | }) 57 | 58 | test('Deactivate Language Server Client', async () => { 59 | const client = LS.client 60 | await LS.deactivate() 61 | 62 | // Check that client remains stopped even if config changes or CLI binary changes 63 | fileModified$.next() 64 | pathChanged$.next('foo') 65 | cliBinary$.next({ 66 | command: 'flow', 67 | version: new SemVer('1.0.1') 68 | }) 69 | 70 | assert.equal(client?.state, State.Stopped) 71 | assert.equal(LS.client, null) 72 | assert.equal(LS.clientState$.getValue(), State.Stopped) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /extension/test/integration/2 - commands.test.ts: -------------------------------------------------------------------------------- 1 | import { MaxTimeout } from '../globals' 2 | import { before, after } from 'mocha' 3 | import * as assert from 'assert' 4 | import * as commands from '../../src/commands/command-constants' 5 | import { CommandController } from '../../src/commands/command-controller' 6 | import { DependencyInstaller } from '../../src/dependency-installer/dependency-installer' 7 | import * as sinon from 'sinon' 8 | 9 | suite('Extension Commands', () => { 10 | let checkDependenciesStub: sinon.SinonStub 11 | let mockDependencyInstaller: DependencyInstaller 12 | let commandController: CommandController 13 | 14 | before(async function () { 15 | this.timeout(MaxTimeout) 16 | 17 | // Initialize the command controller & mock dependencies 18 | checkDependenciesStub = sinon.stub() 19 | mockDependencyInstaller = { 20 | checkDependencies: checkDependenciesStub 21 | } as any 22 | commandController = new CommandController(mockDependencyInstaller) 23 | }) 24 | 25 | after(async function () { 26 | this.timeout(MaxTimeout) 27 | }) 28 | 29 | test('Command: Check Dependencies', async () => { 30 | assert.ok(commandController.executeCommand(commands.CHECK_DEPENDENCIES)) 31 | 32 | // Check that the dependency installer was called to check dependencies 33 | assert.ok(checkDependenciesStub.calledOnce) 34 | }).timeout(MaxTimeout) 35 | }) 36 | -------------------------------------------------------------------------------- /extension/test/integration/3 - schema.test.ts: -------------------------------------------------------------------------------- 1 | import { MaxTimeout } from '../globals' 2 | import { before, after } from 'mocha' 3 | import * as assert from 'assert' 4 | import * as vscode from 'vscode' 5 | import { SemVer } from 'semver' 6 | import { JSONSchemaProvider } from '../../src/json-schema-provider' 7 | import * as fetch from 'node-fetch' 8 | import { readFileSync } from 'fs' 9 | import * as path from 'path' 10 | import * as sinon from 'sinon' 11 | import { CliProvider } from '../../src/flow-cli/cli-provider' 12 | import { Subject } from 'rxjs' 13 | 14 | suite('JSON schema tests', () => { 15 | let mockFlowVersionValue: SemVer | null = null 16 | let mockCliProvider: CliProvider 17 | let extensionPath: string 18 | let schemaProvider: JSONSchemaProvider 19 | 20 | let originalFetch: typeof fetch 21 | 22 | before(async function () { 23 | this.timeout(MaxTimeout) 24 | 25 | // Mock extension path 26 | extensionPath = path.resolve(__dirname, '../../../..') 27 | 28 | // Mock cli provider 29 | mockCliProvider = { 30 | currentBinary$: new Subject(), 31 | getCurrentBinary: sinon.stub().callsFake(async () => ((mockFlowVersionValue != null) 32 | ? { 33 | name: 'flow', 34 | version: mockFlowVersionValue 35 | } 36 | : null)) 37 | } as any 38 | 39 | // Mock fetch (assertion is for linter workaround) 40 | originalFetch = fetch.default 41 | ;(fetch as unknown as any).default = async (url: string) => { 42 | // only mock valid response for version 1.0.0 for testing 43 | // other versions will return 404 and emulate a missing schema 44 | if (url === 'https://raw.githubusercontent.com/onflow/flow-cli/v1.0.0/flowkit/schema.json') { 45 | return { 46 | ok: true, 47 | text: async () => JSON.stringify({ 48 | dummy: 'schema for flow.json' 49 | }) 50 | } as any 51 | } else { 52 | return { 53 | ok: false, 54 | statusText: 'Not found' 55 | } as any 56 | } 57 | } 58 | 59 | // Initialize the schema provider 60 | schemaProvider = new JSONSchemaProvider(extensionPath, mockCliProvider) 61 | }) 62 | 63 | after(async function () { 64 | this.timeout(MaxTimeout) 65 | 66 | // Restore fetch 67 | ;(fetch as unknown as any).default = originalFetch 68 | 69 | // Dispose the schema provider 70 | schemaProvider.dispose() 71 | }) 72 | 73 | test('Defaults to local schema when version not found', async () => { 74 | mockFlowVersionValue = new SemVer('0.0.0') 75 | 76 | // Assert that the schema is the same as the local schema 77 | await vscode.workspace.fs.readFile(vscode.Uri.parse('cadence-schema:///flow.json')).then((data) => { 78 | assert.strictEqual(data.toString(), readFileSync(path.resolve(extensionPath, './flow-schema.json'), 'utf-8')) 79 | }) 80 | }).timeout(MaxTimeout) 81 | 82 | test('Defaults to local schema when version is invalid', async () => { 83 | mockFlowVersionValue = null 84 | 85 | // Assert that the schema is the same as the local schema 86 | await vscode.workspace.fs.readFile(vscode.Uri.parse('cadence-schema:///flow.json')).then((data) => { 87 | assert.strictEqual(data.toString(), readFileSync(path.resolve(extensionPath, './flow-schema.json'), 'utf-8')) 88 | }) 89 | }).timeout(MaxTimeout) 90 | 91 | test('Fetches remote schema for current CLI version', async () => { 92 | mockFlowVersionValue = new SemVer('1.0.0') 93 | 94 | // Assert that the schema is the same as the remote schema 95 | await vscode.workspace.fs.readFile(vscode.Uri.parse('cadence-schema:///flow.json')).then((data) => { 96 | assert.strictEqual(data.toString(), JSON.stringify({ 97 | dummy: 'schema for flow.json' 98 | })) 99 | }) 100 | }).timeout(MaxTimeout) 101 | }) 102 | -------------------------------------------------------------------------------- /extension/test/integration/4 - flow-config.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import { BehaviorSubject, firstValueFrom } from 'rxjs' 3 | import { FlowConfig } from '../../src/server/flow-config' 4 | import { CadenceConfiguration, Settings } from '../../src/settings/settings' 5 | import { before, afterEach } from 'mocha' 6 | import * as assert from 'assert' 7 | import * as path from 'path' 8 | import * as fs from 'fs' 9 | import { getMockSettings } from '../mock/mockSettings' 10 | 11 | const workspacePath = path.resolve(__dirname, './fixtures/workspace') 12 | 13 | suite('flow config tests', () => { 14 | let rootConfigPath: string 15 | let rootConfig: Buffer 16 | 17 | function deleteRootConfig (): void { 18 | fs.unlinkSync(rootConfigPath) 19 | } 20 | 21 | function restoreRootConfig (): void { 22 | fs.writeFileSync(rootConfigPath, rootConfig) 23 | } 24 | 25 | async function withConfig (mockSettings: Settings, cb: (config: FlowConfig) => void | Promise): Promise { 26 | const config = new FlowConfig(mockSettings) 27 | await config.activate() 28 | try { 29 | await cb(config) 30 | } finally { 31 | config?.dispose() 32 | } 33 | } 34 | 35 | before(() => { 36 | // cache config at root if deleted later 37 | rootConfigPath = path.resolve(workspacePath, 'flow.json') 38 | rootConfig = fs.readFileSync(rootConfigPath) 39 | }) 40 | 41 | afterEach(() => { 42 | // restore config at root 43 | restoreRootConfig() 44 | }) 45 | 46 | test('recognizes custom config path', async () => { 47 | const mockSettings = getMockSettings({ customConfigPath: './foo/flow.json' }) 48 | 49 | await withConfig(mockSettings, (config) => { 50 | assert.strictEqual(config.configPath, path.resolve(workspacePath, './foo/flow.json')) 51 | }) 52 | }) 53 | 54 | test('recognizes config path from project root', async () => { 55 | const mockSettings = getMockSettings({ customConfigPath: '' }) 56 | 57 | await withConfig(mockSettings, (config) => { 58 | assert.strictEqual(config.configPath, path.resolve(workspacePath, './flow.json')) 59 | }) 60 | }) 61 | 62 | test('recognizes custom config change & emits pathChanged$', async () => { 63 | const settings$ = new BehaviorSubject>({ customConfigPath: './foo/flow.json' }) 64 | const mockSettings = getMockSettings(settings$) 65 | 66 | await withConfig(mockSettings, async (config) => { 67 | assert.strictEqual(config.configPath, path.resolve(workspacePath, './foo/flow.json')) 68 | 69 | const pathChangedPromise = firstValueFrom(config.pathChanged$) 70 | 71 | // update custom config path 72 | settings$.next({ customConfigPath: './bar/flow.json' }) 73 | 74 | await pathChangedPromise 75 | assert.strictEqual(config.configPath, path.resolve(workspacePath, './bar/flow.json')) 76 | }) 77 | }) 78 | 79 | test('ignores non-existent custom config path', async () => { 80 | const mockSettings = getMockSettings({ customConfigPath: './missing/flow.json' }) 81 | 82 | await withConfig(mockSettings, (config) => { 83 | assert.strictEqual(config.configPath, null) 84 | }) 85 | }) 86 | 87 | test('null if no config at root or custom path', async () => { 88 | // temporarily delete config at root 89 | deleteRootConfig() 90 | 91 | const mockSettings = getMockSettings({ customConfigPath: '' }) 92 | 93 | await withConfig(mockSettings, (config) => { 94 | assert.strictEqual(config.configPath, null) 95 | }) 96 | }) 97 | 98 | test('detects config creation at root', async () => { 99 | // temporarily delete config at root 100 | deleteRootConfig() 101 | 102 | await new Promise((resolve) => { 103 | setTimeout(resolve, 1000) 104 | }) 105 | 106 | const mockSettings = getMockSettings({ customConfigPath: '' }) 107 | 108 | await withConfig(mockSettings, async (config) => { 109 | assert.strictEqual(config.configPath, null) 110 | 111 | // restore config at root 112 | restoreRootConfig() 113 | 114 | await firstValueFrom(config.pathChanged$) 115 | assert.strictEqual(config.configPath, rootConfigPath) 116 | }) 117 | }).timeout(5000) 118 | 119 | test('detects creation of previously non-existent custom config', async () => { 120 | // ensure file does not exist 121 | if (fs.existsSync(path.resolve(workspacePath, './missing/flow.json'))) { 122 | fs.unlinkSync(path.resolve(workspacePath, './missing/flow.json')) 123 | } 124 | 125 | await new Promise((resolve) => { 126 | setTimeout(resolve, 1000) 127 | }) 128 | 129 | const mockSettings = getMockSettings({ customConfigPath: './missing/flow.json' }) 130 | 131 | await withConfig(mockSettings, async (config) => { 132 | assert.strictEqual(config.configPath, null) 133 | 134 | // create custom config must create if non-existent 135 | fs.mkdirSync(path.resolve(workspacePath, './missing'), { recursive: true }) 136 | fs.writeFileSync(path.resolve(workspacePath, './missing/flow.json'), '{}') 137 | 138 | await firstValueFrom(config.pathChanged$) 139 | assert.strictEqual(config.configPath, path.resolve(workspacePath, './missing/flow.json')) 140 | }) 141 | 142 | // delete custom config after test 143 | fs.unlinkSync(path.resolve(workspacePath, './missing/flow.json')) 144 | }) 145 | }) 146 | -------------------------------------------------------------------------------- /extension/test/integration/5 - test-trie.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { TestTrie } from '../../src/test-provider/test-trie' 3 | import * as vscode from 'vscode' 4 | import * as path from 'path' 5 | import { beforeEach, afterEach } from 'mocha' 6 | 7 | interface TreeResult { 8 | id: string 9 | label: string 10 | tags: string[] 11 | range?: vscode.Range 12 | uri?: string 13 | children?: TreeResult[] 14 | } 15 | interface TreeResultComparable { 16 | id: string 17 | label: string 18 | tags: string[] 19 | range?: { start: { line: number, character: number }, end: { line: number, character: number } } 20 | uri?: string 21 | children?: TreeResultComparable[] 22 | } 23 | function expectTreeToEqual (testCollection: vscode.TestItemCollection, expected: TreeResult[]): void { 24 | function buildActualTree (testCollection: vscode.TestItemCollection): TreeResult[] { 25 | const items: TreeResult[] = [] 26 | testCollection.forEach((item) => { 27 | const children = buildActualTree(item.children) 28 | const result: TreeResult = { 29 | id: item.id, 30 | label: item.label, 31 | uri: item.uri?.fsPath, 32 | tags: item.tags?.map?.((tag) => tag.id) ?? [] 33 | } 34 | if (item.range != null) { 35 | result.range = item.range 36 | } 37 | if (children.length > 0) { 38 | result.children = children 39 | } 40 | items.push(result) 41 | }) 42 | return items 43 | } 44 | 45 | function makeComparable (tree: TreeResult[]): TreeResultComparable[] { 46 | return tree.map((item) => { 47 | const result: TreeResultComparable = { 48 | id: item.id, 49 | label: item.label, 50 | uri: item.uri, 51 | tags: item.tags 52 | } 53 | if (item.range != null) { 54 | result.range = { 55 | start: { line: item.range.start.line, character: item.range.start.character }, 56 | end: { line: item.range.end.line, character: item.range.end.character } 57 | } 58 | } 59 | if (item.children != null) { 60 | result.children = makeComparable(item.children) 61 | } 62 | return result 63 | }) 64 | } 65 | 66 | const actual = buildActualTree(testCollection) 67 | assert.deepStrictEqual(makeComparable(actual), makeComparable(expected)) 68 | } 69 | 70 | suite('test trie tests', () => { 71 | let testController: vscode.TestController 72 | let testTrie: TestTrie 73 | let workspaceFolder: vscode.WorkspaceFolder 74 | 75 | beforeEach(() => { 76 | testController = vscode.tests.createTestController('test-controller', 'test-controller') 77 | testTrie = new TestTrie(testController) 78 | 79 | if (vscode.workspace.workspaceFolders?.[0] == null) { 80 | throw new Error('No workspace folders') 81 | } 82 | workspaceFolder = vscode.workspace.workspaceFolders[0] 83 | }) 84 | 85 | afterEach(() => { 86 | testController.dispose() 87 | }) 88 | 89 | test('adds and removes files from test tree', () => { 90 | const workspaceRoot = workspaceFolder.uri.fsPath 91 | 92 | testTrie.add(path.resolve(workspaceRoot, 'test1.cdc'), [ 93 | { name: 'test1', range: new vscode.Range(0, 0, 0, 0) }, 94 | { name: 'test2', range: new vscode.Range(1, 2, 3, 4) } 95 | ]) 96 | testTrie.add(path.resolve(workspaceRoot, 'test2.cdc'), [ 97 | { name: 'test1', range: new vscode.Range(5, 6, 7, 8) }, 98 | { name: 'test2', range: new vscode.Range(4, 3, 2, 1) } 99 | ]) 100 | 101 | expectTreeToEqual(testController.items, [{ 102 | id: workspaceFolder.uri.fsPath, 103 | label: 'workspace', 104 | tags: ['cadence'], 105 | uri: workspaceFolder.uri.fsPath, 106 | children: [ 107 | { 108 | id: 'test1.cdc', 109 | label: 'test1.cdc', 110 | uri: path.resolve(workspaceRoot, 'test1.cdc'), 111 | tags: ['cadence'], 112 | children: [ 113 | { 114 | id: ':test1', 115 | label: 'test1', 116 | tags: ['cadence'], 117 | uri: path.resolve(workspaceRoot, 'test1.cdc'), 118 | range: new vscode.Range(0, 0, 0, 0) 119 | }, 120 | { 121 | id: ':test2', 122 | label: 'test2', 123 | tags: ['cadence'], 124 | uri: path.resolve(workspaceRoot, 'test1.cdc'), 125 | range: new vscode.Range(1, 2, 3, 4) 126 | } 127 | ] 128 | }, 129 | { 130 | id: 'test2.cdc', 131 | label: 'test2.cdc', 132 | uri: path.resolve(workspaceRoot, 'test2.cdc'), 133 | tags: ['cadence'], 134 | children: [ 135 | { 136 | id: ':test1', 137 | label: 'test1', 138 | tags: ['cadence'], 139 | uri: path.resolve(workspaceRoot, 'test2.cdc'), 140 | range: new vscode.Range(5, 6, 7, 8) 141 | }, 142 | { 143 | id: ':test2', 144 | label: 'test2', 145 | tags: ['cadence'], 146 | uri: path.resolve(workspaceRoot, 'test2.cdc'), 147 | range: new vscode.Range(4, 3, 2, 1) 148 | } 149 | ] 150 | } 151 | ] 152 | }]) 153 | 154 | testTrie.remove(path.resolve(workspaceRoot, 'test1.cdc')) 155 | 156 | expectTreeToEqual(testController.items, [{ 157 | id: workspaceFolder.uri.fsPath, 158 | label: 'workspace', 159 | tags: ['cadence'], 160 | uri: workspaceFolder.uri.fsPath, 161 | children: [ 162 | { 163 | id: 'test2.cdc', 164 | label: 'test2.cdc', 165 | uri: path.resolve(workspaceRoot, 'test2.cdc'), 166 | tags: ['cadence'], 167 | children: [ 168 | { 169 | id: ':test1', 170 | label: 'test1', 171 | tags: ['cadence'], 172 | uri: path.resolve(workspaceRoot, 'test2.cdc'), 173 | range: new vscode.Range(5, 6, 7, 8) 174 | }, 175 | { 176 | id: ':test2', 177 | label: 'test2', 178 | tags: ['cadence'], 179 | uri: path.resolve(workspaceRoot, 'test2.cdc'), 180 | range: new vscode.Range(4, 3, 2, 1) 181 | } 182 | ] 183 | } 184 | ] 185 | }]) 186 | }) 187 | 188 | test('adds files contained in folders to test tree', () => { 189 | const workspaceRoot = workspaceFolder.uri.fsPath 190 | 191 | testTrie.add(path.resolve(workspaceRoot, 'folder1/test.cdc'), [ 192 | { name: 'test', range: new vscode.Range(12, 11, 10, 9) } 193 | ]) 194 | testTrie.add(path.resolve(workspaceRoot, 'test2.cdc'), [ 195 | { name: 'test1', range: new vscode.Range(5, 6, 7, 8) } 196 | ]) 197 | 198 | expectTreeToEqual(testController.items, [{ 199 | id: workspaceFolder.uri.fsPath, 200 | label: 'workspace', 201 | tags: ['cadence'], 202 | uri: workspaceFolder.uri.fsPath, 203 | children: [ 204 | { 205 | id: 'folder1', 206 | label: 'folder1', 207 | uri: path.resolve(workspaceRoot, 'folder1'), 208 | tags: ['cadence'], 209 | children: [ 210 | { 211 | id: 'test.cdc', 212 | label: 'test.cdc', 213 | uri: path.resolve(workspaceRoot, 'folder1/test.cdc'), 214 | tags: ['cadence'], 215 | children: [ 216 | { 217 | id: ':test', 218 | label: 'test', 219 | tags: ['cadence'], 220 | uri: path.resolve(workspaceRoot, 'folder1/test.cdc'), 221 | range: new vscode.Range(12, 11, 10, 9) 222 | } 223 | ] 224 | } 225 | ] 226 | }, 227 | { 228 | id: 'test2.cdc', 229 | label: 'test2.cdc', 230 | uri: path.resolve(workspaceRoot, 'test2.cdc'), 231 | tags: ['cadence'], 232 | children: [ 233 | { 234 | id: ':test1', 235 | label: 'test1', 236 | tags: ['cadence'], 237 | uri: path.resolve(workspaceRoot, 'test2.cdc'), 238 | range: new vscode.Range(5, 6, 7, 8) 239 | } 240 | ] 241 | } 242 | ] 243 | }]) 244 | }) 245 | 246 | test('removing last test from folder cleans up all parent items', () => { 247 | const workspaceRoot = workspaceFolder.uri.fsPath 248 | 249 | testTrie.add(path.resolve(workspaceRoot, 'folder1/abc/test4.cdc'), [ 250 | { name: 'test2', range: new vscode.Range(12, 11, 10, 9) } 251 | ]) 252 | 253 | testTrie.remove(path.resolve(workspaceRoot, 'folder1/abc/test4.cdc')) 254 | 255 | expectTreeToEqual(testController.items, [{ 256 | id: workspaceFolder.uri.fsPath, 257 | label: 'workspace', 258 | tags: ['cadence'], 259 | uri: workspaceFolder.uri.fsPath 260 | }]) 261 | }) 262 | 263 | test('gets file from test trie', () => { 264 | const workspaceRoot = workspaceFolder.uri.fsPath 265 | 266 | testTrie.add(path.resolve(workspaceRoot, 'foo/test1.cdc'), [ 267 | { name: 'test1', range: new vscode.Range(1, 2, 3, 4) } 268 | ]) 269 | 270 | const testItem = testTrie.get(path.resolve(workspaceRoot, 'foo/test1.cdc')) 271 | if (testItem == null) throw new Error('testItem is null') 272 | 273 | assert.strictEqual(testItem.id, 'test1.cdc') 274 | assert.strictEqual(testItem.label, 'test1.cdc') 275 | assert.strictEqual(testItem.uri?.fsPath, path.resolve(workspaceRoot, 'foo/test1.cdc')) 276 | expectTreeToEqual(testItem.children, [{ 277 | id: ':test1', 278 | label: 'test1', 279 | tags: ['cadence'], 280 | uri: path.resolve(workspaceRoot, 'foo/test1.cdc'), 281 | range: new vscode.Range(1, 2, 3, 4) 282 | }]) 283 | }) 284 | 285 | test('gets folder from test trie', () => { 286 | const workspaceRoot = workspaceFolder.uri.fsPath 287 | 288 | testTrie.add(path.resolve(workspaceRoot, 'foo/test1.cdc'), [ 289 | { name: 'test1', range: new vscode.Range(1, 2, 3, 4) } 290 | ]) 291 | 292 | const testItem = testTrie.get(path.resolve(workspaceRoot, 'foo')) 293 | if (testItem == null) throw new Error('testItem is null') 294 | 295 | assert.strictEqual(testItem.id, 'foo') 296 | assert.strictEqual(testItem.label, 'foo') 297 | assert.strictEqual(testItem.uri?.fsPath, path.resolve(workspaceRoot, 'foo')) 298 | expectTreeToEqual(testItem.children, [{ 299 | id: 'test1.cdc', 300 | label: 'test1.cdc', 301 | uri: path.resolve(workspaceRoot, 'foo/test1.cdc'), 302 | tags: ['cadence'], 303 | children: [ 304 | { 305 | id: ':test1', 306 | label: 'test1', 307 | tags: ['cadence'], 308 | uri: path.resolve(workspaceRoot, 'foo/test1.cdc'), 309 | range: new vscode.Range(1, 2, 3, 4) 310 | } 311 | ] 312 | }]) 313 | }) 314 | }) 315 | -------------------------------------------------------------------------------- /extension/test/integration/6 - test-provider.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, afterEach } from 'mocha' 2 | import { TestProvider } from '../../src/test-provider/test-provider' 3 | import { Settings } from '../../src/settings/settings' 4 | import { FlowConfig } from '../../src/server/flow-config' 5 | import { of } from 'rxjs' 6 | import * as path from 'path' 7 | import * as vscode from 'vscode' 8 | import * as sinon from 'sinon' 9 | import * as assert from 'assert' 10 | import * as fs from 'fs' 11 | import { getMockSettings } from '../mock/mockSettings' 12 | 13 | const workspacePath = path.resolve(__dirname, './fixtures/workspace') 14 | 15 | suite('test provider tests', () => { 16 | let mockSettings: Settings 17 | let mockConfig: FlowConfig 18 | let testProvider: TestProvider 19 | let cleanupFunctions: Array<() => void | Promise> = [] 20 | 21 | beforeEach(async function () { 22 | this.timeout(5000) 23 | 24 | const parserLocation = path.resolve(__dirname, '../../../../node_modules/@onflow/cadence-parser/dist/cadence-parser.wasm') 25 | 26 | mockSettings = getMockSettings({ 27 | flowCommand: 'flow', 28 | test: { 29 | maxConcurrency: 1 30 | } 31 | }) 32 | mockConfig = { 33 | fileModified$: of(), 34 | pathChanged$: of(), 35 | configPath: path.join(workspacePath, 'flow.json') 36 | } as any 37 | 38 | testProvider = new TestProvider(parserLocation, mockSettings, mockConfig) 39 | 40 | // Wait for test provider to initialize 41 | await new Promise((resolve) => setTimeout(resolve, 2500)) 42 | }) 43 | 44 | afterEach(async function () { 45 | this.timeout(5000) 46 | 47 | testProvider.dispose() 48 | for (const cleanupFunction of cleanupFunctions) { 49 | await cleanupFunction() 50 | } 51 | cleanupFunctions = [] 52 | }) 53 | 54 | test('runs all tests in workspace and reports results', async function () { 55 | let runSpy: sinon.SinonSpiedInstance | undefined 56 | 57 | await new Promise(resolve => { 58 | void testProvider.runAllTests(undefined, (testRun) => { 59 | const originalEnd = testRun.end 60 | testRun.end = () => { 61 | originalEnd.call(testRun) 62 | resolve() 63 | } 64 | 65 | runSpy = sinon.spy(testRun) 66 | return runSpy 67 | }) 68 | }) 69 | 70 | if (runSpy == null) throw new Error('runSpy is null') 71 | 72 | const passedTests = runSpy.passed.getCalls().map(call => ({ filepath: (call.args[0].uri as vscode.Uri).fsPath, id: call.args[0].id })) 73 | const failedTests = runSpy.failed.getCalls().map(call => ({ filepath: (call.args[0].uri as vscode.Uri).fsPath, id: call.args[0].id, message: (call.args[1] as any).message })) 74 | 75 | passedTests.sort((a, b) => a.filepath.localeCompare(b.filepath)) 76 | failedTests.sort((a, b) => a.filepath.localeCompare(b.filepath)) 77 | 78 | assert.strictEqual(passedTests.length + failedTests.length, 5) 79 | assert.deepStrictEqual(passedTests, [ 80 | { filepath: path.join(workspacePath, 'test/bar/test2.cdc'), id: ':testPassing' }, 81 | { filepath: path.join(workspacePath, 'test/bar/test3.cdc'), id: ':testPassing' }, 82 | { filepath: path.join(workspacePath, 'test/test1.cdc'), id: ':testPassing' } 83 | ]) 84 | assert.deepStrictEqual(failedTests, [ 85 | { filepath: path.join(workspacePath, 'test/bar/test2.cdc'), id: ':testFailing', message: 'FAIL: Execution failed:\nerror: assertion failed\n --> 7465737400000000000000000000000000000000000000000000000000000000:8:2\n' }, 86 | { filepath: path.join(workspacePath, 'test/bar/test3.cdc'), id: ':testFailing', message: 'FAIL: Execution failed:\nerror: assertion failed\n --> 7465737400000000000000000000000000000000000000000000000000000000:4:2\n' } 87 | ]) 88 | }).timeout(20000) 89 | 90 | test('runs individual test and reports results', async function () { 91 | let runSpy: sinon.SinonSpiedInstance | undefined 92 | 93 | await new Promise(resolve => { 94 | void testProvider.runIndividualTest(path.join(workspacePath, 'test/test1.cdc'), undefined, (testRun) => { 95 | const originalEnd = testRun.end 96 | testRun.end = () => { 97 | originalEnd.call(testRun) 98 | resolve() 99 | } 100 | 101 | runSpy = sinon.spy(testRun) 102 | return runSpy 103 | }) 104 | }) 105 | 106 | if (runSpy == null) throw new Error('runSpy is null') 107 | 108 | const passedTests = runSpy.passed.getCalls().map(call => ({ filepath: (call.args[0].uri as vscode.Uri).fsPath, id: call.args[0].id })) 109 | const failedTests = runSpy.failed.getCalls().map(call => ({ filepath: (call.args[0].uri as vscode.Uri).fsPath, id: call.args[0].id, message: (call.args[1] as any).message })) 110 | 111 | passedTests.sort((a, b) => a.filepath.localeCompare(b.filepath)) 112 | failedTests.sort((a, b) => a.filepath.localeCompare(b.filepath)) 113 | 114 | assert.strictEqual(passedTests.length + failedTests.length, 1) 115 | assert.deepStrictEqual(passedTests, [ 116 | { filepath: path.join(workspacePath, 'test/test1.cdc'), id: ':testPassing' } 117 | ]) 118 | assert.deepStrictEqual(failedTests, []) 119 | }).timeout(20000) 120 | 121 | test('runs tests including newly created file', async function () { 122 | // Create new file 123 | const testFilePath = path.join(workspacePath, 'test/bar/test4.cdc') 124 | const testFileContents = ` 125 | import Test 126 | access(all) fun testPassing() { 127 | Test.assert(true) 128 | } 129 | ` 130 | fs.writeFileSync(testFilePath, testFileContents) 131 | cleanupFunctions.push(async () => { 132 | fs.rmSync(testFilePath) 133 | }) 134 | await new Promise(resolve => setTimeout(resolve, 1000)) 135 | 136 | // Run tests 137 | let runSpy: sinon.SinonSpiedInstance | undefined 138 | await new Promise(resolve => { 139 | void testProvider.runAllTests(undefined, (testRun) => { 140 | const originalEnd = testRun.end 141 | testRun.end = () => { 142 | originalEnd.call(testRun) 143 | resolve() 144 | } 145 | 146 | runSpy = sinon.spy(testRun) 147 | return runSpy 148 | }) 149 | }) 150 | if (runSpy == null) throw new Error('runSpy is null') 151 | 152 | const passedTests = runSpy.passed.getCalls().map(call => ({ filepath: (call.args[0].uri as vscode.Uri).fsPath, id: call.args[0].id })) 153 | const failedTests = runSpy.failed.getCalls().map(call => ({ filepath: (call.args[0].uri as vscode.Uri).fsPath, id: call.args[0].id, message: (call.args[1] as any).message })) 154 | 155 | passedTests.sort((a, b) => a.filepath.localeCompare(b.filepath)) 156 | failedTests.sort((a, b) => a.filepath.localeCompare(b.filepath)) 157 | 158 | assert.strictEqual(passedTests.length + failedTests.length, 6) 159 | assert.deepStrictEqual(passedTests, [ 160 | { filepath: path.join(workspacePath, 'test/bar/test2.cdc'), id: ':testPassing' }, 161 | { filepath: path.join(workspacePath, 'test/bar/test3.cdc'), id: ':testPassing' }, 162 | { filepath: path.join(workspacePath, 'test/bar/test4.cdc'), id: ':testPassing' }, 163 | { filepath: path.join(workspacePath, 'test/test1.cdc'), id: ':testPassing' } 164 | ]) 165 | assert.deepStrictEqual(failedTests, [ 166 | { filepath: path.join(workspacePath, 'test/bar/test2.cdc'), id: ':testFailing', message: 'FAIL: Execution failed:\nerror: assertion failed\n --> 7465737400000000000000000000000000000000000000000000000000000000:8:2\n' }, 167 | { filepath: path.join(workspacePath, 'test/bar/test3.cdc'), id: ':testFailing', message: 'FAIL: Execution failed:\nerror: assertion failed\n --> 7465737400000000000000000000000000000000000000000000000000000000:4:2\n' } 168 | ]) 169 | }).timeout(20000) 170 | 171 | test('runs tests including newly deleted file', async function () { 172 | // Delete test file 173 | const testFilePath = path.join(workspacePath, 'test/bar/test3.cdc') 174 | const originalContents = fs.readFileSync(testFilePath) 175 | fs.rmSync(testFilePath) 176 | 177 | cleanupFunctions.push(async () => { 178 | fs.writeFileSync(testFilePath, originalContents) 179 | }) 180 | await new Promise(resolve => setTimeout(resolve, 1000)) 181 | 182 | // Run tests 183 | let runSpy: sinon.SinonSpiedInstance | undefined 184 | await new Promise(resolve => { 185 | void testProvider.runAllTests(undefined, (testRun) => { 186 | const originalEnd = testRun.end 187 | testRun.end = () => { 188 | originalEnd.call(testRun) 189 | resolve() 190 | } 191 | 192 | runSpy = sinon.spy(testRun) 193 | return runSpy 194 | }) 195 | }) 196 | if (runSpy == null) throw new Error('runSpy is null') 197 | 198 | const passedTests = runSpy.passed.getCalls().map(call => ({ filepath: (call.args[0].uri as vscode.Uri).fsPath, id: call.args[0].id })) 199 | const failedTests = runSpy.failed.getCalls().map(call => ({ filepath: (call.args[0].uri as vscode.Uri).fsPath, id: call.args[0].id, message: (call.args[1] as any).message })) 200 | 201 | passedTests.sort((a, b) => a.filepath.localeCompare(b.filepath)) 202 | failedTests.sort((a, b) => a.filepath.localeCompare(b.filepath)) 203 | 204 | assert.strictEqual(passedTests.length + failedTests.length, 3) 205 | assert.deepStrictEqual(passedTests, [ 206 | { filepath: path.join(workspacePath, 'test/bar/test2.cdc'), id: ':testPassing' }, 207 | { filepath: path.join(workspacePath, 'test/test1.cdc'), id: ':testPassing' } 208 | ]) 209 | assert.deepStrictEqual(failedTests, [ 210 | { filepath: path.join(workspacePath, 'test/bar/test2.cdc'), id: ':testFailing', message: 'FAIL: Execution failed:\nerror: assertion failed\n --> 7465737400000000000000000000000000000000000000000000000000000000:8:2\n' } 211 | ]) 212 | }).timeout(20000) 213 | }) 214 | -------------------------------------------------------------------------------- /extension/test/mock/mockSettings.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject, Observable, of, map, distinctUntilChanged } from 'rxjs' 2 | import { CadenceConfiguration, Settings } from '../../src/settings/settings' 3 | import * as path from 'path' 4 | import { isEqual } from 'lodash' 5 | 6 | export function getMockSettings (_settings$: BehaviorSubject> | Partial | null = null): Settings { 7 | const mockSettings: Settings = { getSettings, watch$ } as any 8 | 9 | function getSettings (): Partial { 10 | if (!(_settings$ instanceof BehaviorSubject) && _settings$ != null) return _settings$ 11 | 12 | return _settings$?.getValue() ?? { 13 | flowCommand: 'flow', 14 | accessCheckMode: 'strict', 15 | customConfigPath: path.join(__dirname, '../integration/fixtures/workspace/flow.json'), 16 | test: { 17 | maxConcurrency: 1 18 | } 19 | } 20 | } 21 | 22 | function watch$ (selector: (config: CadenceConfiguration) => T = (config) => config as unknown as T): Observable { 23 | if (!(_settings$ instanceof BehaviorSubject)) return of() 24 | 25 | return _settings$.asObservable().pipe( 26 | map(selector as any), 27 | distinctUntilChanged(isEqual) 28 | ) 29 | } 30 | return mockSettings 31 | } 32 | -------------------------------------------------------------------------------- /extension/test/run-tests.ts: -------------------------------------------------------------------------------- 1 | /* Run integration tests */ 2 | import * as path from 'path' 3 | import { runTests } from '@vscode/test-electron' 4 | 5 | async function main (): Promise { 6 | try { 7 | const extensionDevelopmentPath = path.resolve(__dirname, '../src/') 8 | const extensionTestsPath = path.resolve(__dirname, './index.js') 9 | const testWorkspace = path.resolve(__dirname, './integration/fixtures/workspace') 10 | 11 | // Download VS Code, unzip it and run the integration test 12 | await runTests({ 13 | extensionDevelopmentPath, 14 | extensionTestsPath, 15 | launchArgs: [testWorkspace, '--disable-telemetry', '--disable-gpu', '--disable-software-rasterizer'] 16 | }) 17 | } catch (err) { 18 | console.error('Failed to run tests') 19 | process.exit(1) 20 | } 21 | } 22 | 23 | main().then(() => {}, () => {}) 24 | -------------------------------------------------------------------------------- /extension/test/unit/parser.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { parseFlowCliVersion } from '../../src/flow-cli/cli-versions-provider' 3 | import { execDefault } from '../../src/utils/shell/exec' 4 | import { ASSERT_EQUAL } from '../globals' 5 | import * as semver from 'semver' 6 | 7 | suite('Parsing Unit Tests', () => { 8 | test('Flow CLI Version Parsing (buffer input)', async () => { 9 | let versionTest: Buffer = Buffer.from('Version: v0.1.0\nCommit: 0a1b2c3d') 10 | let formatted = parseFlowCliVersion(versionTest) 11 | ASSERT_EQUAL(formatted, '0.1.0') 12 | 13 | versionTest = Buffer.from('Version: v0.1.0') 14 | formatted = parseFlowCliVersion(versionTest) 15 | ASSERT_EQUAL(formatted, '0.1.0') 16 | }) 17 | 18 | test('Flow CLI Version Parsing (string input)', async () => { 19 | const versionTest: string = 'Version: v0.1.0\nCommit: 0a1b2c3d' 20 | const formatted = parseFlowCliVersion(versionTest) 21 | ASSERT_EQUAL(formatted, '0.1.0') 22 | }) 23 | 24 | test('Flow CLI Version Parsing produces valid semver from Flow CLI output', async () => { 25 | // Check that version is parsed from currently installed flow-cli 26 | const { stdout } = await execDefault('flow version') 27 | const formatted = parseFlowCliVersion(stdout) 28 | assert(semver.valid(formatted)) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /extension/test/unit/state-cache.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { StateCache } from '../../src/utils/state-cache' 3 | import { ASSERT_EQUAL } from '../globals' 4 | 5 | suite('State Cache Unit Tests', () => { 6 | test('Returns value when valid', async () => { 7 | const fetcher = async (): Promise => { 8 | return val 9 | } 10 | const val = 0 11 | const stateCache = new StateCache(fetcher) 12 | 13 | ASSERT_EQUAL(await stateCache.getValue(), 0) 14 | }) 15 | 16 | test('Returns value when invalid', async () => { 17 | const fetcher = async (): Promise => { 18 | return val 19 | } 20 | let val = 0 21 | const stateCache = new StateCache(fetcher) 22 | 23 | val += 1 24 | stateCache.invalidate() 25 | 26 | ASSERT_EQUAL(await stateCache.getValue(), 1) 27 | }) 28 | 29 | test('Returns value when invalid and fetching', async () => { 30 | const fetcher = async (): Promise => { 31 | return val 32 | } 33 | let val = 0 34 | const stateCache = new StateCache(fetcher) 35 | 36 | val += 1 37 | stateCache.invalidate() 38 | 39 | val += 1 40 | stateCache.invalidate() 41 | 42 | ASSERT_EQUAL(await stateCache.getValue(), 2) 43 | }) 44 | 45 | test('Multiple invalidation calls will bubble', async () => { 46 | const fetcher = async (): Promise => { 47 | fetcherCallCount++ 48 | const returnVal = val 49 | return await new Promise((resolve) => { 50 | setTimeout(() => { 51 | resolve(returnVal) 52 | }, 1000) 53 | }) 54 | } 55 | let fetcherCallCount = 0 56 | let val = 0 57 | const stateCache = new StateCache(fetcher) 58 | 59 | val += 1 60 | stateCache.invalidate() 61 | 62 | val += 1 63 | stateCache.invalidate() 64 | 65 | val += 1 66 | stateCache.invalidate() 67 | 68 | ASSERT_EQUAL(await stateCache.getValue(), 3) 69 | ASSERT_EQUAL(fetcherCallCount, 2) 70 | }).timeout(5000) 71 | 72 | test('Failed fetch generates error', async () => { 73 | const fetcher = async (): Promise => { 74 | throw new Error('dummy error') 75 | } 76 | const stateCache = new StateCache(fetcher) 77 | 78 | await assert.rejects(async () => { await stateCache.getValue() }, { message: 'dummy error' }) 79 | }).timeout(2000) 80 | 81 | test('Does not return undefined for subscription to first value', async () => { 82 | const fetcher = async (): Promise => { 83 | return 'FOOBAR' 84 | } 85 | const stateCache = new StateCache(fetcher) 86 | 87 | ASSERT_EQUAL(await new Promise((resolve) => { 88 | const subscription = stateCache.subscribe(val => { 89 | resolve(val) 90 | subscription.unsubscribe() 91 | }) 92 | }), 'FOOBAR') 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /extension/test/unit/test-trie.test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onflow/vscode-cadence/4351a8051a99d8df69030a68d513b43a7f77222c/extension/test/unit/test-trie.test.ts -------------------------------------------------------------------------------- /flow-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://github.com/onflow/flow-cli/flowkit/config/json/json-config", 4 | "$ref": "#/$defs/jsonConfig", 5 | "$defs": { 6 | "account": { 7 | "oneOf": [ 8 | { 9 | "$ref": "#/$defs/simpleAccount" 10 | }, 11 | { 12 | "$ref": "#/$defs/advancedAccount" 13 | }, 14 | { 15 | "$ref": "#/$defs/simpleAccountPre022" 16 | }, 17 | { 18 | "$ref": "#/$defs/advanceAccountPre022" 19 | } 20 | ] 21 | }, 22 | "advanceAccountPre022": { 23 | "properties": { 24 | "address": { 25 | "type": "string" 26 | }, 27 | "keys": { 28 | "items": { 29 | "$ref": "#/$defs/advanceKey" 30 | }, 31 | "type": "array" 32 | } 33 | }, 34 | "additionalProperties": false, 35 | "type": "object", 36 | "required": [ 37 | "address", 38 | "keys" 39 | ] 40 | }, 41 | "advanceKey": { 42 | "properties": { 43 | "type": { 44 | "type": "string" 45 | }, 46 | "index": { 47 | "type": "integer" 48 | }, 49 | "signatureAlgorithm": { 50 | "type": "string" 51 | }, 52 | "hashAlgorithm": { 53 | "type": "string" 54 | }, 55 | "privateKey": { 56 | "type": "string" 57 | }, 58 | "mnemonic": { 59 | "type": "string" 60 | }, 61 | "derivationPath": { 62 | "type": "string" 63 | }, 64 | "resourceID": { 65 | "type": "string" 66 | }, 67 | "location": { 68 | "type": "string" 69 | }, 70 | "context": { 71 | "patternProperties": { 72 | ".*": { 73 | "type": "string" 74 | } 75 | }, 76 | "type": "object" 77 | } 78 | }, 79 | "additionalProperties": false, 80 | "type": "object", 81 | "required": [ 82 | "type" 83 | ] 84 | }, 85 | "advancedAccount": { 86 | "properties": { 87 | "address": { 88 | "type": "string" 89 | }, 90 | "key": { 91 | "$ref": "#/$defs/advanceKey" 92 | } 93 | }, 94 | "additionalProperties": false, 95 | "type": "object", 96 | "required": [ 97 | "address", 98 | "key" 99 | ] 100 | }, 101 | "advancedNetwork": { 102 | "properties": { 103 | "host": { 104 | "type": "string" 105 | }, 106 | "key": { 107 | "type": "string" 108 | } 109 | }, 110 | "additionalProperties": false, 111 | "type": "object", 112 | "required": [ 113 | "host", 114 | "key" 115 | ] 116 | }, 117 | "contractDeployment": { 118 | "properties": { 119 | "name": { 120 | "type": "string" 121 | }, 122 | "args": { 123 | "items": { 124 | "type": "object" 125 | }, 126 | "type": "array" 127 | } 128 | }, 129 | "additionalProperties": false, 130 | "type": "object", 131 | "required": [ 132 | "name", 133 | "args" 134 | ] 135 | }, 136 | "deployment": { 137 | "oneOf": [ 138 | { 139 | "type": "string" 140 | }, 141 | { 142 | "$ref": "#/$defs/contractDeployment" 143 | } 144 | ] 145 | }, 146 | "jsonAccounts": { 147 | "patternProperties": { 148 | ".*": { 149 | "$ref": "#/$defs/account" 150 | } 151 | }, 152 | "type": "object" 153 | }, 154 | "jsonConfig": { 155 | "properties": { 156 | "emulators": { 157 | "$ref": "#/$defs/jsonEmulators" 158 | }, 159 | "contracts": { 160 | "$ref": "#/$defs/jsonContracts" 161 | }, 162 | "networks": { 163 | "$ref": "#/$defs/jsonNetworks" 164 | }, 165 | "accounts": { 166 | "$ref": "#/$defs/jsonAccounts" 167 | }, 168 | "deployments": { 169 | "$ref": "#/$defs/jsonDeployments" 170 | } 171 | }, 172 | "additionalProperties": false, 173 | "type": "object" 174 | }, 175 | "jsonContract": { 176 | "oneOf": [ 177 | { 178 | "type": "string" 179 | }, 180 | { 181 | "$ref": "#/$defs/jsonContractAdvanced" 182 | } 183 | ] 184 | }, 185 | "jsonContractAdvanced": { 186 | "properties": { 187 | "source": { 188 | "type": "string" 189 | }, 190 | "aliases": { 191 | "patternProperties": { 192 | ".*": { 193 | "type": "string" 194 | } 195 | }, 196 | "type": "object" 197 | } 198 | }, 199 | "additionalProperties": false, 200 | "type": "object", 201 | "required": [ 202 | "source", 203 | "aliases" 204 | ] 205 | }, 206 | "jsonContracts": { 207 | "patternProperties": { 208 | ".*": { 209 | "$ref": "#/$defs/jsonContract" 210 | } 211 | }, 212 | "type": "object" 213 | }, 214 | "jsonDeployment": { 215 | "patternProperties": { 216 | ".*": { 217 | "items": { 218 | "$ref": "#/$defs/deployment" 219 | }, 220 | "type": "array" 221 | } 222 | }, 223 | "type": "object" 224 | }, 225 | "jsonDeployments": { 226 | "patternProperties": { 227 | ".*": { 228 | "$ref": "#/$defs/jsonDeployment" 229 | } 230 | }, 231 | "type": "object" 232 | }, 233 | "jsonEmulator": { 234 | "properties": { 235 | "port": { 236 | "type": "integer" 237 | }, 238 | "serviceAccount": { 239 | "type": "string" 240 | } 241 | }, 242 | "additionalProperties": false, 243 | "type": "object", 244 | "required": [ 245 | "port", 246 | "serviceAccount" 247 | ] 248 | }, 249 | "jsonEmulators": { 250 | "patternProperties": { 251 | ".*": { 252 | "$ref": "#/$defs/jsonEmulator" 253 | } 254 | }, 255 | "type": "object" 256 | }, 257 | "jsonNetwork": { 258 | "oneOf": [ 259 | { 260 | "$ref": "#/$defs/simpleNetwork" 261 | }, 262 | { 263 | "$ref": "#/$defs/advancedNetwork" 264 | } 265 | ] 266 | }, 267 | "jsonNetworks": { 268 | "patternProperties": { 269 | ".*": { 270 | "$ref": "#/$defs/jsonNetwork" 271 | } 272 | }, 273 | "type": "object" 274 | }, 275 | "simpleAccount": { 276 | "properties": { 277 | "address": { 278 | "type": "string" 279 | }, 280 | "key": { 281 | "type": "string" 282 | } 283 | }, 284 | "additionalProperties": false, 285 | "type": "object", 286 | "required": [ 287 | "address", 288 | "key" 289 | ] 290 | }, 291 | "simpleAccountPre022": { 292 | "properties": { 293 | "address": { 294 | "type": "string" 295 | }, 296 | "keys": { 297 | "type": "string" 298 | } 299 | }, 300 | "additionalProperties": false, 301 | "type": "object", 302 | "required": [ 303 | "address", 304 | "keys" 305 | ] 306 | }, 307 | "simpleNetwork": { 308 | "type": "string" 309 | } 310 | } 311 | } -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onflow/vscode-cadence/4351a8051a99d8df69030a68d513b43a7f77222c/images/icon.png -------------------------------------------------------------------------------- /images/vscode-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onflow/vscode-cadence/4351a8051a99d8df69030a68d513b43a7f77222c/images/vscode-banner.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cadence", 3 | "displayName": "Cadence", 4 | "publisher": "onflow", 5 | "description": "This extension integrates Cadence, the resource-oriented smart contract programming language of Flow, into Visual Studio Code.", 6 | "version": "2.4.1", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/onflow/vscode-cadence.git" 10 | }, 11 | "scripts": { 12 | "vscode:prepublish": "npm run -S esbuild-base -- --minify", 13 | "esbuild-base": "mkdirp ./out/extension && cp ./node_modules/@onflow/cadence-parser/dist/cadence-parser.wasm ./out/extension && esbuild ./extension/src/main.ts --bundle --outfile=out/extension/src/main.js --external:vscode --format=cjs --platform=node", 14 | "esbuild": "npm run -S esbuild-base -- --sourcemap", 15 | "esbuild-watch": "npm run -S esbuild-base -- --sourcemap --watch", 16 | "check": "tsc extension/src/main.ts", 17 | "copy-test-fixtures": "rimraf ./out/extension/test/integration/fixtures && cp -R ./extension/test/fixtures ./out/extension/test/integration/fixtures", 18 | "clean-test": "rimraf ./out", 19 | "test": "npm run clean-test && npm run esbuild && tsc -p ./ && npm run copy-test-fixtures && node ./out/extension/test/run-tests.js", 20 | "package": "vsce package", 21 | "install-extension": "code --install-extension cadence-*.vsix", 22 | "package-test": "vsce package --out ./extension/test/fixtures/workspace/cadence.vsix", 23 | "lint": "ts-standard", 24 | "lint-fix": "ts-standard --fix" 25 | }, 26 | "engines": { 27 | "vscode": "^1.82.0" 28 | }, 29 | "categories": [ 30 | "Programming Languages" 31 | ], 32 | "icon": "images/icon.png", 33 | "activationEvents": [ 34 | "onLanguage:cadence", 35 | "onFileSystem:cadence-schema" 36 | ], 37 | "main": "./out/extension/src/main.js", 38 | "contributes": { 39 | "breakpoints": [ 40 | { 41 | "language": "cadence" 42 | } 43 | ], 44 | "debuggers": [ 45 | { 46 | "type": "cadence", 47 | "languages": [ 48 | "cadence" 49 | ], 50 | "label": "Cadence Debug", 51 | "configurationAttributes": { 52 | "launch": { 53 | "required": [ 54 | "program" 55 | ], 56 | "properties": { 57 | "program": { 58 | "type": "string", 59 | "description": "Absolute path to a file.", 60 | "default": "${file}" 61 | }, 62 | "stopOnEntry": { 63 | "type": "boolean", 64 | "description": "Automatically stop after launch.", 65 | "default": true 66 | } 67 | } 68 | }, 69 | "attach": { 70 | "required": [], 71 | "properties": { 72 | "stopOnEntry": { 73 | "type": "boolean", 74 | "description": "Automatically stop after attach.", 75 | "default": true 76 | } 77 | } 78 | }, 79 | "initialConfigurations": [ 80 | { 81 | "type": "cadence", 82 | "request": "launch", 83 | "name": "Curent file", 84 | "program": "${file}", 85 | "stopOnEntry": true 86 | } 87 | ] 88 | } 89 | } 90 | ], 91 | "commands": [ 92 | { 93 | "command": "cadence.restartServer", 94 | "category": "Cadence", 95 | "title": "Restart language server" 96 | }, 97 | { 98 | "command": "cadence.checkDepencencies", 99 | "category": "Cadence", 100 | "title": "Check Dependencies" 101 | }, 102 | { 103 | "command": "cadence.changeFlowCliBinary", 104 | "category": "Cadence", 105 | "title": "Change Flow CLI Binary" 106 | } 107 | ], 108 | "configuration": { 109 | "title": "Cadence", 110 | "properties": { 111 | "cadence.flowCommand": { 112 | "type": "string", 113 | "default": "flow", 114 | "description": "The command to invoke the Flow CLI.", 115 | "scope": "resource" 116 | }, 117 | "cadence.accessCheckMode": { 118 | "type": "string", 119 | "default": "strict", 120 | "enum": [ 121 | "strict", 122 | "notSpecifiedRestricted", 123 | "notSpecifiedUnrestricted", 124 | "none" 125 | ], 126 | "enumDescriptions": [ 127 | "Access modifiers are required and always enforced", 128 | "Access modifiers are optional. Access is assumed private if not specified", 129 | "Access modifiers are optional. Access is assumed public if not specified", 130 | "Access modifiers are optional and ignored" 131 | ], 132 | "description": "Configures if access modifiers are required and how they are are enforced.", 133 | "scope": "resource" 134 | }, 135 | "cadence.customConfigPath": { 136 | "type": "string", 137 | "default": "", 138 | "description": "Enter a custom flow.json path, or leave empty for the default config search.", 139 | "scope": "resource" 140 | }, 141 | "cadence.test.maxConcurrency": { 142 | "type": "number", 143 | "default": "5", 144 | "description": "The maximum number of test files that can be run concurrently.", 145 | "scope": "resource" 146 | } 147 | } 148 | }, 149 | "languages": [ 150 | { 151 | "id": "cadence", 152 | "extensions": [ 153 | ".cdc" 154 | ], 155 | "icon": { 156 | "light": "./images/icon.png", 157 | "dark": "./images/icon.png" 158 | }, 159 | "configuration": "./extension/language/language-configuration.json" 160 | } 161 | ], 162 | "grammars": [ 163 | { 164 | "language": "cadence", 165 | "scopeName": "source.cadence", 166 | "path": "./extension/language/syntaxes/cadence.tmGrammar.json" 167 | }, 168 | { 169 | "scopeName": "markdown.cadence.codeblock", 170 | "path": "./extension/language/syntaxes/codeblock.json", 171 | "injectTo": [ 172 | "text.html.markdown" 173 | ], 174 | "embeddedLanguages": { 175 | "meta.embedded.block.cadence": "cadence" 176 | } 177 | } 178 | ], 179 | "jsonValidation": [ 180 | { 181 | "fileMatch": "flow.json", 182 | "url": "cadence-schema:///flow.json" 183 | } 184 | ] 185 | }, 186 | "devDependencies": { 187 | "@types/chai": "^5.0.1", 188 | "@types/expect": "^24.3.2", 189 | "@types/glob": "^8.0.1", 190 | "@types/lodash": "^4.17.13", 191 | "@types/mixpanel": "^2.14.9", 192 | "@types/mocha": "^10.0.9", 193 | "@types/node": "^22.10.1", 194 | "@types/object-hash": "^3.0.6", 195 | "@types/semver": "^7.5.8", 196 | "@types/sinon": "^17.0.3", 197 | "@types/uuid": "^10.0.0", 198 | "@types/vscode": "^1.82.0", 199 | "@vscode/test-electron": "^2.4.1", 200 | "chai": "^5.1.2", 201 | "esbuild": "^0.24.0", 202 | "glob": "^11.0.0", 203 | "mkdirp": "^3.0.1", 204 | "mocha": "^10.8.2", 205 | "nyc": "^17.1.0", 206 | "ovsx": "^0.10.1", 207 | "rimraf": "^6.0.1", 208 | "sinon": "^19.0.2", 209 | "ts-mocha": "^10.0.0", 210 | "ts-node": "^10.9.2", 211 | "ts-standard": "^12.0.2", 212 | "typescript": "~5.1.6" 213 | }, 214 | "dependencies": { 215 | "@onflow/cadence-parser": "^1.0.0-preview.50", 216 | "@sentry/node": "^8.42.0", 217 | "@vscode/vsce": "^3.2.1", 218 | "ansi-regex": "^6.1.0", 219 | "async-lock": "^1.4.1", 220 | "crypto": "^1.0.1", 221 | "elliptic": "^6.6.1", 222 | "lodash": "^4.17.21", 223 | "mixpanel": "^0.18.0", 224 | "node-fetch": "^2.6.7", 225 | "object-hash": "^3.0.0", 226 | "os-name": "^6.0.0", 227 | "rxjs": "^7.8.1", 228 | "semver": "^7.6.3", 229 | "sleep-synchronously": "^2.0.0", 230 | "uuid": "^11.0.3", 231 | "vscode-languageclient": "^9.0.1" 232 | }, 233 | "__metadata": { 234 | "id": "94920651-05f7-4ba0-bf76-379f4fef81ac", 235 | "publisherDisplayName": "Flow Blockchain", 236 | "publisherId": "1b4a291e-1133-468e-b471-80338e4c9595", 237 | "isPreReleaseVersion": false 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "resolveJsonModule": true, 7 | "lib": [ 8 | "es6", "dom" 9 | ], 10 | "sourceMap": true, 11 | "strict": true, 12 | "noImplicitReturns": true, 13 | "types": [ 14 | "node" 15 | ] 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | ".vscode-test" 20 | ] 21 | } 22 | --------------------------------------------------------------------------------