├── .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 |
4 |
5 |
6 |
7 | Bringing Cadence, the resource-oriented smart contract language of Flow, to your VSCode Editor.
8 |
9 |
10 |
11 |
12 |
13 | [](https://github.com/onflow/vscode-cadence/actions/workflows/ci.yml)
14 | [](https://developers.flow.com/tools/vscode-extension)
15 | [](https://github.com/onflow/vscode-cadence/issues)
16 | [](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 |
--------------------------------------------------------------------------------