diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..50aed49c
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,20 @@
+name: Continuous Integration
+
+on:
+ push:
+
+jobs:
+ test:
+ name: "Tests"
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ node-version: [15.x]
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v2
+ with:
+ node-version: ${{ matrix.node-version }}
+ - run: make ci
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 4db13721..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-language: node_js
-node_js:
- - 13
-script:
- - make ci
diff --git a/README.md b/README.md
index 697c92a8..7075fc14 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
-# Welcome
+# Flow Playground
-The Flow Playground is the best way to learn and try Cadence. For newcomers to Flow
-the [Flow Developer Documentation](https://docs.onflow.org) has a guide on how to use the Playground.
+The Flow Playground is the best way to learn and try Cadence. For newcomers to Flow,
+the [Flow Developer Documentation](https://docs.onflow.org) includes a guide on how to use the Playground.
## Philosophy
@@ -14,27 +14,28 @@ We built the Flow Playground as a static website or typical "JAM stack" website
- Fast build and deploy cycles
- We want to maximize the amount of potential contributions
-### What Is the Playground?
+### What is the Playground?
We want the Playground to have features that help you build on Flow. We also want to balance functionality with learning.
+
The Playground is a learning tool first and an awesome development tool second, although the two go hand-in-hand.
## Contributing
-### Read the [Contribution Guidelines](https://github.com/onflow/flow-playground/blob/master/CONTRIBUTING.md)
+### Read the [Contribution Guidelines](CONTRIBUTING.md)
-### Git workflow:
+### Git Workflow
- Use merge squashing, not commit merging [eg. here](https://blog.dnsimple.com/2019/01/two-years-of-squash-merge/). Squash merge is your friend.
-- The master branch is the base branch, there is no dedicated development branch
+- The `staging` branch is the base branch and contains the code deployed at https://play.staging.onflow.org.
-# Developing
+## Developing
### Pre-requisites
You'll need to have Docker installed to develop.
-## Installation
+### Installation
Clone the repo
@@ -89,3 +90,7 @@ If you are using VSCode, you can use this debugging config (works with workspace
]
}
```
+
+## Deployment
+
+The runbook contains details on [how to deploy the Flow Playground web app](RUNBOOK.md).
diff --git a/RUNBOOK.md b/RUNBOOK.md
new file mode 100644
index 00000000..c3b9ba4b
--- /dev/null
+++ b/RUNBOOK.md
@@ -0,0 +1,83 @@
+## Overview
+
+The Flow Playground is a web-based interactive IDE for running Cadence code.
+It also provides an environment for the [Cadence intro tutorials](https://docs.onflow.org/cadence/tutorial/01-first-steps).
+
+The overall project consists of the web app (this) and an [API backend](https://github.com/onflow/flow-playground-api).
+
+The Playground Web App is implemented in React. The major components are as follows:
+
+### GraphQL / Apollo Client
+
+- All HTTP communication with the Playground API is done via `GraphQL` using the `Apollo` client.
+- The GraphQL schema is defined by the Playground API in [schemal.graphql](https://github.com/onflow/flow-playground-api/blob/master/schema.graphql)
+ - This project uses Apollo Client's `localStorage` interface as well.
+ - You can view the _local_ GraphQL schema in [local.graphql](src/api/apollo/local.graphql).
+- CRUD methods (wrapped Apollo client) are implemented in [projectMutator.ts](src/providers/Project/projectMutator.ts).
+- TypeScript typings and CRUD methods for Apollo are auto-generated using [GraphQL Code Generator](https://www.graphql-code-generator.com/).
+- After making changes to the `schema.local` you will need to run `npm run graphql:codegen` to auto-generate new typings and methods for Apollo Client.
+
+### Monaco Editor
+
+- The editor interface itself is implemented using [Monaco Editor](https://microsoft.github.io/monaco-editor/).
+- The editor component can be found here: https://github.com/onflow/flow-playground/tree/master/src/containers/Editor
+- The Cadence language definition (for linting and syntax highlighting) for Monaco can be found here: https://github.com/onflow/flow-playground/blob/master/src/util/cadence.ts
+
+### Cadence Language Server
+
+- The Cadence Language Server (used by Monaco) is implemented in Golang and compiled to WASM.
+ - The WASM bundle is built from [source files in the Cadence repository](https://github.com/onflow/cadence/tree/master/npm-packages/cadence-language-server) and [published on npm](https://www.npmjs.com/package/@onflow/cadence-language-server).
+- You can read more about the Cadence Language Server in the [Cadence repository](https://github.com/onflow/cadence/blob/master/languageserver/README.md).
+- The Playground integration can be found here:
+ - Server: https://github.com/onflow/flow-playground/blob/master/src/util/language-server.ts
+ - Client: https://github.com/onflow/flow-playground/blob/master/src/util/language-client.ts
+
+## Deployment
+
+The Playground Web App is deployed to [Vercel](https://vercel.com). You will see a link to join the Flow Vercel team when you open your first PR. You must be a member of the team to trigger deployments.
+
+### Staging Deployment
+
+URL: https://play.staging.onflow.org
+
+The Playground Web App is deployed to https://play.staging.onflow.org each time a new commit is pushed to the `staging` branch on this repository.
+
+1. Open a new pull request and select `staging` as the base branch. Vercel will trigger a new deployment once the PR is approved and merged.
+
+2. Vercel will then report the deployment status on the `staging` branch:
+
+![vercel-deployment](vercel-deployment.png)
+
+### Production Deployment
+
+URL: https://play.onflow.org
+
+Once a staging deployment has been verified, you can promote the changes to production.
+
+The Playground Web App is deployed to https://play.onflow.org each time a new commit is pushed to the `production` branch on this repository.
+
+1. Open a new pull request and select `production` as the base branch and `staging` as the source branch. Production deployments should always deploy the same code that is already live in staging. Vercel will trigger a new deployment once the PR is approved and merged.
+
+2. Vercel will then report the deployment status on the `production` branch:
+
+![vercel-deployment](vercel-deployment.png)
+
+## Important Gotcha: User Sessions & Project "Forking"
+
+_The Playground will not function in browsers where cookies or localStorage are disabled._
+
+### How It Works
+
+The Playground determines what content to load into the UI based on a url query param named `projectId`.
+- When a user first visits the Playground, the `projectId` param is set to `local-project`, indicating that this is a new project and has not been persisted.
+ - https://github.dev/onflow/flow-playground/blob/2e3323aba9504e6a07fc13d1b2cec0e703edce43/src/util/url.ts#L16-L17
+- At this point, a representation of the `Project` _model_ has been boostrapped and persisted to the browser's localStorage using Apollo
+ - https://github.dev/onflow/flow-playground/blob/2e3323aba9504e6a07fc13d1b2cec0e703edce43/src/providers/Project/projectDefault.ts#L216
+ - https://github.dev/onflow/flow-playground/blob/2e3323aba9504e6a07fc13d1b2cec0e703edce43/src/providers/Project/projectHooks.ts#L10-L11
+- When a user performs some action that updates any field in the project, or clicks the save button, the project is read from localStorage, and sent to the API to be persisted.
+ - https://github.dev/onflow/flow-playground/blob/2e3323aba9504e6a07fc13d1b2cec0e703edce43/src/providers/Project/projectMutator.ts#L54-L55
+- Once the mutation has returned successfully (The project state has been saved to the DB), another local value is set using Apollo/localstorage, to reflect the newly generated project's unique `id` (from the database)
+ - https://github.dev/onflow/flow-playground/blob/2e3323aba9504e6a07fc13d1b2cec0e703edce43/src/providers/Project/projectMutator.ts#L93-L94
+- The server response also sets a cookie **that links the current browser session with the new project ID**
+ - This is done so that if a user _shares_ a link to their new project (eg. https://play.onflow.org/46c7136f-803c-4166-9d46-25d8e927114c), to someone without the session cookie linking the ID and browser session, the UI will recognise (the save button becomes "fork") that this is the case, and on subsequent saves of the shared project, _will send a mutation to generate a new project based on the existing contents of the editor, preventing users from overwriting eachothers projects!_
+ - The name of the cookie is `flow-playground`
diff --git a/babel.config.js b/babel.config.js
index 0d543298..db2a9c9d 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -1,7 +1,7 @@
-// babel.config.js
-module.exports = {
- presets: [
- ['@babel/preset-env', {targets: {node: 'current'}}],
- '@babel/preset-typescript'
- ]
-};
\ No newline at end of file
+// babel.config.js
+module.exports = {
+ presets: [
+ ['@babel/preset-env', { targets: { node: 'current' } }],
+ '@babel/preset-typescript',
+ ],
+};
diff --git a/package-lock.json b/package-lock.json
index 3d32e6bf..e26a5777 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,7 @@
"@apollo/react-hooks": "^3.1.3",
"@emotion/core": "^10.0.35",
"@emotion/styled": "^10.0.27",
- "@onflow/cadence-language-server": "^0.20.2",
+ "@onflow/cadence-language-server": "^0.23.0",
"@reach/router": "^1.3.4",
"@types/file-saver": "^2.0.1",
"apollo-cache-inmemory": "^1.6.6",
@@ -26,8 +26,8 @@
"jszip": "^3.5.0",
"markdown-to-jsx": "^7.1.3",
"mixpanel-browser": "^2.39.0",
- "monaco-editor": "^0.22.3",
- "monaco-languageclient": "^0.13.1-next.9",
+ "monaco-editor": "^0.33.0",
+ "monaco-languageclient": "^0.18.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-ga": "^3.1.2",
@@ -72,7 +72,7 @@
"handlebars": "^4.7.6",
"html-webpack-plugin": "^4.4.1",
"jest": "^26.6.3",
- "monaco-editor-webpack-plugin": "^3.0.0",
+ "monaco-editor-webpack-plugin": "^7.0.1",
"prettier": "^2.2.1",
"style-loader": "^1.2.1",
"ts-loader": "^8.0.3",
@@ -5098,9 +5098,9 @@
}
},
"node_modules/@onflow/cadence-language-server": {
- "version": "0.20.2",
- "resolved": "https://registry.npmjs.org/@onflow/cadence-language-server/-/cadence-language-server-0.20.2.tgz",
- "integrity": "sha512-6K2TaWvnz3pM5yY9f9jr0TtexlEdBB/HwXynns4njsPaSVVQVWahGKSyPg4fjb4Dmrgwe41fMY4spJZ6nVLHiA==",
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/@onflow/cadence-language-server/-/cadence-language-server-0.23.0.tgz",
+ "integrity": "sha512-be6mZu+ZPi993m2hN/Chz0PrzeIqkGcLnN05CLwkTAmeIGTpPPF/dmmv1hOqnbviC1ns+WpcbqyntLTUxtCfPA==",
"dependencies": {
"vscode-jsonrpc": "^5.0.1"
}
@@ -15604,27 +15604,28 @@
}
},
"node_modules/monaco-editor": {
- "version": "0.22.3",
- "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.22.3.tgz",
- "integrity": "sha512-RM559z2CJbczZ3k2b+ouacMINkAYWwRit4/vs0g2X/lkYefDiu0k2GmgWjAuiIpQi+AqASPOKvXNmYc8KUSvVQ=="
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.33.0.tgz",
+ "integrity": "sha512-VcRWPSLIUEgQJQIE0pVT8FcGBIgFoxz7jtqctE+IiCxWugD0DwgyQBcZBhdSrdMC84eumoqMZsGl2GTreOzwqw=="
},
"node_modules/monaco-editor-webpack-plugin": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-3.1.0.tgz",
- "integrity": "sha512-TP5NkCAV0OeFTry5k/d60KR7CkhTXL4kgJKtE3BzjgbDb5TGEPEhoKmHBrSa6r7Oc0sNbPLZhKD/TP2ig7A+/A==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-7.0.1.tgz",
+ "integrity": "sha512-M8qIqizltrPlIbrb73cZdTWfU9sIsUVFvAZkL3KGjAHmVWEJ0hZKa/uad14JuOckc0GwnCaoGHvMoYtJjVyCzw==",
"dev": true,
"dependencies": {
- "loader-utils": "^2.0.0"
+ "loader-utils": "^2.0.2"
+ },
+ "peerDependencies": {
+ "monaco-editor": ">= 0.31.0",
+ "webpack": "^4.5.0 || 5.x"
}
},
"node_modules/monaco-editor-webpack-plugin/node_modules/json5": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
- "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
+ "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"dev": true,
- "dependencies": {
- "minimist": "^1.2.5"
- },
"bin": {
"json5": "lib/cli.js"
},
@@ -15633,9 +15634,9 @@
}
},
"node_modules/monaco-editor-webpack-plugin/node_modules/loader-utils": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
- "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
+ "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"dev": true,
"dependencies": {
"big.js": "^5.2.2",
@@ -15647,18 +15648,15 @@
}
},
"node_modules/monaco-languageclient": {
- "version": "0.13.1-next.9",
- "resolved": "https://registry.npmjs.org/monaco-languageclient/-/monaco-languageclient-0.13.1-next.9.tgz",
- "integrity": "sha512-VXzrrlB7RbpvrsgDJw8jmgGqSoFnAWQTOgwwIop7D25O9GB4A0HriyiyTumeOd7JJAQRLyserzSbfrRlBDD+aQ==",
+ "version": "0.18.1",
+ "resolved": "https://registry.npmjs.org/monaco-languageclient/-/monaco-languageclient-0.18.1.tgz",
+ "integrity": "sha512-8x5YbYhtnS5xZ6+Xkk6lJcuMxLdKmc8Y7flW/u5MmFm5/qbe+uUV5vsmqPjcbJy6gAJF4pjFd0PtfhTtOiaxnw==",
"dependencies": {
"glob-to-regexp": "^0.4.1",
"vscode-jsonrpc": "6.0.0",
"vscode-languageclient": "7.0.0",
- "vscode-languageserver-textdocument": "^1.0.1",
- "vscode-uri": "^3.0.2"
- },
- "engines": {
- "vscode": "^1.50.0"
+ "vscode-languageserver-textdocument": "^1.0.4",
+ "vscode-uri": "^3.0.3"
}
},
"node_modules/move-concurrently": {
@@ -20253,9 +20251,9 @@
}
},
"node_modules/vscode-languageserver-textdocument": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz",
- "integrity": "sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA=="
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.4.tgz",
+ "integrity": "sha512-/xhqXP/2A2RSs+J8JNXpiiNVvvNM0oTosNVmQnunlKvq9o4mupHOBAnnzH0lwIPKazXKvAKsVp1kr+H/K4lgoQ=="
},
"node_modules/vscode-languageserver-types": {
"version": "3.16.0",
@@ -20263,9 +20261,9 @@
"integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA=="
},
"node_modules/vscode-uri": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.2.tgz",
- "integrity": "sha512-jkjy6pjU1fxUvI51P+gCsxg1u2n8LSt0W6KrCNQceaziKzff74GoWmjVG46KieVzybO1sttPQmYfrwSHey7GUA=="
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.3.tgz",
+ "integrity": "sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA=="
},
"node_modules/vue-template-compiler": {
"version": "2.6.12",
@@ -25876,9 +25874,9 @@
}
},
"@onflow/cadence-language-server": {
- "version": "0.20.2",
- "resolved": "https://registry.npmjs.org/@onflow/cadence-language-server/-/cadence-language-server-0.20.2.tgz",
- "integrity": "sha512-6K2TaWvnz3pM5yY9f9jr0TtexlEdBB/HwXynns4njsPaSVVQVWahGKSyPg4fjb4Dmrgwe41fMY4spJZ6nVLHiA==",
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/@onflow/cadence-language-server/-/cadence-language-server-0.23.0.tgz",
+ "integrity": "sha512-be6mZu+ZPi993m2hN/Chz0PrzeIqkGcLnN05CLwkTAmeIGTpPPF/dmmv1hOqnbviC1ns+WpcbqyntLTUxtCfPA==",
"requires": {
"vscode-jsonrpc": "^5.0.1"
},
@@ -34710,32 +34708,29 @@
}
},
"monaco-editor": {
- "version": "0.22.3",
- "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.22.3.tgz",
- "integrity": "sha512-RM559z2CJbczZ3k2b+ouacMINkAYWwRit4/vs0g2X/lkYefDiu0k2GmgWjAuiIpQi+AqASPOKvXNmYc8KUSvVQ=="
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.33.0.tgz",
+ "integrity": "sha512-VcRWPSLIUEgQJQIE0pVT8FcGBIgFoxz7jtqctE+IiCxWugD0DwgyQBcZBhdSrdMC84eumoqMZsGl2GTreOzwqw=="
},
"monaco-editor-webpack-plugin": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-3.1.0.tgz",
- "integrity": "sha512-TP5NkCAV0OeFTry5k/d60KR7CkhTXL4kgJKtE3BzjgbDb5TGEPEhoKmHBrSa6r7Oc0sNbPLZhKD/TP2ig7A+/A==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-7.0.1.tgz",
+ "integrity": "sha512-M8qIqizltrPlIbrb73cZdTWfU9sIsUVFvAZkL3KGjAHmVWEJ0hZKa/uad14JuOckc0GwnCaoGHvMoYtJjVyCzw==",
"dev": true,
"requires": {
- "loader-utils": "^2.0.0"
+ "loader-utils": "^2.0.2"
},
"dependencies": {
"json5": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
- "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
- "dev": true,
- "requires": {
- "minimist": "^1.2.5"
- }
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
+ "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
+ "dev": true
},
"loader-utils": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
- "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
+ "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
@@ -34746,15 +34741,15 @@
}
},
"monaco-languageclient": {
- "version": "0.13.1-next.9",
- "resolved": "https://registry.npmjs.org/monaco-languageclient/-/monaco-languageclient-0.13.1-next.9.tgz",
- "integrity": "sha512-VXzrrlB7RbpvrsgDJw8jmgGqSoFnAWQTOgwwIop7D25O9GB4A0HriyiyTumeOd7JJAQRLyserzSbfrRlBDD+aQ==",
+ "version": "0.18.1",
+ "resolved": "https://registry.npmjs.org/monaco-languageclient/-/monaco-languageclient-0.18.1.tgz",
+ "integrity": "sha512-8x5YbYhtnS5xZ6+Xkk6lJcuMxLdKmc8Y7flW/u5MmFm5/qbe+uUV5vsmqPjcbJy6gAJF4pjFd0PtfhTtOiaxnw==",
"requires": {
"glob-to-regexp": "^0.4.1",
"vscode-jsonrpc": "6.0.0",
"vscode-languageclient": "7.0.0",
- "vscode-languageserver-textdocument": "^1.0.1",
- "vscode-uri": "^3.0.2"
+ "vscode-languageserver-textdocument": "^1.0.4",
+ "vscode-uri": "^3.0.3"
}
},
"move-concurrently": {
@@ -38604,9 +38599,9 @@
}
},
"vscode-languageserver-textdocument": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz",
- "integrity": "sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA=="
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.4.tgz",
+ "integrity": "sha512-/xhqXP/2A2RSs+J8JNXpiiNVvvNM0oTosNVmQnunlKvq9o4mupHOBAnnzH0lwIPKazXKvAKsVp1kr+H/K4lgoQ=="
},
"vscode-languageserver-types": {
"version": "3.16.0",
@@ -38614,9 +38609,9 @@
"integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA=="
},
"vscode-uri": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.2.tgz",
- "integrity": "sha512-jkjy6pjU1fxUvI51P+gCsxg1u2n8LSt0W6KrCNQceaziKzff74GoWmjVG46KieVzybO1sttPQmYfrwSHey7GUA=="
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.3.tgz",
+ "integrity": "sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA=="
},
"vue-template-compiler": {
"version": "2.6.12",
diff --git a/package.json b/package.json
index e99af0d1..e0d0c010 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
"@apollo/react-hooks": "^3.1.3",
"@emotion/core": "^10.0.35",
"@emotion/styled": "^10.0.27",
- "@onflow/cadence-language-server": "^0.20.2",
+ "@onflow/cadence-language-server": "^0.23.0",
"@reach/router": "^1.3.4",
"@types/file-saver": "^2.0.1",
"apollo-cache-inmemory": "^1.6.6",
@@ -31,8 +31,8 @@
"jszip": "^3.5.0",
"markdown-to-jsx": "^7.1.3",
"mixpanel-browser": "^2.39.0",
- "monaco-editor": "^0.22.3",
- "monaco-languageclient": "^0.13.1-next.9",
+ "monaco-editor": "^0.33.0",
+ "monaco-languageclient": "^0.18.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-ga": "^3.1.2",
@@ -77,7 +77,7 @@
"handlebars": "^4.7.6",
"html-webpack-plugin": "^4.4.1",
"jest": "^26.6.3",
- "monaco-editor-webpack-plugin": "^3.0.0",
+ "monaco-editor-webpack-plugin": "^7.0.1",
"prettier": "^2.2.1",
"style-loader": "^1.2.1",
"ts-loader": "^8.0.3",
diff --git a/src/App.tsx b/src/App.tsx
index e374e25b..b0f61321 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,18 +1,20 @@
import React, { useEffect } from 'react';
import { Global } from '@emotion/core';
-import { ThemeProvider, Text } from 'theme-ui';
+import { ThemeProvider } from 'theme-ui';
import { Router, globalHistory } from '@reach/router';
import { ApolloProvider } from '@apollo/react-hooks';
-import AppMobileWrapper from 'containers/AppMobileWrapper';
-import BrowserDetector from 'components/BrowserDetector';
+import 'reset-css';
+
import * as GoogleAnalytics from 'util/google-analytics';
import client from 'api/apollo/client';
-import globalStyles from './globalStyles';
-import theme from './theme';
-import 'reset-css';
import Playground from 'containers/Editor';
+import AppMobileWrapper from 'containers/AppMobileWrapper';
+import BrowserDetector from 'components/BrowserDetector';
+
import FourOhFour from './pages/404';
+import globalStyles from './globalStyles';
+import theme from './theme';
GoogleAnalytics.initialize(process.env.GA_TRACKING_CODE);
@@ -20,19 +22,6 @@ const Base = (props: any) => {
return
{props.children}
;
};
-const version = (
-
- v0.3.5
-
-);
-
const App: React.FC = () => {
useEffect(() => {
// record initial pageview
@@ -52,13 +41,12 @@ const App: React.FC = () => {
-
-
-
-
-
+
+
+
+
+
- {version}
diff --git a/src/components/Arguments/components.tsx b/src/components/Arguments/components.tsx
index 1ff88e56..f9e207c1 100644
--- a/src/components/Arguments/components.tsx
+++ b/src/components/Arguments/components.tsx
@@ -1,5 +1,10 @@
import React, { useState } from 'react';
-import { FaArrowCircleRight, FaExclamationTriangle, FaCaretSquareUp, FaCaretSquareDown } from 'react-icons/fa';
+import {
+ FaArrowCircleRight,
+ FaExclamationTriangle,
+ FaCaretSquareUp,
+ FaCaretSquareDown,
+} from 'react-icons/fa';
import { EntityType } from 'providers/Project';
import Button from 'components/Button';
import { useProject } from 'providers/Project/projectHooks';
@@ -46,19 +51,11 @@ export const ArgumentsTitle: React.FC = (props) => {
{errors}
)}
- {expanded ?
-
- :
-
- }
+ {expanded ? (
+
+ ) : (
+
+ )}
);
@@ -104,7 +101,7 @@ const renderMessage = (message: string) => {
let spanClass = getSpanClass(message);
const { items } = message.split(' ').reduce(
- (acc, item,i) => {
+ (acc, item, i) => {
let current = acc.items[acc.items.length - 1];
if (acc.startNew) {
acc.startNew = false;
@@ -115,7 +112,9 @@ const renderMessage = (message: string) => {
if (item.startsWith('`')) {
acc.startNew = true;
const span = (
- {item.replace(/`/g, '')}
+
+ {item.replace(/`/g, '')}
+
);
acc.items.push(span);
acc.startNew = true;
@@ -134,10 +133,13 @@ const renderMessage = (message: string) => {
};
export const ErrorsList: React.FC = (props) => {
- const { list, goTo, hideDecorations, hover } = props;
+ const { list, actions } = props;
+ const { goTo, hideDecorations, hover } = actions;
if (list.length === 0) {
+ hideDecorations()
return null;
}
+
return (
@@ -166,9 +168,9 @@ export const ErrorsList: React.FC = (props) => {
};
export const Hints: React.FC = (props: HintsProps) => {
- const [ expanded, setExpanded ] = useState(true);
- const { problems, goTo, hideDecorations, hover } = props;
-
+ const [expanded, setExpanded] = useState(true);
+ const { problems, actions } = props;
+ const { goTo, hideDecorations, hover } = actions;
const toggle = () => {
setExpanded(!expanded);
};
@@ -177,7 +179,7 @@ export const Hints: React.FC = (props: HintsProps) => {
return null;
}
const fullList = [...problems.warning, ...problems.info];
- const hintsAmount = fullList.length
+ const hintsAmount = fullList.length;
return (
@@ -188,17 +190,11 @@ export const Hints: React.FC = (props: HintsProps) => {
{hintsAmount}
)}
- {expanded ?
-
- :
-
- }
+ {expanded ? (
+
+ ) : (
+
+ )}
{expanded && (
@@ -229,7 +225,7 @@ export const Hints: React.FC = (props: HintsProps) => {
const getLabel = (type: EntityType) => {
const { project, active } = useProject();
const { accounts } = project;
-
+
switch (true) {
case type === EntityType.Account:
return accounts[active.index].deployedCode ? 'Redeploy' : 'Deploy';
diff --git a/src/components/Arguments/index.tsx b/src/components/Arguments/index.tsx
index 0819364a..4f2d3442 100644
--- a/src/components/Arguments/index.tsx
+++ b/src/components/Arguments/index.tsx
@@ -405,8 +405,8 @@ const Arguments: React.FC = (props) => {
>
)}
-
-
+
+
diff --git a/src/components/Arguments/styles.tsx b/src/components/Arguments/styles.tsx
index 2cf0e375..22fd214e 100644
--- a/src/components/Arguments/styles.tsx
+++ b/src/components/Arguments/styles.tsx
@@ -14,6 +14,13 @@ export const HoverPanel = styled.div`
box-shadow: 10px 10px 20px #c9c9c9, -10px -10px 20px #ffffff;
`;
+interface HidableProps {
+ hidden: Boolean
+}
+export const Hidable = styled.div`
+ display: ${({hidden = false}) => hidden ? "none" : "block"};
+`
+
export const Heading = styled.div`
display: flex;
align-items: center;
diff --git a/src/components/Arguments/types.tsx b/src/components/Arguments/types.tsx
index e11113a9..6dcce08e 100644
--- a/src/components/Arguments/types.tsx
+++ b/src/components/Arguments/types.tsx
@@ -1,4 +1,4 @@
-import * as monaco from 'monaco-editor';
+import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { EntityType } from 'providers/Project';
import {
CadenceProblem,
@@ -44,18 +44,20 @@ export type ArgumentsListProps = {
errors: any;
};
-export type ErrorListProps = {
- list: CadenceProblem[];
+export type Actions = {
goTo: (position: monaco.IPosition) => void;
hover: (highlight: Highlight) => void;
hideDecorations: () => void;
+}
+
+export type ErrorListProps = {
+ list: CadenceProblem[];
+ actions: Actions
};
export type HintsProps = {
problems: ProblemsList;
- goTo: (position: monaco.IPosition) => void;
- hover: (highlight: Highlight) => void;
- hideDecorations: () => void;
+ actions: Actions
};
export type HintsState = {
diff --git a/src/components/CadenceEditor.tsx b/src/components/CadenceEditor.tsx
deleted file mode 100644
index 1b6ef59f..00000000
--- a/src/components/CadenceEditor.tsx
+++ /dev/null
@@ -1,458 +0,0 @@
-import React from 'react';
-import styled from '@emotion/styled';
-import { keyframes } from '@emotion/core';
-import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
-import { extractSigners } from "util/parser";
-
-
-import configureCadence, { CADENCE_LANGUAGE_ID } from 'util/cadence';
-import {
- CadenceCheckCompleted,
- CadenceLanguageServer,
- Callbacks,
-} from 'util/language-server';
-import { createCadenceLanguageClient } from 'util/language-client';
-import { EntityType } from 'providers/Project';
-import Arguments from 'components/Arguments';
-import { Argument } from 'components/Arguments/types';
-import {
- CadenceProblem,
- formatMarker,
- goTo,
- Highlight,
- ProblemsList,
-} from 'util/language-syntax-errors';
-import {
- MonacoLanguageClient,
- ExecuteCommandRequest,
-} from 'monaco-languageclient';
-import { WithShowProps } from "containers/Editor/components";
-
-const blink = keyframes`
- 50% {
- opacity: 0.5;
- }
-`;
-
-const EditorContainer = styled.div`
- width: 100%;
- height: 100%;
- position: relative;
-
- display: ${({ show }) => (show ? 'block' : 'none')};
-
- .drag-box {
- width: fit-content;
- height: fit-content;
- position: absolute;
- right: 30px;
- top: 0;
- z-index: 12;
- }
-
- .constraints {
- width: 96vw;
- height: 90vh;
- position: fixed;
- left: 2vw;
- right: 2vw;
- top: 2vw;
- bottom: 2vw;
- pointer-events: none;
- }
-
- .playground-syntax-error-hover {
- background-color: rgba(238, 67, 30, 0.1);
- }
-
- .playground-syntax-error-hover-selection {
- background-color: rgba(238, 67, 30, 0.3);
- border-radius: 3px;
- animation: ${blink} 1s ease-in-out infinite;
- }
-
- .playground-syntax-warning-hover {
- background-color: rgb(238, 169, 30, 0.1);
- }
-
- .playground-syntax-warning-hover-selection {
- background-color: rgb(238, 169, 30, 0.3);
- border-radius: 3px;
- animation: ${blink} 1s ease-in-out infinite;
- }
-
- .playground-syntax-info-hover {
- background-color: rgb(85, 238, 30, 0.1);
- }
-
- .playground-syntax-info-hover-selection {
- background-color: rgb(85, 238, 30, 0.3);
- border-radius: 3px;
- animation: ${blink} 1s ease-in-out infinite;
- }
-
- .playground-syntax-hint-hover,
- .playground-syntax-unknown-hover {
- background-color: rgb(160, 160, 160, 0.1);
- }
-
- .playground-syntax-hint-hover-selection,
- .playground-syntax-unknown-hover-selection {
- background-color: rgb(160, 160, 160, 0.3);
- border-radius: 3px;
- animation: ${blink} 1s ease-in-out infinite;
- }
-`;
-
-type EditorState = {
- model: any;
- viewState: any;
-};
-
-type CadenceEditorProps = {
- type: EntityType;
- code: string;
- mount: string;
- show: boolean;
- onChange: any;
- activeId: string;
- languageServer: CadenceLanguageServer
- callbacks: Callbacks
- serverReady: boolean
-};
-
-type CadenceEditorState = {
- args: { [key: string]: Argument[] };
- problems: { [key: string]: ProblemsList };
-};
-
-class CadenceEditor extends React.Component<
- CadenceEditorProps,
- CadenceEditorState
-> {
- editor: monaco.editor.ICodeEditor;
- languageClient?: MonacoLanguageClient;
- _subscription: any;
- editorStates: { [key: string]: EditorState };
- clients: { [key: string]: MonacoLanguageClient }
- private callbacks: Callbacks;
-
- constructor(props: {
- code: string;
- mount: string;
- show: boolean;
- onChange: any;
- activeId: string;
- type: EntityType;
- languageServer: any;
- callbacks: Callbacks;
- serverReady: boolean;
- }) {
- super(props);
-
- this.editorStates = {};
- this.clients = {};
- this.handleResize = this.handleResize.bind(this);
- window.addEventListener('resize', this.handleResize);
- configureCadence();
-
- this.state = {
- args: {},
- problems: {},
- };
- }
-
- handleResize() {
- this.editor && this.editor.layout();
- }
-
- async componentDidMount() {
- await this.initEditor()
-
- if (this.props.serverReady) {
- await this.loadLanguageClient()
- }
- }
-
- async initEditor() {
- this.editor = monaco.editor.create(
- document.getElementById(this.props.mount),
- {
- theme: 'vs-light',
- language: CADENCE_LANGUAGE_ID,
- minimap: {
- enabled: false,
- },
- },
- );
- this._subscription = this.editor.onDidChangeModelContent((event: any) => {
- this.props.onChange(this.editor.getValue(), event);
- });
-
- const state = this.getOrCreateEditorState(
- this.props.activeId,
- this.props.code,
- );
- this.editor.setModel(state.model);
- this.editor.focus();
- }
-
- private async loadLanguageClient() {
- this.callbacks = this.props.callbacks;
- const clientId = this.props.activeId;
- if(!this.clients[clientId]){
- this.languageClient = createCadenceLanguageClient(this.callbacks);
- this.languageClient.start();
- await this.languageClient.onReady()
- this.languageClient.onNotification(
- CadenceCheckCompleted.methodName,
- async (result: CadenceCheckCompleted.Params) => {
- if (result.valid) {
- const params = await this.getParameters();
- this.setExecutionArguments(params);
- }
- this.processMarkers();
- },
- );
- this.clients[clientId] = this.languageClient;
- } else {
- this.languageClient = this.clients[clientId]
- }
-
- }
-
- private async getParameters() {
- await this.languageClient.onReady();
-
- try {
- const args = await this.languageClient.sendRequest(
- ExecuteCommandRequest.type,
- {
- command: 'cadence.server.getEntryPointParameters',
- arguments: [this.editor.getModel().uri.toString()],
- },
- );
- return args || [];
- } catch (error) {
- console.error(error);
- }
- }
-
- processMarkers() {
- const model = this.editor.getModel();
- const modelMarkers = monaco.editor.getModelMarkers({ resource: model.uri });
- const errors = modelMarkers.reduce(
- (acc: { [key: string]: CadenceProblem[] }, marker) => {
- const mappedMarker: CadenceProblem = formatMarker(marker);
- acc[mappedMarker.type].push(mappedMarker);
- return acc;
- },
- {
- error: [],
- warning: [],
- info: [],
- hint: [],
- },
- );
-
- const { activeId } = this.props;
-
- this.setState({
- problems: {
- [activeId]: errors,
- },
- });
- }
-
- setExecutionArguments(args: Argument[]) {
- const { activeId } = this.props;
- this.setState({
- args: {
- [activeId]: args,
- },
- });
- }
-
- getOrCreateEditorState(id: string, code: string): EditorState {
- const existingState = this.editorStates[id];
-
- if (existingState !== undefined) {
- return existingState;
- }
-
- const model = monaco.editor.createModel(code, CADENCE_LANGUAGE_ID);
-
- const state: EditorState = {
- model,
- viewState: null,
- };
-
- this.editorStates[id] = state;
-
- return state;
- }
-
- saveEditorState(id: string, viewState: any) {
- this.editorStates[id].viewState = viewState;
- }
-
- switchEditor(prevId: string, newId: string) {
- const newState = this.getOrCreateEditorState(newId, this.props.code);
-
- const currentViewState = this.editor.saveViewState();
-
- this.saveEditorState(prevId, currentViewState);
-
- this.editor.setModel(newState.model);
- this.editor.restoreViewState(newState.viewState);
- this.editor.focus();
- }
-
- componentWillUnmount() {
- this.destroyMonaco();
- window.removeEventListener('resize', this.handleResize);
- if (this.callbacks && this.callbacks.onClientClose) {
- this.callbacks.onClientClose();
- }
- }
-
- async componentDidUpdate(prevProps: any) {
- if (this.props.activeId !== prevProps.activeId) {
- await this.swapMonacoEditor(prevProps.activeId, this.props.activeId)
- }
-
- const serverStatusChanged = this.props.serverReady !== prevProps.serverReady
- const activeIdChanged = this.props.activeId !== prevProps.activeId
- const typeChanged = this.props.type !== prevProps.type
-
- if (serverStatusChanged || activeIdChanged || typeChanged) {
- if (this.props.callbacks.toServer !== null) {
- await this.loadLanguageClient()
- }
- }
- }
-
- async swapMonacoEditor(prev: any, current: any) {
- await this.destroyMonaco();
- await this.initEditor();
- this.switchEditor(prev, current);
- }
-
- destroyMonaco() {
- if (this.editor) {
- this.editor.dispose();
- const model = this.editor.getModel();
- if (model) {
- model.dispose();
- }
- }
- if (this._subscription) {
- this._subscription.dispose();
- }
- }
-
- extract(code: string, keyWord: string): string[] {
- const target = code
- .split(/\r\n|\n|\r/)
- .find((line) => line.includes(keyWord));
-
- if (target) {
- const match = target.match(/(?:\()(.*)(?:\))/);
- if (match) {
- return match[1].split(',').map((item) => item.replace(/\s*/g, ''));
- }
- }
- return [];
- }
-
- hover(highlight: Highlight): void {
- const { startLine, startColumn, endLine, endColumn, color } = highlight;
- const model = this.editor.getModel();
-
- const selection = model.getAllDecorations().find((item: any) => {
- return (
- item.range.startLineNumber === startLine &&
- item.range.startColumn === startColumn
- );
- });
-
- const selectionEndLine = selection
- ? selection.range.endLineNumber
- : endLine;
- const selectionEndColumn = selection
- ? selection.range.endColumn
- : endColumn;
-
- const highlightLine = [
- {
- range: new monaco.Range(startLine, startColumn, endLine, endColumn),
- options: {
- isWholeLine: true,
- className: `playground-syntax-${color}-hover`,
- },
- },
- {
- range: new monaco.Range(
- startLine,
- startColumn,
- selectionEndLine,
- selectionEndColumn,
- ),
- options: {
- isWholeLine: false,
- className: `playground-syntax-${color}-hover-selection`,
- },
- },
- ];
- this.editor.getModel().deltaDecorations([], highlightLine);
- this.editor.revealLineInCenter(startLine);
- }
-
- hideDecorations(): void {
- const model = this.editor.getModel();
- let current = model
- .getAllDecorations()
- .filter((item) => {
- const { className } = item.options;
- return className?.includes('playground-syntax');
- })
- .map((item) => item.id);
-
- model.deltaDecorations(current, []);
- }
-
- render() {
- const { type, code, show } = this.props;
-
- /// Get a list of args from language server
- const { activeId } = this.props;
- const { args, problems } = this.state;
- const list = args[activeId] || [];
-
- /// Extract number of signers from code
- const signers = extractSigners(code).length
- const problemsList: ProblemsList = problems[activeId] || {
- error: [],
- warning: [],
- hint: [],
- info: [],
- };
- return (
-
- this.hover(highlight)}
- hideDecorations={() => this.hideDecorations()}
- goTo={(position: monaco.IPosition) => goTo(this.editor, position)}
- editor={this.editor}
- languageClient={this.languageClient}
- />
-
- );
- }
-}
-
-export default CadenceEditor;
diff --git a/src/components/CadenceEditor/ControlPanel/components.tsx b/src/components/CadenceEditor/ControlPanel/components.tsx
new file mode 100644
index 00000000..9ffabec2
--- /dev/null
+++ b/src/components/CadenceEditor/ControlPanel/components.tsx
@@ -0,0 +1,232 @@
+import React from 'react';
+import { motion } from 'framer-motion';
+import styled from 'styled-components';
+import theme from '../../../theme';
+
+export const MotionBox = (props: any) => {
+ const { children, dragConstraints } = props;
+ return (
+
+ {children}
+
+ );
+};
+
+interface HoverPanelProps {
+ width?: string;
+}
+
+export const HoverPanel = styled.div`
+ min-width: 300px;
+ max-width: 500px;
+ padding: 20px;
+ border-radius: 4px;
+ background-color: #fff;
+ box-shadow: 10px 10px 20px #c9c9c9, -10px -10px 20px #ffffff;
+`;
+
+export const Heading = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 16px;
+`;
+
+interface TitleProps {
+ lineColor?: string;
+}
+
+export const Title = styled.div`
+ font-size: 10px;
+ font-weight: bold;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ position: relative;
+ color: #919191;
+
+ &:after {
+ opacity: 0.5;
+ content: '';
+ display: block;
+ position: absolute;
+ left: 0;
+ background: ${(props: any) => props.lineColor || theme.colors.primary};
+ height: 3px;
+ width: 1rem;
+ bottom: -6px;
+ border-radius: 3px;
+ }
+`;
+
+export const Controls = styled.div`
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ cursor: pointer;
+`;
+
+export const Badge = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ font-weight: bold;
+ font-size: 12px;
+ margin-right: 5px;
+
+ --size: 16px;
+ width: var(--size);
+ height: var(--size);
+ border-radius: var(--size);
+
+ span {
+ transform: translateY(1px);
+ }
+
+ background-color: #ee431e;
+ &.green {
+ background-color: ${theme.colors.primary};
+ color: #222;
+ }
+`;
+
+interface ListProps {
+ hidden?: boolean;
+}
+export const List = styled.div`
+ display: ${({ hidden }) => (hidden ? 'none' : 'grid')};
+ grid-gap: 12px;
+ grid-template-columns: 100%;
+ margin-bottom: 24px;
+ max-height: 350px;
+ overflow-y: auto;
+`;
+
+export const SignersContainer = styled.div`
+ margin-bottom: 20px;
+`;
+
+interface ControlContainerProps {
+ isOk: boolean;
+ progress: boolean;
+}
+export const ControlContainer = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ color: ${({ isOk, progress }) => {
+ switch (true) {
+ case progress:
+ return '#a2a2a2';
+ case isOk:
+ return '#2bb169';
+ default:
+ return '#EE431E';
+ }
+ }};
+`;
+
+export const ToastContainer = styled.div`
+ z-index: 1000;
+ position: fixed;
+ bottom: 40px;
+ left: 80px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ color: ${theme.colors.darkPrimary};
+`;
+
+export const StatusMessage = styled.div`
+ @keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+ }
+
+ display: flex;
+ justify-content: flex-start;
+ font-size: 16px;
+ svg {
+ margin-right: 5px;
+ }
+
+ svg.spin {
+ animation: spin 0.5s linear infinite;
+ }
+`;
+
+export const ErrorsContainer = styled.div`
+ display: grid;
+ grid-gap: 10px;
+ grid-template-columns: 100%;
+ margin-bottom: 12px;
+`;
+
+export const SingleError = styled.div`
+ cursor: pointer;
+ display: flex;
+ align-items: baseline;
+ box-sizing: border-box;
+ padding: 10px;
+ border-radius: 4px;
+ font-size: 14px;
+ &:hover {
+ background-color: rgba(244, 57, 64, 0.15);
+
+ &.hint-warning {
+ background-color: rgb(238, 169, 30, 0.15);
+ }
+
+ &.hint-info {
+ background-color: rgb(85, 238, 30, 0.15);
+ }
+ }
+`;
+
+export const ErrorIndex = styled.div`
+ width: 20px;
+ height: 20px;
+ background-color: rgba(0, 0, 0, 0.15);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 20px;
+ margin-right: 8px;
+ flex: 0 0 auto;
+`;
+
+export const ErrorMessage = styled.p`
+ line-height: 1.2;
+ word-break: break-word;
+ span {
+ background-color: rgba(0, 0, 0, 0.05);
+ padding: 2px 6px;
+ border-radius: 3px;
+ margin: 3px 3px 3px 5px;
+ line-height: 20px;
+ .suggestion {
+ background-color: ${theme.colors.primary};
+ }
+ }
+`;
+
+export const SignersError = styled.p`
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ box-sizing: border-box;
+ margin: 10px 0;
+ color: ${theme.colors.error};
+ svg {
+ margin-right: 0.5em;
+ }
+`;
diff --git a/src/components/CadenceEditor/ControlPanel/index.tsx b/src/components/CadenceEditor/ControlPanel/index.tsx
new file mode 100644
index 00000000..38200b2b
--- /dev/null
+++ b/src/components/CadenceEditor/ControlPanel/index.tsx
@@ -0,0 +1,419 @@
+// External Modules
+import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
+import { FaRegCheckCircle, FaRegTimesCircle, FaSpinner } from 'react-icons/fa';
+import { ExecuteCommandRequest } from 'monaco-languageclient';
+import {
+ IPosition,
+ editor as monacoEditor,
+} from 'monaco-editor/esm/vs/editor/editor.api';
+
+// Project Modules
+import { CadenceCheckerContext } from 'providers/CadenceChecker';
+import { EntityType } from 'providers/Project';
+import { useProject } from 'providers/Project/projectHooks';
+import {
+ CadenceProblem,
+ formatMarker,
+ goTo,
+ hideDecorations,
+ Highlight,
+ hover,
+ ProblemsList,
+} from 'util/language-syntax-errors';
+import { extractSigners } from 'util/parser';
+import { CadenceCheckCompleted } from 'util/language-server';
+
+// Local Generated Modules
+import {
+ ResultType,
+ useSetExecutionResultsMutation,
+} from 'api/apollo/generated/graphql';
+
+// Component Scoped Files
+import { getLabel, validateByType, useTemplateType } from './utils';
+import { ControlPanelProps, IValue } from './types';
+import { MotionBox } from './components';
+
+// Other
+import {
+ ActionButton,
+ ArgumentsList,
+ ArgumentsTitle,
+ ErrorsList,
+ Hints,
+ Signers,
+} from '../../Arguments/components';
+
+import {
+ ControlContainer,
+ HoverPanel,
+ Hidable,
+ StatusMessage,
+} from '../../Arguments/styles';
+
+const ControlPanel: React.FC = (props) => {
+ // We should not render this component if editor is non-existent
+ if (!props.editor) {
+ return null;
+ }
+
+ // ===========================================================================
+ // GLOBAL HOOKS
+ const { languageClient } = useContext(CadenceCheckerContext);
+ const { project, active, isSavingCode } = useProject();
+
+ // HOOKS -------------------------------------------------------------------
+ const [executionArguments, setExecutionArguments] = useState({});
+ const [processingStatus, setProcessingStatus] = useState(false);
+ const [setResult] = useSetExecutionResultsMutation();
+ const {
+ scriptFactory,
+ transactionFactory,
+ contractDeployment,
+ } = useTemplateType();
+ const [selected, updateSelectedAccounts] = useState([]);
+ const [expanded, setExpanded] = useState(true);
+
+ const [values, setValue] = useState({});
+ // Handles errors with arguments
+ const [errors, setErrors] = useState({});
+ // Handles problems, hints and info for checked code
+ const [problemsList, setProblemsList] = useState({});
+
+ // REFS -------------------------------------------------------------------
+ // Holds reference to constraining div for floating window
+ const constraintsRef = useRef();
+ // Holds reference to Disposable callback for languageClient
+ const clientOnNotification = useRef(null);
+
+ // ===========================================================================
+ // METHODS ------------------------------------------------------------------
+ /**
+ * Make active key out of active project item type and index
+ */
+ const getActiveKey = () => `${active.type}-${active.index}`;
+
+ /**
+ * Returns a list of problems, hints and info for active code
+ */
+ const getProblems = (): ProblemsList => {
+ const key = getActiveKey();
+ return (
+ problemsList[key] || {
+ error: [],
+ warning: [],
+ hint: [],
+ info: [],
+ }
+ );
+ };
+
+ /**
+ * Sends request to langugeClient to get entry point parameters.
+ * @return Promise which resolved to a list of arguments
+ */
+ const getParameters = async (): Promise<[any?]> => {
+ if (!languageClient) {
+ return [];
+ }
+ try {
+ const args = await languageClient.sendRequest(
+ ExecuteCommandRequest.type,
+ {
+ command: 'cadence.server.getEntryPointParameters',
+ arguments: [editor.getModel().uri.toString()],
+ },
+ );
+ return args || [];
+ } catch (error) {
+ console.error(error);
+ return [];
+ }
+ };
+
+ /**
+ * Process model markers and collect them into respective groups for rendering
+ * Pay attention, that we are passing "processMarkers" into the callback function of
+ * language server. This will create closure around methods - like "getActiveKey"
+ * and their returned values, which would be able to pick up changes in component state.
+ */
+ const processMarkers = () => {
+ const model = editor.getModel();
+ const modelMarkers = monacoEditor.getModelMarkers({ resource: model.uri });
+ const errors = modelMarkers.reduce(
+ (acc: { [key: string]: CadenceProblem[] }, marker) => {
+ const mappedMarker: CadenceProblem = formatMarker(marker);
+ acc[mappedMarker.type].push(mappedMarker);
+ return acc;
+ },
+ {
+ error: [],
+ warning: [],
+ info: [],
+ hint: [],
+ },
+ );
+
+ const key = getActiveKey(); // <- this value will be from static closure
+
+ setProblemsList({
+ ...problemsList,
+ [key]: errors,
+ });
+ };
+
+ /**
+ * Get list of arguments from this component's state
+ */
+ const getArguments = (): any => {
+ const key = getActiveKey();
+ return executionArguments[key] || [];
+ };
+
+ /**
+ * Disposes old languageClient callback and attached new one to create proper closure for all local methods.
+ * Otherwise, they will refer to old value of "project" prop and provide de-synced values
+ */
+ const setupLanguageClientListener = () => {
+ if (clientOnNotification.current) {
+ clientOnNotification.current.dispose();
+ }
+ clientOnNotification.current = languageClient.onNotification(
+ CadenceCheckCompleted.methodName,
+ async (result: CadenceCheckCompleted.Params) => {
+ if (result.valid) {
+ const params = await getParameters();
+ const key = getActiveKey();
+
+ // Update state
+ setExecutionArguments({
+ ...executionArguments,
+ [key]: params,
+ });
+ }
+ processMarkers();
+ },
+ );
+ };
+
+ /**
+ * Validates that list of arguments conforms to their respective types
+ * @param list - list of argument types
+ * @param values - list of argument values
+ */
+ const validate = (list: any, values: any) => {
+ const errors = list.reduce((acc: any, item: any) => {
+ const { name, type } = item;
+ const value = values[name];
+ if (value) {
+ const error = validateByType(value, type);
+ if (error) {
+ acc[name] = error;
+ }
+ } else {
+ if (type !== 'String') {
+ acc[name] = "Value can't be empty";
+ }
+ }
+ return acc;
+ }, {});
+
+ setErrors(errors);
+ };
+
+ /**
+ * Processes arguments and send scripts and transaction for execution or contracts for deployment
+ */
+ const send = async () => {
+ if (!processingStatus) {
+ setProcessingStatus(true);
+ }
+
+ const fixed = list.map((arg) => {
+ const { name, type } = arg;
+ let value = values[name];
+
+ if (type === `String`) {
+ value = `${value}`;
+ }
+
+ // We probably better fix this on server side...
+ if (type === 'UFix64') {
+ if (value.indexOf('.') < 0) {
+ value = `${value}.0`;
+ }
+ }
+
+ // Language server throws "input is not literal" without quotes
+ if (type === `String`) {
+ value = `\"${value.replace(/"/g, '\\"')}\"`;
+ }
+
+ return value;
+ });
+
+ let formatted: any;
+ try {
+ formatted = await languageClient.sendRequest(ExecuteCommandRequest.type, {
+ command: 'cadence.server.parseEntryPointArguments',
+ arguments: [editor.getModel().uri.toString(), fixed],
+ });
+ } catch (e) {
+ console.log(e);
+ }
+
+ // Map values to strings that will be passed to backend
+ const args: any = list.map((_, index) => JSON.stringify(formatted[index]));
+
+ let rawResult, resultType;
+ try {
+ switch (type) {
+ case EntityType.ScriptTemplate: {
+ resultType = ResultType.Script;
+ rawResult = await scriptFactory(args);
+ break;
+ }
+
+ case EntityType.TransactionTemplate: {
+ resultType = ResultType.Transaction;
+ rawResult = await transactionFactory(signersAccounts, args);
+ break;
+ }
+
+ case EntityType.Account: {
+ // Ask if user wants to redeploy the contract
+ if (accounts[active.index] && accounts[active.index].deployedCode) {
+ const choiceMessage =
+ 'Redeploying will clear the state of all accounts. Proceed?';
+ if (!confirm(choiceMessage)) {
+ setProcessingStatus(false);
+ return;
+ }
+ }
+ resultType = ResultType.Contract;
+ rawResult = await contractDeployment();
+ break;
+ }
+ default:
+ break;
+ }
+ } catch (e) {
+ console.error(e);
+ rawResult = e.toString();
+ }
+
+ setProcessingStatus(false);
+
+ // Display result in the bottom area
+ setResult({
+ variables: {
+ label: getLabel(resultType, project, active.index),
+ resultType,
+ rawResult,
+ },
+ }).then();
+ };
+
+ // MEMOIZED -----------------------------------------------------------------
+ // we need to wrap it in useMemo, cause otherwise it might push component into infinite rerender
+ // as "getArguments" will return new empty array on each render
+ const list = useMemo(getArguments, [active, executionArguments]);
+
+ // VARIABLES AND CONSTANTS -------------------------------------------------
+ const { editor } = props;
+ const { type } = active;
+ const code = editor.getModel().getValue();
+ const problems = getProblems();
+ const validCode = problems.error.length === 0;
+
+ const signers = extractSigners(code).length;
+ const needSigners = type == EntityType.TransactionTemplate && signers > 0;
+
+ const numberOfErrors = Object.keys(errors).length;
+ const notEnoughSigners = needSigners && selected.length < signers;
+ const haveErrors = numberOfErrors > 0 || notEnoughSigners;
+
+ const { accounts } = project;
+ const signersAccounts = selected.map((i) => accounts[i]);
+
+ const actions = {
+ goTo: (position: IPosition) => goTo(editor, position),
+ hideDecorations: () => hideDecorations(editor),
+ hover: (highlight: Highlight) => hover(editor, highlight),
+ };
+
+ const isOk = !haveErrors && validCode !== undefined && !!validCode;
+ let statusIcon = isOk ? : ;
+ let statusMessage = isOk ? 'Ready' : 'Fix errors';
+
+ const progress = isSavingCode || processingStatus;
+ if (progress) {
+ statusIcon = ;
+ statusMessage = 'Please, wait...';
+ }
+
+ // EFFECTS ------------------------------------------------------------------
+ useEffect(() => {
+ if (languageClient) {
+ setupLanguageClientListener();
+ }
+ }, [languageClient, active]);
+
+ useEffect(() => {
+ validate(list, values);
+ }, [list, values]);
+
+ // ===========================================================================
+ // RENDER
+ return (
+ <>
+
+
+
+
+ {list.length > 0 && (
+ <>
+
+ {
+ let key = name.toString();
+ let newValue = { ...values, [key]: value };
+ setValue(newValue);
+ }}
+ />
+ >
+ )}
+ {needSigners && (
+
+ )}
+
+
+
+
+
+
+
+ {statusIcon}
+ {statusMessage}
+
+
+
+
+
+ >
+ );
+};
+
+export default ControlPanel;
diff --git a/src/components/CadenceEditor/ControlPanel/types.tsx b/src/components/CadenceEditor/ControlPanel/types.tsx
new file mode 100644
index 00000000..0f958302
--- /dev/null
+++ b/src/components/CadenceEditor/ControlPanel/types.tsx
@@ -0,0 +1,24 @@
+import { Account } from 'api/apollo/generated/graphql';
+import { editor as monacoEditor } from 'monaco-editor/esm/vs/editor/editor.api';
+
+export interface IValue {
+ [key: string]: string;
+}
+
+export type ControlPanelProps = {
+ editor: monacoEditor.ICodeEditor;
+};
+
+export type ScriptExecution = (args?: string[]) => Promise;
+export type TransactionExecution = (
+ signingAccounts: Account[],
+ args?: string[],
+) => Promise;
+export type DeployExecution = () => Promise;
+
+export type ProcessingArgs = {
+ disabled: boolean;
+ scriptFactory?: ScriptExecution;
+ transactionFactory?: TransactionExecution;
+ contractDeployment?: DeployExecution;
+};
diff --git a/src/components/CadenceEditor/ControlPanel/utils.tsx b/src/components/CadenceEditor/ControlPanel/utils.tsx
new file mode 100644
index 00000000..07dfea34
--- /dev/null
+++ b/src/components/CadenceEditor/ControlPanel/utils.tsx
@@ -0,0 +1,114 @@
+import { ResultType } from 'api/apollo/generated/graphql';
+import { useProject } from 'providers/Project/projectHooks';
+import { ProcessingArgs } from './types';
+
+const isDictionary = (type: string) => type.includes('{');
+const isArray = (type: string) => type.includes('[');
+const isImportedType = (type: string) => type.includes('.');
+const isComplexType = (type: string) =>
+ isDictionary(type) || isArray(type) || isImportedType(type);
+
+export const startsWith = (value: string, prefix: string) => {
+ return value.startsWith(prefix) || value.startsWith('U' + prefix);
+};
+
+export const checkJSON = (value: any, type: string) => {
+ try {
+ JSON.parse(value);
+ return null;
+ } catch (e) {
+ return `Not a valid argument of type ${type}`;
+ }
+};
+
+export const validateByType = (value: any, type: string) => {
+ if (value.length === 0) {
+ return "Value can't be empty";
+ }
+
+ switch (true) {
+ // Strings
+ case type === 'String': {
+ return null; // no need to validate String for now
+ }
+
+ // Integers
+ case startsWith(type, 'Int'): {
+ if (isNaN(value) || value === '') {
+ return 'Should be a valid Integer number';
+ }
+ return null;
+ }
+
+ // Words
+ case startsWith(type, 'Word'): {
+ if (isNaN(value) || value === '') {
+ return 'Should be a valid Word number';
+ }
+ return null;
+ }
+
+ // Fixed Point
+ case startsWith(type, 'Fix'): {
+ if (isNaN(value) || value === '') {
+ return 'Should be a valid fixed point number';
+ }
+ return null;
+ }
+
+ case isComplexType(type): {
+ // This case it to catch complex arguments like Dictionaries
+ return checkJSON(value, type);
+ }
+
+ // Address
+ case type === 'Address': {
+ if (!value.match(/(^0x[\w\d]{16})|(^0x[\w\d]{1,4})/)) {
+ return 'Not a valid Address';
+ }
+ return null;
+ }
+
+ // Booleans
+ case type === 'Bool': {
+ if (value !== 'true' && value !== 'false') {
+ return 'Boolean values can be either true or false';
+ }
+ return null;
+ }
+
+ default: {
+ return null;
+ }
+ }
+};
+
+export const getLabel = (
+ resultType: ResultType,
+ project: any,
+ index: number,
+): string => {
+ return resultType === ResultType.Contract
+ ? 'Deployment'
+ : resultType === ResultType.Script
+ ? project.scriptTemplates[index].title
+ : resultType === ResultType.Transaction
+ ? project.transactionTemplates[index].title
+ : 'Interaction';
+};
+
+export const useTemplateType = (): ProcessingArgs => {
+ const { isSavingCode } = useProject();
+ const {
+ createScriptExecution,
+ createTransactionExecution,
+ updateAccountDeployedCode,
+ } = useProject();
+
+ return {
+ disabled: isSavingCode,
+ scriptFactory: createScriptExecution,
+ transactionFactory: createTransactionExecution,
+ contractDeployment: updateAccountDeployedCode,
+ };
+};
diff --git a/src/components/CadenceEditor/Notifications/components.tsx b/src/components/CadenceEditor/Notifications/components.tsx
new file mode 100644
index 00000000..5193be92
--- /dev/null
+++ b/src/components/CadenceEditor/Notifications/components.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import styled from 'styled-components';
+import { motion } from 'framer-motion';
+import { Box, Flex, Text } from 'theme-ui';
+
+import theme from '../../../theme';
+
+export const ToastContainer = styled.div`
+ z-index: 1000;
+ position: fixed;
+ bottom: 40px;
+ left: 80px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ color: ${theme.colors.darkPrimary};
+`;
+
+export const RemoveToastButton = styled.button`
+ border: none;
+ background: transparent;
+ transform: translate(25%, 50%);
+ color: ${theme.colors.grey};
+ &:hover {
+ color: ${theme.colors.heading};
+ }
+`;
+
+export const ButtonContainer = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export const ContentBox = ({ children }) => {
+ const sx = {
+ marginTop: '0.0rem',
+ padding: '0.8rem 0.5rem',
+ alignItems: 'center',
+ border: `1px solid ${theme.colors.borderDark}`,
+ backgroundColor: theme.colors.background,
+ borderRadius: '8px',
+ maxWidth: '500px',
+ boxShadow: '10px 10px 20px #c9c9c9, -10px -10px 20px #ffffff',
+ };
+ return (
+
+ {children}
+
+ );
+};
+
+export const Content = ({ children }) => {
+ const sx = {
+ padding: '0.75rem',
+ };
+ return {children};
+};
+
+export const SingleToast = (props: any) => {
+ const { children } = props;
+ const toastProps = {
+ layout: true,
+ initial: { opacity: 0, y: 50, scale: 0.3 },
+ animate: { opacity: 1, y: 0, scale: 1 },
+ exit: { opacity: 0, scale: 0.5, transition: { duration: 0.2 } },
+ };
+ return {children};
+};
diff --git a/src/components/CadenceEditor/Notifications/index.tsx b/src/components/CadenceEditor/Notifications/index.tsx
new file mode 100644
index 00000000..72264547
--- /dev/null
+++ b/src/components/CadenceEditor/Notifications/index.tsx
@@ -0,0 +1,117 @@
+import React, { useEffect, useState } from 'react';
+import { useProject } from 'providers/Project/projectHooks';
+
+import {
+ SingleToast,
+ ToastContainer,
+ RemoveToastButton,
+ ButtonContainer,
+ ContentBox,
+ Content,
+} from './components';
+import { AiFillCloseCircle } from 'react-icons/ai';
+
+const Notifications = () => {
+ // ===========================================================================
+ // GLOBAL HOOKS
+ const { project, lastSigners } = useProject();
+
+ // HOOKS -------------------------------------------------------------------
+ const [_, setProjectAccounts] = useState(project.accounts);
+ const [counter, setCounter] = useState(0);
+ const [notifications, setNotifications] = useState<{
+ [identifier: string]: string[];
+ }>({});
+
+ // METHODS -------------------------------------------------------------------
+ const removeNotification = (set: any, id: number) => {
+ set((prev: any[]) => {
+ delete prev[id];
+ return {
+ ...prev,
+ };
+ });
+ };
+
+ // EFFECTS -------------------------------------------------------------------
+ useEffect(() => {
+ setProjectAccounts((prevAccounts) => {
+ const latestAccounts = project.accounts;
+ const updatedAccounts = latestAccounts.filter(
+ (latestAccount, index) =>
+ latestAccount.state !== prevAccounts[index].state,
+ );
+
+ if (updatedAccounts.length > 0) {
+ setNotifications((prev) => {
+ return {
+ ...prev,
+ [counter]: updatedAccounts,
+ };
+ });
+ setTimeout(() => removeNotification(setNotifications, counter), 5000);
+ setCounter((prev) => prev + 1);
+ }
+ return project.accounts;
+ });
+ }, [project]);
+
+ // VARIABLES AND CONSTANTS ---------------------------------------------------
+ const toasts = Object.keys(notifications).map((id) => {
+ const updatedAccounts = notifications[id];
+ let updatedStorageAccounts: string[] = [];
+
+ updatedAccounts.forEach((acct: any) => {
+ const { address } = acct;
+ const accountIndex = address.charAt(address.length - 1);
+ const accountHex = `0x0${accountIndex}`;
+ updatedStorageAccounts.push(accountHex);
+ });
+
+ const shallRender = lastSigners && updatedStorageAccounts;
+ if (!shallRender) {
+ return null;
+ }
+
+ const pluralSigners = lastSigners?.length > 1 ? 'Accounts' : 'Account';
+ const pluralUpdated =
+ updatedStorageAccounts?.length > 1 ? 'accounts' : 'account';
+ const signers = lastSigners.join(', ');
+ const updated = updatedStorageAccounts.join(', ');
+ const toastText = `${pluralSigners} ${signers} updated the storage in ${pluralUpdated} ${updated}.`;
+
+ const onClick = () => removeNotification(setNotifications, parseInt(id));
+
+ return {
+ id,
+ toastText,
+ onClick,
+ };
+ });
+
+ // RENDER
+ return (
+
+
+ {toasts.map((toast) => {
+ const { id, toastText, onClick } = toast;
+
+ return (
+
+
+
+
+
+
+
+ {toastText}
+
+
+ );
+ })}
+
+
+ );
+};
+
+export default Notifications;
diff --git a/src/components/CadenceEditor/components.tsx b/src/components/CadenceEditor/components.tsx
new file mode 100644
index 00000000..376c40a7
--- /dev/null
+++ b/src/components/CadenceEditor/components.tsx
@@ -0,0 +1,82 @@
+import styled from '@emotion/styled';
+import { keyframes } from '@emotion/core';
+
+const blink = keyframes`
+ 50% {
+ opacity: 0.5;
+ }
+`;
+
+interface EditorContainerProps {
+ show: boolean;
+}
+
+export const EditorContainer = styled.div`
+ width: 100%;
+ height: 100%;
+ position: relative;
+
+ display: ${({ show = true }) => (show ? 'block' : 'none')};
+
+ .drag-box {
+ width: fit-content;
+ height: fit-content;
+ position: absolute;
+ right: 30px;
+ top: 0;
+ z-index: 12;
+ }
+
+ .constraints {
+ width: 96vw;
+ height: 90vh;
+ position: fixed;
+ left: 2vw;
+ right: 2vw;
+ top: 2vw;
+ bottom: 2vw;
+ pointer-events: none;
+ }
+
+ .playground-syntax-error-hover {
+ background-color: rgba(238, 67, 30, 0.1);
+ }
+
+ .playground-syntax-error-hover-selection {
+ background-color: rgba(238, 67, 30, 0.3);
+ border-radius: 3px;
+ animation: ${blink} 1s ease-in-out infinite;
+ }
+
+ .playground-syntax-warning-hover {
+ background-color: rgb(238, 169, 30, 0.1);
+ }
+
+ .playground-syntax-warning-hover-selection {
+ background-color: rgb(238, 169, 30, 0.3);
+ border-radius: 3px;
+ animation: ${blink} 1s ease-in-out infinite;
+ }
+
+ .playground-syntax-info-hover {
+ background-color: rgb(85, 238, 30, 0.1);
+ }
+
+ .playground-syntax-info-hover-selection {
+ background-color: rgb(85, 238, 30, 0.3);
+ border-radius: 3px;
+ animation: ${blink} 1s ease-in-out infinite;
+ }
+
+ .playground-syntax-hint-hover,
+ .playground-syntax-unknown-hover {
+ background-color: rgb(160, 160, 160, 0.1);
+ }
+
+ .playground-syntax-hint-hover-selection,
+ .playground-syntax-unknown-hover-selection {
+ background-color: rgb(160, 160, 160, 0.3);
+ border-radius: 3px;
+ animation: ${blink} 1s ease-in-out infinite;
+ }
+`;
diff --git a/src/components/CadenceEditor/index.tsx b/src/components/CadenceEditor/index.tsx
new file mode 100644
index 00000000..c5a579c6
--- /dev/null
+++ b/src/components/CadenceEditor/index.tsx
@@ -0,0 +1,207 @@
+import React, { useEffect, useRef, useState } from 'react';
+import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
+
+import { useProject } from 'providers/Project/projectHooks';
+import { EntityType } from 'providers/Project';
+import configureCadence, { CADENCE_LANGUAGE_ID } from 'util/cadence';
+import debounce from 'util/debounce';
+
+import Notifications from './Notifications';
+import ControlPanel from './ControlPanel';
+import { EditorContainer } from './components';
+
+import { EditorState } from './types';
+
+const MONACO_CONTAINER_ID = 'monaco-container';
+
+const CadenceEditor = (props: any) => {
+ const project = useProject();
+ const [editor, setEditor] = useState(null);
+ const editorOnChange = useRef(null);
+ // We will specify type as index as non-existent numbers to prevent collision with existing enums
+ const lastEdit = useRef({
+ type: 8,
+ index: 8,
+ });
+
+ const [editorStates, setEditorStates] = useState({});
+
+ // TODO: restore view state in next implementation
+ /*
+ const saveEditorState = (id: string, viewState: any) => {
+ setEditorStates({
+ ...editorStates,
+ [id]: viewState,
+ });
+ };
+ */
+
+ // This method is used to retrieve previous MonacoEditor state
+ const getOrCreateEditorState = (id: string, code: string): EditorState => {
+ const existingState = editorStates[id];
+
+ if (existingState !== undefined) {
+ return existingState;
+ }
+
+ const newState = {
+ model: monaco.editor.createModel(code, CADENCE_LANGUAGE_ID),
+ viewState: null,
+ };
+
+ setEditorStates({
+ ...editorStates,
+ [id]: newState,
+ });
+
+ return newState;
+ };
+
+ // "getActiveCode" is used to read Cadence code from active(selected) item
+ const getActiveCode = () => {
+ const { active } = project;
+ const { accounts, scriptTemplates, transactionTemplates } = project.project;
+
+ const { type, index } = active;
+ let code, id;
+ switch (type) {
+ case EntityType.Account:
+ code = accounts[index].draftCode;
+ id = accounts[index].id;
+ break;
+ case EntityType.TransactionTemplate:
+ code = transactionTemplates[index].script;
+ id = transactionTemplates[index].id;
+ break;
+ case EntityType.ScriptTemplate:
+ code = scriptTemplates[index].script;
+ id = scriptTemplates[index].id;
+ break;
+ default:
+ code = '';
+ id = 8;
+ }
+ return [code, id];
+ };
+
+ // Method to use, when model was changed
+ const debouncedModelChange = debounce(() => {
+ if (project.project?.accounts) {
+ // we will ignore text line, cause onChange is based on active type
+ // @ts-ignore
+ project.active.onChange(editor.getValue());
+ }
+ }, 100);
+
+ //@ts-ignore
+ const setupEditor = () => {
+ const projectExist = project && project.project.accounts;
+ if (editor && projectExist) {
+ if (editorOnChange.current) {
+ editorOnChange.current.dispose();
+ }
+
+ // To pick up new changes in accounts, we will track project's active item and then add and remove
+ // new line at EOF to trick Language Client to send code changes and reimport the latest changes,
+ // clearing errors and warning about missing fields.
+ const [code, newId] = getActiveCode();
+ const newState = getOrCreateEditorState(newId, code);
+ if (
+ lastEdit.current.type == project.active.type &&
+ lastEdit.current.index == project.active.index
+ ) {
+ editor.setModel(newState.model);
+ editor.restoreViewState(newState.viewState);
+ editor.focus();
+ } else {
+ // - Add new line at the end of the model
+ // Remove tracking of model updates to prevent re-rendering
+ if (editorOnChange.current) {
+ editorOnChange.current.dispose();
+ }
+
+ newState.model.setValue(code + '\n');
+ lastEdit.current = {
+ type: project.active.type,
+ index: project.active.index,
+ };
+
+ // - Mark last edited as type, index, edited = true
+ editor.setModel(newState.model);
+ editor.restoreViewState(newState.viewState);
+ editor.focus();
+ editor.layout();
+
+ setTimeout(() => {
+ newState.model.setValue(code);
+ }, 150);
+ }
+ editorOnChange.current = editor.onDidChangeModelContent(
+ debouncedModelChange,
+ );
+ }
+ };
+
+ useEffect(() => {
+ configureCadence();
+ }, []);
+
+ useEffect(() => {
+ if (editor) {
+ setupEditor();
+ }
+ }, [editor, project.active.index, project.active.type]);
+
+ // "initEditor" will create new instance of Monaco Editor and set it up
+ const initEditor = async () => {
+ const container = document.getElementById(MONACO_CONTAINER_ID);
+ const editor = monaco.editor.create(container, {
+ theme: 'vs-light',
+ language: CADENCE_LANGUAGE_ID,
+ minimap: {
+ enabled: false,
+ },
+ });
+
+ const [code] = getActiveCode();
+ const model = monaco.editor.createModel(code, CADENCE_LANGUAGE_ID);
+ const state: EditorState = {
+ model,
+ viewState: null,
+ };
+ editor.setModel(state.model);
+ editor.restoreViewState(state.viewState);
+ editor.focus();
+ editor.layout();
+
+ window.addEventListener('resize', () => {
+ editor && editor.layout();
+ });
+
+ // Save editor in component state
+ setEditor(editor);
+ setupEditor();
+ };
+
+ // "destroyEditor" is used to dispose of Monaco Editor instance, when the component is unmounted (for any reasons)
+ const destroyEditor = () => {
+ editor.dispose();
+ };
+
+ // Do it once, when CadenceEditor component is instantiated
+ useEffect(() => {
+ initEditor().then(); // drop returned Promise as we are not going to use it
+ return () => {
+ editor && destroyEditor();
+ };
+ }, []);
+
+ return (
+
+
+
+
+ );
+};
+
+export default CadenceEditor;
diff --git a/src/components/CadenceEditor/types.tsx b/src/components/CadenceEditor/types.tsx
new file mode 100644
index 00000000..74105854
--- /dev/null
+++ b/src/components/CadenceEditor/types.tsx
@@ -0,0 +1 @@
+export type EditorState = { model: any; viewState: any };
\ No newline at end of file
diff --git a/src/components/CadenceVersion.tsx b/src/components/CadenceVersion.tsx
index 42442182..e2068b5d 100644
--- a/src/components/CadenceVersion.tsx
+++ b/src/components/CadenceVersion.tsx
@@ -1,22 +1,65 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useContext } from 'react';
+import { CadenceCheckerContext } from 'providers/CadenceChecker';
+import styled from 'styled-components';
+
+export const StatusContainer = styled.div`
+ display: grid;
+ justify-content: flex-end;
+ grid-gap: 10px;
+ grid-template-columns: repeat(2, auto);
+ align-items: center;
+`;
+
+export const DotBox = styled.div`
+ display: flex;
+ justify-content: flex-end;
+ align-items: baseline;
+ flex-direction: row;
+ gap: 3px;
+`
+
+interface DotType {
+ active: string;
+}
+export const Dot = styled.div`
+ --size: 8px;
+ display: block;
+ width: var(--size);
+ height: var(--size);
+ border-radius: var(--size);
+ background-color: ${({ active = false }) => {
+ return active === 'OFF' ? '#ee431e' : '#00ff76';
+ }};
+`;
const API = process.env.PLAYGROUND_API;
export const Version = () => {
const [version, setVersion] = useState('--');
+ const { languageClient, languageServer } = useContext(CadenceCheckerContext);
useEffect(() => {
- getCadenceVerion();
+ getCadenceVersion().then();
}, []);
const url = `${API}/utils/version`;
- const getCadenceVerion = async () => {
+ const getCadenceVersion = async () => {
const response = await fetch(url);
const { version } = await response.json();
setVersion(version);
};
- return(
- Cadence: {version}
- )
+ const lsStatus = languageServer ? 'ON' : 'OFF';
+ const lcStatus = languageClient ? 'ON' : 'OFF';
+
+ return (
+
+
+ LSP:
+
+
+
+ Cadence: {version}
+
+ );
};
diff --git a/src/containers/Editor/components.tsx b/src/containers/Editor/components.tsx
index 1ca9ac04..485b2915 100644
--- a/src/containers/Editor/components.tsx
+++ b/src/containers/Editor/components.tsx
@@ -10,15 +10,17 @@ import { Editor as EditorRoot } from 'layout/Editor';
import { Heading } from 'layout/Heading';
import { EntityType, ActiveEditor } from 'providers/Project';
import { useProject } from 'providers/Project/projectHooks';
-import { PLACEHOLDER_DESCRIPTION, PLACEHOLDER_TITLE } from "providers/Project/projectDefault";
-import {Account, Project} from 'api/apollo/generated/graphql';
-
+import {
+ PLACEHOLDER_DESCRIPTION,
+ PLACEHOLDER_TITLE,
+} from 'providers/Project/projectDefault';
+import { Account, Project } from 'api/apollo/generated/graphql';
import debounce from 'util/debounce';
import Mixpanel from 'util/mixpanel';
import { default as FlowButton } from 'components/Button';
-import CadenceEditor from 'components/CadenceEditor';
+// import CadenceEditor from 'components/CadenceEditor';
import TransactionBottomBar from 'components/TransactionBottomBar';
import ScriptBottomBar from 'components/ScriptBottomBar';
import { Version } from 'components/CadenceVersion';
@@ -31,11 +33,15 @@ import {
Label,
} from 'components/Arguments/SingleArgument/styles';
import { Markdown } from 'components/Markdown';
+import {
+ ProjectInfoContainer,
+ ProjectDescription,
+ ProjectHeading,
+ ReadmeHtmlContainer,
+} from './layout-components';
-import { decodeText } from "util/readme";
-import { CadenceLanguageServer, Callbacks } from "util/language-server";
-import { MonacoServices } from "monaco-languageclient/lib/monaco-services";
-import * as monaco from "monaco-editor";
+import { decodeText } from 'util/readme';
+import CadenceEditor from 'components/CadenceEditor';
export interface WithShowProps {
show: boolean;
@@ -165,59 +171,29 @@ function getActiveId(project: Project, active: ActiveEditor): string {
}
}
-const ProjectInfoContainer = styled.div`
- display: ${({ show }) => (show ? 'block' : 'none')};
- margin: 0.2rem 1rem 0rem 1rem;
- min-width: 500px;
- margin-top: 1rem;
-`;
-
-const ProjectHeading = styled.div`
- font-size: 2rem;
- font-weight: 700;
- margin-top: 0.25rem;
- padding: 1rem;
-`;
-
-const ProjectDescription = styled.div`
- font-size: 1.2rem;
- margin: 1rem;
- margin-top: 2rem;
- padding: 0.5rem;
- border-radius: 2px;
- font-style: italic;
-`;
-
-const ReadmeHtmlContainer = styled.div`
- margin: 1rem;
- margin-top: 0rem;
-`;
-
const usePrevious = (value: any) => {
const ref = useRef();
useEffect(() => {
ref.current = value; //assign the value of ref to the argument
- },[value]); //this code will run when the value of 'value' changes
+ }, [value]); //this code will run when the value of 'value' changes
return ref.current; //in the end, return the current ref value.
-}
+};
// This method
const compareContracts = (prev: Account[], current: Account[]) => {
for (let i = 0; i < prev.length; i++) {
- if (prev[i].deployedCode !== current[i].deployedCode){
- return false
+ if (prev[i].deployedCode !== current[i].deployedCode) {
+ return false;
}
}
- return true
-}
-
-let monacoServicesInstalled = false;
+ return true;
+};
-const MAX_DESCRIPTION_SIZE = Math.pow(1024, 2) // 1mb of storage can be saved into readme field
+const MAX_DESCRIPTION_SIZE = Math.pow(1024, 2); // 1mb of storage can be saved into readme field
const calculateSize = (readme: string) => {
- const { size } = new Blob([readme])
- return size >= MAX_DESCRIPTION_SIZE
-}
+ const { size } = new Blob([readme]);
+ return size >= MAX_DESCRIPTION_SIZE;
+};
const EditorContainer: React.FC = ({
isLoading,
@@ -225,19 +201,23 @@ const EditorContainer: React.FC = ({
active,
}) => {
const [title, setTitle] = useState(
- decodeText(project.title)
+ decodeText(project.title),
);
const [description, setDescription] = useState(
- decodeText(project.description)
+ decodeText(project.description),
);
const [readme, setReadme] = useState(project.readme);
+ //@ts-ignore
const [code, setCode] = useState('');
+ //@ts-ignore
const [activeId, setActiveId] = useState(null);
- const projectAccess = useProject()
+ const projectAccess = useProject();
- const [descriptionOverflow, setDescriptionOverflow] = useState(calculateSize(project.readme))
+ const [descriptionOverflow, setDescriptionOverflow] = useState(
+ calculateSize(project.readme),
+ );
useEffect(() => {
if (isLoading) {
@@ -254,96 +234,20 @@ const EditorContainer: React.FC = ({
}
}, [isLoading, active, projectAccess.project]);
-
- // The Monaco Language Client services have to be installed globally, once.
- // An editor must be passed, which is only used for commands.
- // As the Cadence language server is not providing any commands this is OK
-
- if (!monacoServicesInstalled) {
- monacoServicesInstalled = true;
- MonacoServices.install(monaco);
- }
-
- // We will move callbacks out
- let callbacks : Callbacks = {
- // The actual callback will be set as soon as the language server is initialized
- toServer: null,
-
- // The actual callback will be set as soon as the language server is initialized
- onClientClose: null,
-
- // The actual callback will be set as soon as the language client is initialized
- onServerClose: null,
-
- // The actual callback will be set as soon as the language client is initialized
- toClient: null,
-
- //@ts-ignore
- getAddressCode(address: string): string | undefined {
- // we will set it once it is instantiated
- }
- };
-
- const getCode = (project: any) => (address: string) =>{
- const number = parseInt(address, 16);
- if (!number) {
- return;
- }
-
- const index = number - 1
- if (index < 0 || index >= project.accounts.length) {
- return;
- }
- let code = project.accounts[index].deployedCode;
- return code
- }
-
- const [serverReady, setServerReady] = useState(false)
- const [serverCallbacks, setServerCallbacks] = useState(callbacks)
- const [languageServer, setLanguageServer] = useState(null)
- const initLanguageServer = async ()=>{
- const server = await CadenceLanguageServer.create(callbacks)
- setLanguageServer(server)
- }
-
- useEffect(()=>{
- // Init language server
- initLanguageServer()
-
- let checkInterval = setInterval(()=>{
- // .toServer() method is populated by language server
- // if it was not properly started or in progress it will be "null"
- if (callbacks.toServer !== null){
- clearInterval(checkInterval);
- setServerReady(true)
- callbacks.getAddressCode = getCode(project)
- setServerCallbacks(callbacks)
- }
- }, 300)
- // TODO: Check if we can reinstantiate language server after accounts has been changed
- },[])
-
- const reloadServer = async ()=>{
- serverCallbacks.getAddressCode = getCode(project)
- const server = await CadenceLanguageServer.create(serverCallbacks)
- setServerCallbacks(serverCallbacks)
- setLanguageServer(server)
- }
-
- const previousProjectState = usePrevious(project)
+ const previousProjectState = usePrevious(project);
// This hook will listen for project updates and if one of the contracts has been changed,
// it will reload language server
- useEffect(()=>{
- if (previousProjectState !== undefined){
+ useEffect(() => {
+ if (previousProjectState !== undefined) {
// @ts-ignore
- const previousAccounts = previousProjectState.accounts || []
- const equal = compareContracts(previousAccounts, project.accounts)
- if (!equal){
- reloadServer()
+ const previousAccounts = previousProjectState.accounts || [];
+ const equal = compareContracts(previousAccounts, project.accounts);
+ if (!equal) {
+ // reloadServer()
}
}
- }, [project])
+ }, [project]);
const onEditorChange = debounce(active.onChange);
const updateProject = (
@@ -358,10 +262,9 @@ const EditorContainer: React.FC = ({
};
const isReadmeEditor = active.type === 4;
- const readmeLabel = `README.md${descriptionOverflow
- ? " - Content can't be more than 1Mb in size"
- : ""
- }`
+ const readmeLabel = `README.md${
+ descriptionOverflow ? " - Content can't be more than 1Mb in size" : ''
+ }`;
return (
@@ -411,10 +314,10 @@ const EditorContainer: React.FC = ({
{
- const overflow = calculateSize(readme)
- setDescriptionOverflow(overflow)
+ const overflow = calculateSize(readme);
+ setDescriptionOverflow(overflow);
setReadme(readme);
- if(!overflow){
+ if (!overflow) {
updateProject(title, description, readme);
}
}}
@@ -424,17 +327,15 @@ const EditorContainer: React.FC = ({
)}
{/* This is Cadence Editor */}
- onEditorChange(code)}
show={!isReadmeEditor}
- languageServer={languageServer}
- callbacks={serverCallbacks}
- serverReady={serverReady}
- />
+ />*/}
+
diff --git a/src/containers/Editor/index.tsx b/src/containers/Editor/index.tsx
index af2d4dba..47bdd4ff 100644
--- a/src/containers/Editor/index.tsx
+++ b/src/containers/Editor/index.tsx
@@ -1,9 +1,12 @@
import React from 'react';
+import { Redirect } from '@reach/router';
+
import { ProjectProvider } from 'providers/Project';
+import CadenceChecker from 'providers/CadenceChecker';
+
import EditorLayout from './layout';
import { Base } from 'layout/Base';
import { LOCAL_PROJECT_ID } from 'util/url';
-import { Redirect} from '@reach/router';
const Playground: any = (props: any) => {
const { projectId } = props;
@@ -16,7 +19,9 @@ const Playground: any = (props: any) => {
return (
-
+
+
+
);
diff --git a/src/containers/Editor/layout-components.tsx b/src/containers/Editor/layout-components.tsx
new file mode 100644
index 00000000..7eda35a9
--- /dev/null
+++ b/src/containers/Editor/layout-components.tsx
@@ -0,0 +1,30 @@
+import styled from "@emotion/styled";
+import {WithShowProps} from "containers/Editor/components";
+
+export const ProjectInfoContainer = styled.div`
+ display: ${({ show }) => (show ? 'block' : 'none')};
+ margin: 0.2rem 1rem 0rem 1rem;
+ min-width: 500px;
+ margin-top: 1rem;
+`;
+
+export const ProjectHeading = styled.div`
+ font-size: 2rem;
+ font-weight: 700;
+ margin-top: 0.25rem;
+ padding: 1rem;
+`;
+
+export const ProjectDescription = styled.div`
+ font-size: 1.2rem;
+ margin: 1rem;
+ margin-top: 2rem;
+ padding: 0.5rem;
+ border-radius: 2px;
+ font-style: italic;
+`;
+
+export const ReadmeHtmlContainer = styled.div`
+ margin: 1rem;
+ margin-top: 0rem;
+`;
\ No newline at end of file
diff --git a/src/hooks/useLanguageServer.ts b/src/hooks/useLanguageServer.ts
new file mode 100644
index 00000000..d619b5cc
--- /dev/null
+++ b/src/hooks/useLanguageServer.ts
@@ -0,0 +1,121 @@
+import { useEffect, useState } from 'react';
+import { CadenceLanguageServer, Callbacks } from 'util/language-server';
+import { MonacoServices } from 'monaco-languageclient/lib/monaco-services';
+import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
+import { createCadenceLanguageClient } from 'util/language-client';
+import { useProject } from 'providers/Project/projectHooks';
+
+let monacoServicesInstalled = false;
+
+async function startLanguageServer(callbacks: any, getCode: any, ops) {
+ const { setLanguageServer, setCallbacks } = ops;
+ const server = await CadenceLanguageServer.create(callbacks);
+ new Promise(() => {
+ let checkInterval = setInterval(() => {
+ // .toServer() method is populated by language server
+ // if it was not properly started or in progress it will be "null"
+ if (callbacks.toServer !== null) {
+ clearInterval(checkInterval);
+ callbacks.getAddressCode = getCode;
+ setCallbacks(callbacks);
+ setLanguageServer(server);
+ console.log('%c LS: Is Up!', 'color: #00FF00');
+ }
+ }, 100);
+ });
+}
+
+const launchLanguageClient = async (callbacks, languageServer, setLanguageClient) => {
+ if (languageServer) {
+ const newClient = createCadenceLanguageClient(callbacks);
+ newClient.start();
+ await newClient.onReady();
+ setLanguageClient(newClient);
+ }
+};
+
+export default function useLanguageServer() {
+ const project = useProject();
+
+ // Language Server Callbacks
+ let initialCallbacks: Callbacks = {
+ // The actual callback will be set as soon as the language server is initialized
+ toServer: null,
+
+ // The actual callback will be set as soon as the language server is initialized
+ onClientClose: null,
+
+ // The actual callback will be set as soon as the language client is initialized
+ onServerClose: null,
+
+ // The actual callback will be set as soon as the language client is initialized
+ toClient: null,
+
+ //@ts-ignore
+ getAddressCode(address: string): string | undefined {
+ // we will set it once it is instantiated
+ },
+ };
+
+ // Base state handler
+ const [languageServer, setLanguageServer] = useState(null);
+ const [languageClient, setLanguageClient] = useState(null);
+ const [callbacks, setCallbacks] = useState(initialCallbacks);
+
+ const getCode = (address) => {
+ const { accounts } = project.project;
+
+ const number = parseInt(address, 16);
+ if (!number) {
+ return;
+ }
+
+ const index = number - 1;
+ if (index < 0 || index >= accounts.length) {
+ return;
+ }
+ let code = accounts[index].deployedCode;
+ return code;
+ };
+
+ const restartServer = () => {
+ console.log('Restarting server...');
+
+ startLanguageServer(callbacks, getCode, {
+ setLanguageServer,
+ setCallbacks,
+ });
+ };
+
+ useEffect(() => {
+ if (languageServer) {
+ languageServer.updateCodeGetter(getCode);
+ }
+ }, [project.project.accounts]);
+
+ useEffect(() => {
+ // The Monaco Language Client services have to be installed globally, once.
+ // An editor must be passed, which is only used for commands.
+ // As the Cadence language server is not providing any commands this is OK
+
+ console.log('Installing monaco services');
+ if (!monacoServicesInstalled) {
+ MonacoServices.install(monaco);
+ monacoServicesInstalled = true;
+ }
+
+ restartServer();
+ }, []);
+
+ useEffect(() => {
+ if (!languageClient) {
+ launchLanguageClient(callbacks, languageServer, setLanguageClient).then();
+ }
+ }, [languageServer]);
+
+ return {
+ languageClient,
+ languageServer,
+ restartServer,
+ };
+}
diff --git a/src/providers/CadenceChecker/index.tsx b/src/providers/CadenceChecker/index.tsx
new file mode 100644
index 00000000..308d5dd9
--- /dev/null
+++ b/src/providers/CadenceChecker/index.tsx
@@ -0,0 +1,17 @@
+import React, { createContext } from 'react';
+import useLanguageServer from '../../hooks/useLanguageServer';
+
+export const CadenceCheckerContext: React.Context = createContext(null);
+
+export default function CadenceChecker(props) {
+ // Connect project to cadence checker hook
+ const cadenceChecker = useLanguageServer();
+
+ // render
+ const { children } = props;
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/providers/Project/index.tsx b/src/providers/Project/index.tsx
index c43d58d8..107e29b4 100644
--- a/src/providers/Project/index.tsx
+++ b/src/providers/Project/index.tsx
@@ -1,4 +1,4 @@
-import React, { createContext, useState } from 'react';
+import React, {createContext, useState, useMemo} from 'react';
import { useApolloClient, useQuery } from '@apollo/react-hooks';
import { navigate, useLocation, Redirect } from '@reach/router';
import ProjectMutator from './projectMutator';
@@ -220,13 +220,13 @@ export const ProjectProvider: React.FC = ({
return res;
};
- const updateActiveScriptTemplate = async (script: string, title: string) => {
+ const updateActiveScriptTemplate = async (script: string) => {
clearTimeout(timeout);
setIsSaving(true);
const res = await mutator.updateScriptTemplate(
project.scriptTemplates[active.index].id,
script,
- title,
+ project.scriptTemplates[active.index].title,
);
timeout = setTimeout(() => {
setIsSaving(false);
@@ -234,16 +234,13 @@ export const ProjectProvider: React.FC = ({
return res;
};
- const updateActiveTransactionTemplate = async (
- script: string,
- title: string,
- ) => {
+ const updateActiveTransactionTemplate = async (script: string) => {
clearTimeout(timeout);
setIsSaving(true);
const res = await mutator.updateTransactionTemplate(
project.transactionTemplates[active.index].id,
script,
- title,
+ project.transactionTemplates[active.index].title,
);
timeout = setTimeout(() => {
setIsSaving(false);
@@ -331,15 +328,15 @@ export const ProjectProvider: React.FC = ({
return {
type: active.type,
index: active.index,
- onChange: (code: any, title: string) =>
- updateActiveTransactionTemplate(code, title),
+ onChange: (code: any) =>
+ updateActiveTransactionTemplate(code),
};
case EntityType.ScriptTemplate:
return {
type: active.type,
index: active.index,
- onChange: (code: any, title: string) =>
- updateActiveScriptTemplate(code, title),
+ onChange: (code: any) =>
+ updateActiveScriptTemplate(code),
};
case EntityType.Readme:
return {
@@ -352,7 +349,10 @@ export const ProjectProvider: React.FC = ({
}
};
- const activeEditor = getActiveEditor();
+ const activeEditor = useMemo(
+ getActiveEditor,
+ [active.type, active.index, project]
+ )
const location = useLocation();
if (isLoading) return null;
diff --git a/src/util/language-client.ts b/src/util/language-client.ts
index a6346c9d..c86b43b4 100644
--- a/src/util/language-client.ts
+++ b/src/util/language-client.ts
@@ -11,7 +11,7 @@ import {
PartialMessageInfo
} from "vscode-jsonrpc"
import {ConnectionErrorHandler} from "monaco-languageclient/src/connection"
-import {ConnectionCloseHandler} from "monaco-languageclient"
+import {ConnectionCloseHandler, CloseAction, createConnection, ErrorAction, MonacoLanguageClient} from "monaco-languageclient"
export function createCadenceLanguageClient(callbacks: Callbacks) {
const logger: Logger = {
@@ -66,14 +66,13 @@ export function createCadenceLanguageClient(callbacks: Callbacks) {
})
},
dispose() {
+ console.log("-------------------------->", "Language Client is closed. Do something!")
callbacks.onClientClose()
}
}
const messageConnection = createMessageConnection(reader, writer, logger)
- const {CloseAction, createConnection, ErrorAction, MonacoLanguageClient} = require("monaco-languageclient");
-
return new MonacoLanguageClient({
name: "Cadence Language Client",
clientOptions: {
diff --git a/src/util/language-server.ts b/src/util/language-server.ts
index a72555f2..e794f2aa 100644
--- a/src/util/language-server.ts
+++ b/src/util/language-server.ts
@@ -161,4 +161,13 @@ export class CadenceLanguageServer {
window[this.functionName('onClientClose')]()
}
}
+
+ updateCodeGetter(newMethod){
+ window[this.functionName('getAddressCode')] = (address: string): string | undefined => {
+ if (!newMethod) {
+ return undefined
+ }
+ return newMethod(address)
+ }
+ }
}
diff --git a/src/util/language-syntax-errors.ts b/src/util/language-syntax-errors.ts
index 3f3af0d1..32a2ef42 100644
--- a/src/util/language-syntax-errors.ts
+++ b/src/util/language-syntax-errors.ts
@@ -1,52 +1,55 @@
// import { FaUnlink, FaRandom } from 'react-icons/fa';
import { FaExclamationTriangle } from 'react-icons/fa';
-import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
+import {
+ Range,
+ IPosition,
+ editor as monacoEditor,
+} from 'monaco-editor/esm/vs/editor/editor.api';
export enum ProblemType {
Error = 'error',
Warning = 'warning',
Info = 'info',
Hint = 'hint',
- Unknown = "unknown"
+ Unknown = 'unknown',
}
export type Highlight = {
- color: string,
- startLine: number,
- startColumn: number,
- endLine: number,
- endColumn: number
-}
+ color: string;
+ startLine: number;
+ startColumn: number;
+ endLine: number;
+ endColumn: number;
+};
export type CadenceProblem = {
- message: string,
- type: string,
- position: monaco.IPosition,
- icon: any,
- highlight: Highlight,
-}
+ message: string;
+ type: string;
+ position: IPosition;
+ icon: any;
+ highlight: Highlight;
+};
export type ProblemsList = {
- [key: string]: CadenceProblem[]
-}
+ [key: string]: CadenceProblem[];
+};
const getMarkerType = (severity: number): ProblemType => {
switch (true) {
- case (severity === 8):
- return ProblemType.Error
- case (severity === 4):
- return ProblemType.Warning
- case (severity === 2):
- return ProblemType.Info
- case (severity === 1):
- return ProblemType.Hint
+ case severity === 8:
+ return ProblemType.Error;
+ case severity === 4:
+ return ProblemType.Warning;
+ case severity === 2:
+ return ProblemType.Info;
+ case severity === 1:
+ return ProblemType.Hint;
default:
- return ProblemType.Unknown
+ return ProblemType.Unknown;
}
-}
-
-export const formatMarker = (marker: monaco.editor.IMarker): CadenceProblem => {
+};
+export const formatMarker = (marker: monacoEditor.IMarker): CadenceProblem => {
const markerType = getMarkerType(marker.severity);
return {
@@ -55,7 +58,7 @@ export const formatMarker = (marker: monaco.editor.IMarker): CadenceProblem => {
icon: FaExclamationTriangle,
position: {
lineNumber: marker.startLineNumber,
- column: marker.startColumn
+ column: marker.startColumn,
},
highlight: {
color: markerType,
@@ -63,13 +66,16 @@ export const formatMarker = (marker: monaco.editor.IMarker): CadenceProblem => {
endLine: marker.endLineNumber,
startColumn: marker.startColumn,
endColumn: marker.endColumn,
- }
- }
+ },
+ };
};
-export const hasErrors = (problemList: any[]):boolean => {
- return problemList.filter(problem => problem.type === ProblemType.Error).length === 0
-}
+export const hasErrors = (problemList: any[]): boolean => {
+ return (
+ problemList.filter((problem) => problem.type === ProblemType.Error)
+ .length === 0
+ );
+};
export const getIconByErrorType = (errorType: SyntaxError): any => {
switch (errorType) {
@@ -85,10 +91,65 @@ export const getIconByErrorType = (errorType: SyntaxError): any => {
};
export const goTo = (
- editor: monaco.editor.ICodeEditor,
- position: monaco.IPosition,
-) => {
+ editor: monacoEditor.ICodeEditor,
+ position: IPosition,
+): void => {
editor.revealLineInCenter(position.lineNumber);
editor.setPosition(position);
editor.focus();
};
+
+export const hover = (
+ editor: monacoEditor.ICodeEditor,
+ highlight: Highlight,
+): void => {
+ const { startLine, startColumn, endLine, endColumn, color } = highlight;
+ const model = editor.getModel();
+
+ const selection = model.getAllDecorations().find((item: any) => {
+ return (
+ item.range.startLineNumber === startLine &&
+ item.range.startColumn === startColumn
+ );
+ });
+
+ const selectionEndLine = selection ? selection.range.endLineNumber : endLine;
+ const selectionEndColumn = selection ? selection.range.endColumn : endColumn;
+
+ const highlightLine = [
+ {
+ range: new Range(startLine, startColumn, endLine, endColumn),
+ options: {
+ isWholeLine: true,
+ className: `playground-syntax-${color}-hover`,
+ },
+ },
+ {
+ range: new Range(
+ startLine,
+ startColumn,
+ selectionEndLine,
+ selectionEndColumn,
+ ),
+ options: {
+ isWholeLine: false,
+ className: `playground-syntax-${color}-hover-selection`,
+ },
+ },
+ ];
+ editor.getModel().deltaDecorations([], highlightLine);
+ editor.revealLineInCenter(startLine);
+};
+
+export const hideDecorations = (editor: monacoEditor.ICodeEditor): void => {
+ const model = editor.getModel();
+ let current = model
+ .getAllDecorations()
+ .filter((item) => {
+ const { className } = item.options;
+ return className?.includes('playground-syntax');
+ })
+ .map((item) => item.id);
+
+ model.deltaDecorations(current, []);
+};
diff --git a/tsconfig.json b/tsconfig.json
index 2cee9c65..704c44da 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,9 +7,9 @@
"allowJs": false,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
- "noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
+ "noImplicitAny": false,
"removeComments": false,
"preserveConstEnums": true,
"sourceMap": true,
diff --git a/vercel-deployment.png b/vercel-deployment.png
new file mode 100644
index 00000000..e513b8c4
Binary files /dev/null and b/vercel-deployment.png differ