Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blockly editor #124

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ node_modules/
*.vsix
out/
.DS_Store
media/
5 changes: 5 additions & 0 deletions .mocharc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
loader: 'ts-node/esm',
extension: ['ts'],
recursive: true,
}
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ popd
```

Then exit your 2nd editor and start it again via `Run`

## Cucumber Blockly

See [Inspecting and debugging webviews](https://code.visualstudio.com/api/extension-guides/webview#inspecting-and-debugging-webviews)
2,497 changes: 2,195 additions & 302 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 17 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@
"configuration": "./language-configuration.json"
}
],
"customEditors": [
{
"viewType": "Cucumber.Blockly",
"displayName": "Cucumber Blockly",
"selector": [
{
"filenamePattern": "*.gherkin"
}
],
"priority": "default"
}
],
"configuration": {
"title": "Cucumber",
"properties": {
Expand Down Expand Up @@ -85,7 +97,8 @@
"scripts": {
"vscode:prepublish": "npm run build",
"esbuild-extension": "esbuild ./src/extension.ts --external:vscode --bundle --outfile=out/extension.js --format=cjs --platform=node --minify --sourcemap",
"build": "npm run esbuild-extension",
"esbuild-cucumber-blockly": "esbuild ./src/cucumber-blockly.ts --external:vscode --bundle --outfile=media/cucumber-blockly.js --format=esm --platform=browser --sourcemap=inline",
"build": "npm run esbuild-extension && npm run esbuild-cucumber-blockly",
"compile": "tsc --build",
"prepare": "npm run copy-wasms",
"copy-wasms": "mkdir -p out && cp node_modules/@cucumber/language-service/dist/*.wasm out",
Expand All @@ -100,13 +113,15 @@
"upgrade": "npm-check-updates --upgrade"
},
"dependencies": {
"@cucumber/blockly": "0.0.6",
"@cucumber/language-server": "1.4.0",
"vscode-languageclient": "8.0.2"
},
"devDependencies": {
"@types/glob": "8.0.0",
"@types/mocha": "10.0.1",
"@types/vscode": "1.72.0",
"@types/vscode-webview": "1.57.0",
"@typescript-eslint/eslint-plugin": "5.48.0",
"@typescript-eslint/parser": "5.48.0",
"esbuild": "0.16.14",
Expand All @@ -121,6 +136,7 @@
"npm-check-updates": "16.6.2",
"prettier": "2.8.1",
"pretty-quick": "3.1.3",
"ts-node": "10.9.1",
"typescript": "4.9.4",
"vsce": "2.15.0",
"vscode-test": "1.6.1"
Expand Down
132 changes: 132 additions & 0 deletions src/CucumberBlocklyEditorProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { CucumberExpressions, Suggestion } from '@cucumber/language-server'
import * as vscode from 'vscode'

import { expressionToJson, registryToJson } from './CucumberExpressionsSerde'

export class CucumberBlocklyEditorProvider implements vscode.CustomTextEditorProvider {
private updateWebviewHtml: () => void

constructor(
private readonly context: vscode.ExtensionContext,
private registry: CucumberExpressions.ParameterTypeRegistry,
private expressions: readonly CucumberExpressions.Expression[],
private suggestions: readonly Suggestion[]
) {}

async resolveCustomTextEditor(
document: vscode.TextDocument,
webviewPanel: vscode.WebviewPanel,
token: vscode.CancellationToken
) {
this.updateWebviewHtml = () => {
webviewPanel.webview.options = { enableScripts: true }
webviewPanel.webview.html = this.getHtmlForWebview(
webviewPanel.webview,
document.getText(),
this.registry,
this.expressions,
this.suggestions
)
}
this.updateWebviewHtml()
}

onReindexed(
registry: CucumberExpressions.ParameterTypeRegistry,
expressions: readonly CucumberExpressions.Expression[],
suggestions: readonly Suggestion[]
) {
this.registry = registry
this.expressions = expressions
this.suggestions = suggestions
if (this.updateWebviewHtml) {
this.updateWebviewHtml()
}
}

/**
* Get the static html used for the editor webviews.
*/
private getHtmlForWebview(
webview: vscode.Webview,
gherkinSource: string,
registry: CucumberExpressions.ParameterTypeRegistry,
expressions: readonly CucumberExpressions.Expression[],
suggestions: readonly Suggestion[]
): string {
// Local path to script and css for the webview
const mediaUri = webview.asWebviewUri(vscode.Uri.joinPath(this.context.extensionUri, 'media'))

const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(this.context.extensionUri, 'media', 'cucumber-blockly.js')
)

const styleResetUri = webview.asWebviewUri(
vscode.Uri.joinPath(this.context.extensionUri, 'media', 'reset.css')
)

const styleVSCodeUri = webview.asWebviewUri(
vscode.Uri.joinPath(this.context.extensionUri, 'media', 'vscode.css')
)

// Use a nonce to whitelist which scripts can be run
const nonce = getNonce()

return /* html */ `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${
webview.cspSource
}; style-src 'unsafe-inline' ${webview.cspSource}; media-src ${
webview.cspSource
}; script-src 'nonce-${nonce}';">
<title>Cucumber Blocks</title>
<link href="${styleResetUri}" rel="stylesheet">
<link href="${styleVSCodeUri}" rel="stylesheet">
<style>
.flex-container {
display: flex;
}

.flex-child {
flex: 1;
}

.flex-child:first-child {
margin-right: 20px;
}

.cucumber-blockly {
height: 800px;
width: 100%;
}
</style>
</head>
<body>
<div class="flex-container">
<div class="flex-child" id="app"></div>
</div>
<script nonce="${nonce}">
window.blocklyMedia = ${JSON.stringify(mediaUri.toString())}
window.gherkinSource = ${JSON.stringify(gherkinSource)}
window.registryJson = ${JSON.stringify(registryToJson(registry))}
window.expressionsJson = ${JSON.stringify(expressions.map(expressionToJson))}
window.suggestionsJson = ${JSON.stringify(suggestions)}
</script>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`
}
}

function getNonce() {
let text = ''
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length))
}
return text
}
92 changes: 92 additions & 0 deletions src/CucumberExpressionsSerde.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { CucumberExpressions } from '@cucumber/language-server'

export type CucumberExpressionJson = {
type: 'CucumberExpression'
expression: string
}

export type RegularExpressionJson = {
type: 'RegularExpression'
expression: string
flags: string
}

export type ExpressionJson = CucumberExpressionJson | RegularExpressionJson

export type ParameterTypeJson = {
name: string | undefined
regexpStrings: readonly string[]
useForSnippets?: boolean
preferForRegexpMatch?: boolean
builtin?: boolean
}

export type ParameterTypeRegistryJson = {
parameterTypes: readonly ParameterTypeJson[]
}

export function expressionToJson(expression: CucumberExpressions.Expression): ExpressionJson {
if (expression instanceof CucumberExpressions.RegularExpression) {
return {
type: 'RegularExpression',
expression: expression.regexp.source,
flags: expression.regexp.flags,
}
} else if (expression instanceof CucumberExpressions.CucumberExpression) {
return {
type: 'CucumberExpression',
expression: expression.source,
}
} else {
throw new Error(`Unexpected expression: ${JSON.stringify(expression)}`)
}
}

export function expressionFromJson(
registry: CucumberExpressions.ParameterTypeRegistry,
json: ExpressionJson
): CucumberExpressions.Expression {
switch (json.type) {
case 'CucumberExpression':
return new CucumberExpressions.CucumberExpression(json.expression, registry)
case 'RegularExpression':
return new CucumberExpressions.RegularExpression(
new RegExp(json.expression, json.flags),
registry
)
}
}

export function registryToJson(
registry: CucumberExpressions.ParameterTypeRegistry
): ParameterTypeRegistryJson {
return {
parameterTypes: [...registry.parameterTypes].filter((t) => !t.builtin).map(parameterTypeToJson),
}
}

export function registryFromJson(
json: ParameterTypeRegistryJson
): CucumberExpressions.ParameterTypeRegistry {
const registry = new CucumberExpressions.ParameterTypeRegistry()
for (const parameterTypeJson of json.parameterTypes) {
const parameterType = new CucumberExpressions.ParameterType(
parameterTypeJson.name,
parameterTypeJson.regexpStrings,
null,
() => undefined,
parameterTypeJson.useForSnippets,
parameterTypeJson.preferForRegexpMatch,
parameterTypeJson.builtin
)
registry.defineParameterType(parameterType)
}
return registry
}

export function parameterTypeToJson(
type: CucumberExpressions.ParameterType<unknown>
): ParameterTypeJson {
const { name, regexpStrings, useForSnippets, preferForRegexpMatch, builtin } = type
return { name, regexpStrings, useForSnippets, preferForRegexpMatch, builtin }
}
45 changes: 45 additions & 0 deletions src/cucumber-blockly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// const vscode = acquireVsCodeApi()
import { mount } from '@cucumber/blockly'
import { Suggestion } from '@cucumber/language-server'

import {
expressionFromJson,
ExpressionJson,
ParameterTypeRegistryJson,
registryFromJson,
} from './CucumberExpressionsSerde'

declare global {
interface Window {
gherkinSource: string
registryJson: ParameterTypeRegistryJson
expressionsJson: ExpressionJson[]
suggestionsJson: Suggestion[]
blocklyMedia: string
}
}

const $app = document.querySelector('#app')

if ($app) {
const registry = registryFromJson(window.registryJson)
const expressions = window.expressionsJson.map((expressionJson) =>
expressionFromJson(registry, expressionJson)
)

mount(
$app,
window.gherkinSource,
window.suggestionsJson,
expressions,
window.blocklyMedia,
(error, gherkinSource) => {
if (error) {
console.error(error)
return
}
// TODO: Send a message with the new source
console.log(gherkinSource)
}
)
}
29 changes: 25 additions & 4 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,39 @@ import { startEmbeddedServer } from '@cucumber/language-server/wasm'
import vscode from 'vscode'
import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node'

import { CucumberBlocklyEditorProvider } from './CucumberBlocklyEditorProvider'
import { VscodeFiles } from './VscodeFiles'

let client: LanguageClient

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function activate(context: vscode.ExtensionContext) {
const serverOptions: ServerOptions = async () =>
startEmbeddedServer(
console.log('ACTIVATED******')
const serverOptions: ServerOptions = async () => {
// eslint-disable-next-line prefer-const
let blocklyProvider: CucumberBlocklyEditorProvider
const serverInfo = startEmbeddedServer(
__dirname,
() => new VscodeFiles(vscode.workspace.fs),
() => undefined
(registry, expressions, suggestions) => {
serverInfo.connection.console.log(
`******** onReindexed: ${expressions.length}, ${suggestions.length}`
)
blocklyProvider.onReindexed(registry, expressions, suggestions)
}
)
blocklyProvider = new CucumberBlocklyEditorProvider(
context,
serverInfo.server.registry,
serverInfo.server.expressions,
serverInfo.server.suggestions
)

// blocklyProvider.onReindexed(serverInfo.server.expressions, serverInfo.server.suggestions)
context.subscriptions.push(
vscode.window.registerCustomEditorProvider('Cucumber.Blockly', blocklyProvider)
)
return serverInfo
}

const clientOptions: LanguageClientOptions = {
// We need to list all supported languages here so that
Expand Down
Loading