diff --git a/.vscode/launch.json b/.vscode/launch.json index 8880465..2a1f1c9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,7 +15,7 @@ "outFiles": [ "${workspaceFolder}/out/**/*.js" ], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "npm: compile" } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index c636ff0..6f90bc3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ // Turn off tsc task auto detection since we have the necessary tasks as npm scripts "typescript.tsc.autoDetect": "off", "cSpell.words": [ + "frontmatter", "Stencila" ], "yaml.schemas": { diff --git a/README.md b/README.md index 66f813b..b02517b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,15 @@ # stencila README + +## Developing + +### Organization + +- `fixtures/nodes`: contains examples of Stencila nodes in JSON + +- `templates`: contains Jinja2 Markdown templates for rendering [`Hover`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) for nodes. These are prototypes that will be moved to the `rust/lsp` crate (in the main `stencila/stencila` repo) later. + + This is the README for your extension "stencila". After writing up a brief description, we recommend including the following sections. ## Features diff --git a/src/fixtures/syntax.smd b/fixtures/docs/syntax.smd similarity index 100% rename from src/fixtures/syntax.smd rename to fixtures/docs/syntax.smd diff --git a/fixtures/nodes/code-block.json b/fixtures/nodes/code-block.json new file mode 100644 index 0000000..4e25676 --- /dev/null +++ b/fixtures/nodes/code-block.json @@ -0,0 +1,6 @@ +{ + "type": "CodeBlock", + "code": "print(\"Hello world!\")", + "programmingLanguage": "r" + } + \ No newline at end of file diff --git a/fixtures/nodes/code-chunk.json b/fixtures/nodes/code-chunk.json new file mode 100644 index 0000000..ee7589b --- /dev/null +++ b/fixtures/nodes/code-chunk.json @@ -0,0 +1,19 @@ +{ + "type": "CodeChunk", + "code": "print(\"Hello world!\")", + "programmingLanguage": "python", + "outputs": ["Hello world!"], + "executionCount": 1, + "executionRequired": "No", + "executionStatus": "Succeeded", + "executionEnded": { + "type": "Timestamp", + "value": 1710299943307, + "timeUnit": "Millisecond" + }, + "executionDuration": { + "type": "Duration", + "value": 1605, + "timeUnit": "Millisecond" + } +} diff --git a/fixtures/nodes/code-chunk.with-exception.json b/fixtures/nodes/code-chunk.with-exception.json new file mode 100644 index 0000000..23714c2 --- /dev/null +++ b/fixtures/nodes/code-chunk.with-exception.json @@ -0,0 +1,27 @@ +{ + "type": "CodeChunk", + "code": "if # This is a syntax error", + "programmingLanguage": "python", + "executionCount": 1, + "executionRequired": "ExecutionFailed", + "executionStatus": "Exceptions", + "executionEnded": { + "type": "Timestamp", + "value": 1710456671989, + "timeUnit": "Millisecond" + }, + "executionDuration": { + "type": "Duration", + "value": 3, + "timeUnit": "Millisecond" + }, + "executionMessages": [ + { + "type": "ExecutionMessage", + "level": "Exception", + "message": "invalid syntax (, line 1)", + "errorType": "SyntaxError", + "stackTrace": "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"\", line 1\n if # This is a syntax error\n ^^\nSyntaxError: invalid syntax" + } + ] +} diff --git a/fixtures/nodes/for-block.json b/fixtures/nodes/for-block.json new file mode 100644 index 0000000..88a884b --- /dev/null +++ b/fixtures/nodes/for-block.json @@ -0,0 +1,134 @@ +{ + "type": "ForBlock", + "code": "items", + "variable": "item", + "content": [ + { + "type": "Paragraph", + "content": [ + { + "type": "Text", + "value": "This is item " + }, + { + "type": "CodeExpression", + "code": "item" + } + ] + } + ], + "iterations": [ + { + "type": "Section", + "content": [ + { + "type": "Paragraph", + "content": [ + { + "type": "Text", + "value": "This is item " + }, + { + "type": "CodeExpression", + "code": "item", + "output": 1, + "executionCount": 1, + "executionRequired": "No", + "executionStatus": "Succeeded", + "executionEnded": { + "type": "Timestamp", + "value": 1710300042681, + "timeUnit": "Millisecond" + }, + "executionDuration": { + "type": "Duration", + "value": 44, + "timeUnit": "Millisecond" + } + } + ] + } + ], + "sectionType": "Iteration" + }, + { + "type": "Section", + "content": [ + { + "type": "Paragraph", + "content": [ + { + "type": "Text", + "value": "This is item " + }, + { + "type": "CodeExpression", + "code": "item", + "output": 2, + "executionCount": 1, + "executionRequired": "No", + "executionStatus": "Succeeded", + "executionEnded": { + "type": "Timestamp", + "value": 1710300042696, + "timeUnit": "Millisecond" + }, + "executionDuration": { + "type": "Duration", + "value": 2, + "timeUnit": "Millisecond" + } + } + ] + } + ], + "sectionType": "Iteration" + }, + { + "type": "Section", + "content": [ + { + "type": "Paragraph", + "content": [ + { + "type": "Text", + "value": "This is item " + }, + { + "type": "CodeExpression", + "code": "item", + "output": 3, + "executionCount": 1, + "executionRequired": "No", + "executionStatus": "Succeeded", + "executionEnded": { + "type": "Timestamp", + "value": 1710300042700, + "timeUnit": "Millisecond" + }, + "executionDuration": { + "type": "Duration", + "value": 2, + "timeUnit": "Millisecond" + } + } + ] + } + ], + "sectionType": "Iteration" + } + ], + "executionCount": 1, + "executionRequired": "No", + "executionStatus": "Succeeded", + "executionEnded": { + "type": "Timestamp", + "value": 1710300042702, + "timeUnit": "Millisecond" + }, + "executionDuration": { + "type": "Duration", + "value": 77, + "timeUnit": "Millisecond" + } +} diff --git a/fixtures/nodes/paragraph.json b/fixtures/nodes/paragraph.json new file mode 100644 index 0000000..bd380e2 --- /dev/null +++ b/fixtures/nodes/paragraph.json @@ -0,0 +1,9 @@ +{ + "type": "Paragraph", + "content": [ + { + "type": "Text", + "value": "The ocean, a vast expanse of mystery and wonder..." + } + ] +} diff --git a/fixtures/nodes/paragraph.with-authors.json b/fixtures/nodes/paragraph.with-authors.json new file mode 100644 index 0000000..ec3835f --- /dev/null +++ b/fixtures/nodes/paragraph.with-authors.json @@ -0,0 +1,41 @@ +{ + "type": "Paragraph", + "content": [ + { + "type": "Text", + "value": "The ocean, a vast expanse of mystery and wonder, is home to some of the most fascinating creatures on Earth. Among these, the fish known for their uncanny speed stand out, showcasing natures marvel in the aquatic realm. This article dives into the realm of these speedy swimmers, discussing various aspects from the mechanisms behind their remarkable speed to a comprehensive overview of the top contenders." + } + ], + "authors": [ + { + "type": "AuthorRole", + "author": { + "type": "SoftwareApplication", + "id": "openai/gpt-4-0125-preview", + "name": "GPT", + "version": "4-0125-preview" + }, + "roleName": "Generator", + "lastModified": { + "type": "Timestamp", + "value": 1710989144957, + "timeUnit": "Millisecond" + } + }, + { + "type": "AuthorRole", + "author": { + "type": "SoftwareApplication", + "id": "stencila/insert-blocks", + "name": "Insert Blocks", + "version": "0.1.0" + }, + "roleName": "Prompter", + "lastModified": { + "type": "Timestamp", + "value": 1710989144957, + "timeUnit": "Millisecond" + } + } + ] +} diff --git a/icons/nodes/code-block.svg b/icons/nodes/code-block.svg new file mode 100644 index 0000000..ef3f796 --- /dev/null +++ b/icons/nodes/code-block.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/icons/nodes/code-chunk.svg b/icons/nodes/code-chunk.svg new file mode 100644 index 0000000..6a11d2f --- /dev/null +++ b/icons/nodes/code-chunk.svg @@ -0,0 +1,3 @@ + + + diff --git a/icons/nodes/for-block.svg b/icons/nodes/for-block.svg new file mode 100644 index 0000000..16b1953 --- /dev/null +++ b/icons/nodes/for-block.svg @@ -0,0 +1,3 @@ + + + diff --git a/package-lock.json b/package-lock.json index 4fbc738..c31040f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,16 @@ "": { "name": "stencila", "version": "0.0.1", + "dependencies": { + "nunjucks": "^3.2.4", + "vscode-languageclient": "^9.0.1", + "vscode-languageserver": "^9.0.1" + }, "devDependencies": { + "@stencila/types": "^2.0.0-alpha.25", "@types/mocha": "^10.0.6", "@types/node": "18.x", + "@types/nunjucks": "^3.2.6", "@types/vscode": "^1.88.0", "@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/parser": "^7.4.0", @@ -294,6 +301,12 @@ "node": ">=14" } }, + "node_modules/@stencila/types": { + "version": "2.0.0-alpha.25", + "resolved": "https://registry.npmjs.org/@stencila/types/-/types-2.0.0-alpha.25.tgz", + "integrity": "sha512-o5qBcBFe4IF3QwbVjcWeEkQY9qEufKigvBYtGDjDohvwbsuNaYSGWvLMyEVSLqVbuM7NKmiwOQceRWCPAVtMWg==", + "dev": true + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -330,6 +343,12 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/nunjucks": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@types/nunjucks/-/nunjucks-3.2.6.tgz", + "integrity": "sha512-pHiGtf83na1nCzliuAdq8GowYiXvH5l931xZ0YEHaLMNFgynpEqx+IPStlu7UaDkehfvl01e4x/9Tpwhy7Ue3w==", + "dev": true + }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -573,6 +592,11 @@ "node": ">=16" } }, + "node_modules/a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==" + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -659,7 +683,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, + "devOptional": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -683,17 +707,21 @@ "node": ">=8" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" }, @@ -705,7 +733,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -714,7 +741,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, + "devOptional": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -806,7 +833,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, + "devOptional": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -895,6 +922,14 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1276,7 +1311,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, + "devOptional": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1400,7 +1435,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, + "devOptional": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -1566,7 +1601,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, + "devOptional": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -1578,7 +1613,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -1596,7 +1631,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "devOptional": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -1608,7 +1643,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.12.0" } @@ -2106,11 +2141,35 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/nunjucks": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", + "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", + "dependencies": { + "a-sync-waterfall": "^1.0.0", + "asap": "^2.0.3", + "commander": "^5.1.0" + }, + "bin": { + "nunjucks-precompile": "bin/precompile" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "chokidar": "^3.3.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2241,7 +2300,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8.6" }, @@ -2321,7 +2380,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, + "devOptional": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -2447,7 +2506,6 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -2462,7 +2520,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -2725,7 +2782,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, + "devOptional": true, "dependencies": { "is-number": "^7.0.0" }, @@ -2817,6 +2874,63 @@ "node": ">=10.12.0" } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.5" + }, + "engines": { + "vscode": "^1.82.0" + } + }, + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2950,8 +3064,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { "version": "17.7.2", diff --git a/package.json b/package.json index e0b4760..20670b8 100644 --- a/package.json +++ b/package.json @@ -19,12 +19,45 @@ "Visualization", "Notebooks" ], + "configuration": { + "type": "object", + "title": "Stencila", + "properties": { + "smd.trace.server": { + "description": "Traces the communication between VS Code and the Stencila language server.", + "scope": "window", + "type": "string", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "off" + } + } + }, "commands": [ { - "command": "stencila.helloWorld", - "title": "Hello World" + "command": "stencila.invokeExecuteDocument", + "title": "Execute the current document" + }, + { + "command": "stencila.invokeExecuteNode", + "title": "Execute the current node" } ], + "menus": { + "commandPalette": [ + { + "command": "stencila.invokeExecuteDocument", + "when": "editorLangId == smd" + }, + { + "command": "stencila.invokeExecuteNode", + "when": "editorLangId == smd" + } + ] + }, "languages": [ { "id": "smd", @@ -71,8 +104,10 @@ "test": "vscode-test" }, "devDependencies": { + "@stencila/types": "^2.0.0-alpha.25", "@types/mocha": "^10.0.6", "@types/node": "18.x", + "@types/nunjucks": "^3.2.6", "@types/vscode": "^1.88.0", "@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/parser": "^7.4.0", @@ -81,5 +116,10 @@ "eslint": "^8.57.0", "js-yaml": "^4.1.0", "typescript": "^5.3.3" + }, + "dependencies": { + "nunjucks": "^3.2.4", + "vscode-languageclient": "^9.0.1", + "vscode-languageserver": "^9.0.1" } } diff --git a/src/extension.ts b/src/extension.ts index eb22c2d..82d9092 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,26 +1,86 @@ -// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below -import * as vscode from 'vscode'; +import * as path from "path"; -// This method is called when your extension is activated -// Your extension is activated the very first time the command is executed +import * as vscode from "vscode"; +import { + LanguageClient, + LanguageClientOptions, + ServerOptions, + TransportKind, +} from "vscode-languageclient/node"; + +let client: LanguageClient; + +/** + * Activate the extension + */ export function activate(context: vscode.ExtensionContext) { + // Register command to execute the active document + context.subscriptions.push( + vscode.commands.registerCommand("stencila.invokeExecuteDocument", () => { + const editor = vscode.window.activeTextEditor; + if (!editor) { + vscode.window.showErrorMessage("No active editor"); + return; + } + + vscode.commands.executeCommand( + "stencila.executeDocument", + editor.document.uri.path + ); - // Use the console to output diagnostic information (console.log) and errors (console.error) - // This line of code will only be executed once when your extension is activated - console.log('Congratulations, your extension "stencila" is now active!'); + // TODO: Provide cancellation buttons and notify of completion + vscode.window.showInformationMessage("Document is executing"); + }) + ); - // The command has been defined in the package.json file - // Now provide the implementation of the command with registerCommand - // The commandId parameter must match the command field in package.json - let disposable = vscode.commands.registerCommand('stencila.helloWorld', () => { - // The code you place here will be executed every time your command is executed - // Display a message box to the user - vscode.window.showInformationMessage('Hello World from Stencila!'); - }); + // Register command to execute the selected node + context.subscriptions.push( + vscode.commands.registerCommand("stencila.invokeExecuteNode", () => { + const editor = vscode.window.activeTextEditor; + if (!editor) { + vscode.window.showErrorMessage("No active editor"); + return; + } - context.subscriptions.push(disposable); + vscode.commands.executeCommand( + "stencila.executeNode", + editor.document.uri.path, + editor.selection.active + ); + }) + ); + + // Start the language server client + const serverModule = context.asAbsolutePath(path.join("out", "server.js")); + const serverOptions: ServerOptions = { + run: { module: serverModule, transport: TransportKind.ipc }, + debug: { + module: serverModule, + transport: TransportKind.ipc, + }, + }; + const clientOptions: LanguageClientOptions = { + documentSelector: [{ scheme: "file", language: "smd" }], + markdown: { + isTrusted: true, + supportHtml: true, + }, + }; + client = new LanguageClient( + "stencila", + "Stencila", + serverOptions, + clientOptions + ); + client.start(); } -// This method is called when your extension is deactivated -export function deactivate() {} +/** + * Deactivate the extension + */ +export function deactivate() { + if (!client) { + return undefined; + } + return client.stop(); +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..5572256 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,164 @@ +/** + * A module to run the Stencila language server from within Node.js + * + * Rather than bundling the Stencila CLI binary with this VSCode extension + * and spawning `stencila lsp` this script calls Stencila LSP functions + * exposed by the `@stencila/node` bindings. This has two main advantages: + * + * - distributing `@stencila/node` is easier than distributing `stencila` binaries + * + * - we can prototype interactions between VSCode and the Stencila language server + * i.e. event handlers in this module act as mocks for what the Rust-based + * language server function will implement + */ + +import { ExecutionStatus } from "@stencila/types"; +import { InitializeResult, createConnection } from "vscode-languageserver/node"; +import { configure } from "nunjucks"; +import { readFileSync } from "fs"; +import * as path from "path"; + +// The root dir of this repo +const rootDir = path.join(__dirname, ".."); + +const camelToKebab = (str: string) => + str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); + +// Point Nunjucks to the templates folder +const env = configure(path.join(rootDir, "templates")); + +// Get an emoji for the execution status of a node +env.addGlobal("execution_status_emoji", (status: ExecutionStatus) => { + return ( + { + Pending: "🕑", + Running: "⚡", + Succeeded: "🟢", + Warnings: "⚠️", + Errors: "🚨", + Exceptions: "💥", + Cancelled: "🔶", + }[status as string] ?? "🔵" + ); +}); + +// Get the Markdown icon for a node +env.addGlobal("node_icon", (type: string) => { + try { + const base64 = readFileSync( + path.join(rootDir, "icons", "nodes", `${camelToKebab(type)}.svg`), + "base64" + ); + return `![](data:image/svg+xml;base64,${base64})`; + } catch { + return undefined; + } +}); + +// Get a Markdown link to execute a node +env.addGlobal("execute_node_button", (node_id: string) => { + return `[Run](command:stencila.executeNode)`; +}); + +// Represent a node as JSON +env.addGlobal("to_json", (object: any) => JSON.stringify(object)); + +// Custom logging functions for better visibility in server outputs +const debug = (...args: any[]) => + process.stderr.write(`DEBUG ${JSON.stringify(args)}\n`); +const info = (...args: any[]) => + process.stderr.write(`INFO ${JSON.stringify(args)}\n`); +const warning = (...args: any[]) => + process.stderr.write(`WARN ${JSON.stringify(args)}\n`); +const error = (...args: any[]) => + process.stderr.write(`ERROR ${JSON.stringify(args)}\n`); + +// Create a connection for the server, using Node's IPC as a transport. +const connection = createConnection(); + +/** + * Handle the `initialize` request with capabilities etc + */ +connection.onInitialize((params): InitializeResult => { + return { + capabilities: { + hoverProvider: true, + executeCommandProvider: { + commands: ["stencila.executeDocument", "stencila.executeNode"], + }, + }, + }; +}); + +/** + * Handle a hover request + * + * This should return Markdown content related to the node at the `position`. + */ +connection.onHover(({ textDocument, position }) => { + // Read the line + const line = readFileSync(textDocument.uri.replace("file://", ""), "utf-8") + .split("\n") + [position.line].trim(); + + // Guess the type of node based on the content of the line + let template = "default"; + let fixture; + if (line.startsWith("```") && line.endsWith("exec")) { + template = "code-chunk"; + fixture = "code-chunk"; + if (line.startsWith("```py")) { + fixture += ".with-exception"; + } + } else if (line.startsWith("```")) { + fixture = "code-block"; + } else if (line.startsWith("::: for")) { + template = "for-block"; + fixture = "for-block"; + } else if (position.line > 10) { + fixture = "paragraph.with-authors"; + } else { + fixture = "paragraph"; + } + + // If no fixture, don't return a hover + if (fixture === undefined) { + return undefined; + } + + // Read the corresponding node from fixtures + const node = JSON.parse( + readFileSync( + path.join(rootDir, "fixtures", "nodes", `${fixture}.json`), + "utf-8" + ) + ); + + // Render the Markdown template for the node + const md = env.render(`hover/${template}.jinja`, node); + //debug(md); + + return { + contents: { + kind: "markdown", + value: md, + }, + }; +}); + +/** + * Handle an `executeCommand` request + */ +connection.onExecuteCommand(({ command, arguments: args }) => { + if (command === "stencila.executeDocument") { + info("Execute document", args); + } else if (command === "stencila.executeNode") { + info("Execute node", args); + } else { + debug(command, args); + } +}); + +// Listen on the connection +info("Listening on LSP connection"); +connection.listen(); diff --git a/templates/hover/code-chunk.jinja b/templates/hover/code-chunk.jinja new file mode 100644 index 0000000..e77fe11 --- /dev/null +++ b/templates/hover/code-chunk.jinja @@ -0,0 +1 @@ +{% extends 'hover/executable.jinja' %} diff --git a/templates/hover/default.jinja b/templates/hover/default.jinja new file mode 100644 index 0000000..b095415 --- /dev/null +++ b/templates/hover/default.jinja @@ -0,0 +1,18 @@ +{% block header %} +### {{ type }} + +*** +{% endblock %} + +{% block main %} +{% endblock %} + +{% if authors %} +*** + +#### Authors + +{% for author in authors %} +- {{ to_json(author) }} +{% endfor %} +{% endif %} diff --git a/templates/hover/executable.jinja b/templates/hover/executable.jinja new file mode 100644 index 0000000..914ad65 --- /dev/null +++ b/templates/hover/executable.jinja @@ -0,0 +1,24 @@ +{# Override of default template for executable node types #} +{% block header %} +### {{ execution_status_emoji(executionStatus) }} {{ type }} + +*** + +{{ execute_node_button('theNodeId') }} + +*** +{% endblock %} + +{% if executionMessages %} +*** + +{% for message in executionMessages %} +**{{ message.level }}**: `{{ message.message | safe }}` + +```{{ programminLanguage }} +{{ message.stackTrace | safe }} +``` + +*** +{% endfor %} +{% endif %} diff --git a/templates/hover/for-block.jinja b/templates/hover/for-block.jinja new file mode 100644 index 0000000..e77fe11 --- /dev/null +++ b/templates/hover/for-block.jinja @@ -0,0 +1 @@ +{% extends 'hover/executable.jinja' %} diff --git a/tsconfig.json b/tsconfig.json index 6954702..8d9b533 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,8 @@ ], "sourceMap": true, "rootDir": "src", - "strict": true /* enable all strict type-checking options */ + "strict": true , + "skipLibCheck": true /* Additional Checks */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */