diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9896fb34..754766ca 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,6 @@ "name": "Rust", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile "image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye", - // Use 'mounts' to make the cargo cache persistent in a Docker Volume. // "mounts": [ // { @@ -13,21 +12,33 @@ // "type": "volume" // } // ] - // Features to add to the dev container. More info: https://containers.dev/features. - "features": { "ghcr.io/devcontainers/features/node": "18" }, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - "forwardPorts": [8080], - - // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "sh ./.devcontainer/postCreateCommand.sh", - - "postStartCommand": "sh ./.devcontainer/postStartCommand.sh" - + "features": { + // Install Node.js. + "ghcr.io/devcontainers/features/node:latest": {}, + // Add the GitHub CLI as a feature (to support the CodeChat Editor). + "ghcr.io/devcontainers/features/github-cli:1": {} + }, + // Run this script after the container is created. + "postCreateCommand": "./.devcontainer/postCreateCommand.sh", + // Run this script each time the container is started. + "postStartCommand": "./.devcontainer/postStartCommand.sh", // Configure tool-specific properties. - // "customizations": {}, - + "customizations": { + "codespaces": { + "openFiles": [ + "README.md" + ] + }, + "vscode": { + "settings": { + "CodeChatEditor.Server.Command": "/workspaces/CodeChat_Editor/server/target/debug/codechat-editor-server" + }, + "extensions": [ + "codechat.codechat-editor-client" + ] + } + } // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" -} +} \ No newline at end of file diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh index 51c6d633..cf951753 100755 --- a/.devcontainer/postCreateCommand.sh +++ b/.devcontainer/postCreateCommand.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash cd server -../bt install --dev \ No newline at end of file +./bt install --dev +./bt build \ No newline at end of file diff --git a/.devcontainer/postStartCommand.sh b/.devcontainer/postStartCommand.sh old mode 100644 new mode 100755 index 2688f77e..0dadfdbf --- a/.devcontainer/postStartCommand.sh +++ b/.devcontainer/postStartCommand.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash cd server -cargo run +# TODO: this doesn't open a file from the codespace, unfortunately. +cargo run -- start ../README.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9e96cae..29a02bfb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ on: jobs: # Run 'dist plan' (or host) to determine what tasks we need to do plan: - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" outputs: val: ${{ steps.plan.outputs.manifest }} tag: ${{ !github.event.pull_request && github.ref_name || '' }} @@ -177,7 +177,7 @@ jobs: needs: - plan - build-local-artifacts - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json @@ -234,7 +234,7 @@ jobs: if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" outputs: val: ${{ steps.host.outputs.manifest }} steps: @@ -299,7 +299,7 @@ jobs: # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! if: ${{ always() && needs.host.result == 'success' }} - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: diff --git a/builder/Cargo.lock b/builder/Cargo.lock index aa505621..fd3f9f36 100644 --- a/builder/Cargo.lock +++ b/builder/Cargo.lock @@ -80,9 +80,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.34" +version = "4.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" +checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" dependencies = [ "clap_builder", "clap_derive", @@ -90,9 +90,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.34" +version = "4.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" +checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" dependencies = [ "anstream", "anstyle", diff --git a/client/package-lock.json b/client/package-lock.json index dc0e5cfd..2551b980 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "codechat-editor-client", - "version": "0.1.15", + "version": "0.1.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codechat-editor-client", - "version": "0.1.15", + "version": "0.1.16", "license": "GPL-3.0-or-later", "dependencies": { "@codemirror/lang-cpp": "^6", @@ -127,9 +127,9 @@ } }, "node_modules/@codemirror/commands": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.0.tgz", - "integrity": "sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz", + "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -802,9 +802,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -841,9 +841,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz", - "integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -912,9 +912,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", - "integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", + "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", "dev": true, "license": "MIT", "engines": { @@ -932,19 +932,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.12.0", + "@eslint/core": "^0.13.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1216,31 +1229,31 @@ } }, "node_modules/@napi-rs/canvas": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.68.tgz", - "integrity": "sha512-LQESrePLEBLvhuFkXx9jjBXRC2ClYsO5mqQ1m/puth5z9SOuM3N/B3vDuqnC3RJFktDktyK9khGvo7dTkqO9uQ==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.69.tgz", + "integrity": "sha512-ydvNeJMRm+l3T14yCoUKqjYQiEdXDq1isznI93LEBGYssXKfSaLNLHOkeM4z9Fnw9Pkt2EKOCAtW9cS4b00Zcg==", "license": "MIT", "optional": true, "engines": { "node": ">= 10" }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.68", - "@napi-rs/canvas-darwin-arm64": "0.1.68", - "@napi-rs/canvas-darwin-x64": "0.1.68", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.68", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.68", - "@napi-rs/canvas-linux-arm64-musl": "0.1.68", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.68", - "@napi-rs/canvas-linux-x64-gnu": "0.1.68", - "@napi-rs/canvas-linux-x64-musl": "0.1.68", - "@napi-rs/canvas-win32-x64-msvc": "0.1.68" + "@napi-rs/canvas-android-arm64": "0.1.69", + "@napi-rs/canvas-darwin-arm64": "0.1.69", + "@napi-rs/canvas-darwin-x64": "0.1.69", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.69", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.69", + "@napi-rs/canvas-linux-arm64-musl": "0.1.69", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.69", + "@napi-rs/canvas-linux-x64-gnu": "0.1.69", + "@napi-rs/canvas-linux-x64-musl": "0.1.69", + "@napi-rs/canvas-win32-x64-msvc": "0.1.69" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.68.tgz", - "integrity": "sha512-h1KcSR4LKLfRfzeBH65xMxbWOGa1OtMFQbCMVlxPCkN1Zr+2gK+70pXO5ktojIYcUrP6KDcOwoc8clho5ccM/w==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.69.tgz", + "integrity": "sha512-4icWTByY8zPvM9SelfQKf3I6kwXw0aI5drBOVrwfER5kjwXJd78FPSDSZkxDHjvIo9Q86ljl18Yr963ehA4sHQ==", "cpu": [ "arm64" ], @@ -1254,9 +1267,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.68.tgz", - "integrity": "sha512-/VURlrAD4gDoxW1GT/b0nP3fRz/fhxmHI/xznTq2FTwkQLPOlLkDLCvTmQ7v6LtGKdc2Ed6rvYpRan+JXThInQ==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.69.tgz", + "integrity": "sha512-HOanhhYlHdukA+unjelT4Dg3ta7e820x87/AG2dKUMsUzH19jaeZs9bcYjzEy2vYi/dFWKz7cSv2yaIOudB8Yg==", "cpu": [ "arm64" ], @@ -1270,9 +1283,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.68.tgz", - "integrity": "sha512-tEpvGR6vCLTo1Tx9wmDnoOKROpw57wiCWwCpDOuVlj/7rqEJOUYr9ixW4aRJgmeGBrZHgevI0EURys2ER6whmg==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.69.tgz", + "integrity": "sha512-SIp7WfhxAPnSVK9bkFfJp+84rbATCIq9jMUzDwpCLhQ+v+OqtXe4pggX1oeV+62/HK6BT1t18qRmJfyqwJ9f3g==", "cpu": [ "x64" ], @@ -1286,9 +1299,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.68.tgz", - "integrity": "sha512-U9xbJsumPOiAYeAFZMlHf62b9dGs2HJ6Q5xt7xTB0uEyPeurwhgYBWGgabdsEidyj38YuzI/c3LGBbSQB3vagw==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.69.tgz", + "integrity": "sha512-Ls+KujCp6TGpkuMVFvrlx+CxtL+casdkrprFjqIuOAnB30Mct6bCEr+I83Tu29s3nNq4EzIGjdmA3fFAZG/Dtw==", "cpu": [ "arm" ], @@ -1302,9 +1315,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.68.tgz", - "integrity": "sha512-KFkn8wEm3mPnWD4l8+OUUkxylSJuN5q9PnJRZJgv15RtCA1bgxIwTkBhI/+xuyVMcHqON9sXq7cDkEJtHm35dg==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.69.tgz", + "integrity": "sha512-m8VcGmeSBNRbHZBd1srvdM1aq/ScS2y8KqGqmCCEgJlytYK4jdULzAo2K/BPKE1v3xvn8oUPZDLI/NBJbJkEoA==", "cpu": [ "arm64" ], @@ -1318,9 +1331,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.68.tgz", - "integrity": "sha512-IQzts91rCdOALXBWQxLZRCEDrfFTGDtNRJMNu+2SKZ1uT8cmPQkPwVk5rycvFpvgAcmiFiOSCp1aRrlfU8KPpQ==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.69.tgz", + "integrity": "sha512-a3xjNRIeK2m2ZORGv2moBvv3vbkaFZG1QKMeiEv/BKij+rkztuEhTJGMar+buICFgS0fLgphXXsKNkUSJb7eRQ==", "cpu": [ "arm64" ], @@ -1334,9 +1347,9 @@ } }, "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.68.tgz", - "integrity": "sha512-e9AS5UttoIKqXSmBzKZdd3NErSVyOEYzJfNOCGtafGk1//gibTwQXGlSXmAKuErqMp09pyk9aqQRSYzm1AQfBw==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.69.tgz", + "integrity": "sha512-pClUoJF5wdC9AvD0mc15G9JffL1Q85nuH1rLSQPRkGmGmQOtRjw5E9xNbanz7oFUiPbjH7xcAXUjVAcf7tdgPQ==", "cpu": [ "riscv64" ], @@ -1350,9 +1363,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.68.tgz", - "integrity": "sha512-Pa/I36VE3j57I3Obhrr+J48KGFfkZk2cJN/2NmW/vCgmoF7kCP6aTVq5n+cGdGWLd/cN9CJ9JvNwEoMRDghu0g==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.69.tgz", + "integrity": "sha512-96X3bFAmzemfw84Ts6Jg/omL86uuynvK06MWGR/mp3JYNumY9RXofA14eF/kJIYelbYFWXcwpbcBR71lJ6G/YQ==", "cpu": [ "x64" ], @@ -1366,9 +1379,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.68.tgz", - "integrity": "sha512-9c6rkc5195wNxuUHJdf4/mmnq433OQey9TNvQ9LspJazvHbfSkTij8wtKjASVQsJyPDva4fkWOeV/OQ7cLw0GQ==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.69.tgz", + "integrity": "sha512-2QTsEFO72Kwkj53W9hc5y1FAUvdGx0V+pjJB+9oQF6Ys9+y989GyPIl5wZDzeh8nIJW6koZZ1eFa8pD+pA5BFQ==", "cpu": [ "x64" ], @@ -1382,9 +1395,9 @@ } }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.68", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.68.tgz", - "integrity": "sha512-Fc5Dez23u0FoSATurT6/w1oMytiRnKWEinHivdMvXpge6nG4YvhrASrtqMk8dGJMVQpHr8QJYF45rOrx2YU2Aw==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.69.tgz", + "integrity": "sha512-Q4YA8kVnKarApBVLu7F8icGlIfSll5Glswo5hY6gPS4Is2dCI8+ig9OeDM8RlwYevUIxKq8lZBypN8Q1iLAQ7w==", "cpu": [ "x64" ], @@ -1436,9 +1449,9 @@ } }, "node_modules/@pkgr/core": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.0.tgz", - "integrity": "sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.2.tgz", + "integrity": "sha512-25L86MyPvnlQoX2MTIV2OiUcb6vJ6aRbFa9pbwByn95INKD5mFH2smgjDhq+fwJoqAgvgbdJLj6Tz7V9X5CFAQ==", "dev": true, "license": "MIT", "engines": { @@ -1767,13 +1780,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", - "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", + "version": "22.14.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", + "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/trusted-types": { @@ -1784,17 +1797,17 @@ "optional": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", - "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.1.tgz", + "integrity": "sha512-ba0rr4Wfvg23vERs3eB+P3lfj2E+2g3lhWcCVukUuhtcdUx5lSIFZlGFEBHKr+3zizDa/TvZTptdNHVZWAkSBg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/type-utils": "8.28.0", - "@typescript-eslint/utils": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/scope-manager": "8.29.1", + "@typescript-eslint/type-utils": "8.29.1", + "@typescript-eslint/utils": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1814,16 +1827,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz", - "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.1.tgz", + "integrity": "sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/typescript-estree": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/scope-manager": "8.29.1", + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/typescript-estree": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1", "debug": "^4.3.4" }, "engines": { @@ -1839,14 +1852,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz", - "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.1.tgz", + "integrity": "sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0" + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1857,14 +1870,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz", - "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.1.tgz", + "integrity": "sha512-DkDUSDwZVCYN71xA4wzySqqcZsHKic53A4BLqmrWFFpOpNSoxX233lwGu/2135ymTCR04PoKiEEEvN1gFYg4Tw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.28.0", - "@typescript-eslint/utils": "8.28.0", + "@typescript-eslint/typescript-estree": "8.29.1", + "@typescript-eslint/utils": "8.29.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -1881,9 +1894,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz", - "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.1.tgz", + "integrity": "sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ==", "dev": true, "license": "MIT", "engines": { @@ -1895,14 +1908,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz", - "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.1.tgz", + "integrity": "sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1922,16 +1935,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz", - "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.1.tgz", + "integrity": "sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/typescript-estree": "8.28.0" + "@typescript-eslint/scope-manager": "8.29.1", + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/typescript-estree": "8.29.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1946,13 +1959,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz", - "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.1.tgz", + "integrity": "sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/types": "8.29.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2415,9 +2428,9 @@ "license": "MIT" }, "node_modules/confbox": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.1.tgz", - "integrity": "sha512-hkT3yDPFbs95mNCy1+7qNKC6Pro+/ibzYxtM2iqEigpf0sVw+bg4Zh9/snjsBcf990vfIsg5+1U7VyiyBb3etg==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", "license": "MIT" }, "node_modules/cose-base": { @@ -2451,9 +2464,9 @@ } }, "node_modules/cytoscape": { - "version": "3.31.1", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.1.tgz", - "integrity": "sha512-Hx5Mtb1+hnmAKaZZ/7zL1Y5HTFYOjdDswZy/jD+1WINRU8KVi1B7+vlHdsTwY+VCFucTreoyu1RDzQJ9u0d2Hw==", + "version": "3.31.2", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.2.tgz", + "integrity": "sha512-/eOXg2uGdMdpGlEes5Sf6zE+jUG+05f3htFNQIxLxduOH/SsaUZiPBfAwP1btVIVzsnhiNOdi+hvDRLYfMZjGw==", "license": "MIT", "engines": { "node": ">=0.10" @@ -3102,9 +3115,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", - "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.5.tgz", + "integrity": "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -3326,19 +3339,19 @@ } }, "node_modules/eslint": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz", - "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", + "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", + "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.0", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.23.0", + "@eslint/js": "9.24.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -3387,9 +3400,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz", - "integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz", + "integrity": "sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA==", "dev": true, "license": "MIT", "bin": { @@ -3528,14 +3541,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.5.tgz", - "integrity": "sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz", + "integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.10.2" + "synckit": "^0.11.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -4639,9 +4652,9 @@ } }, "node_modules/katex": { - "version": "0.16.21", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", - "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", + "version": "0.16.22", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", + "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -4774,9 +4787,9 @@ "license": "MIT" }, "node_modules/marked": { - "version": "15.0.7", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.7.tgz", - "integrity": "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==", + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.8.tgz", + "integrity": "sha512-rli4l2LyZqpQuRve5C0rkn6pj3hT8EWPC+zkAxFTAJLxRbENfTAhEQq9itrmf1Y81QtAX5D/MYlGlIomNgj9lA==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -4933,9 +4946,9 @@ "license": "MIT" }, "node_modules/npm-check-updates": { - "version": "17.1.16", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-17.1.16.tgz", - "integrity": "sha512-9nohkfjLRzLfsLVGbO34eXBejvrOOTuw5tvNammH73KEFG5XlFoi3G2TgjTExHtnrKWCbZ+mTT+dbNeSjASIPw==", + "version": "17.1.18", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-17.1.18.tgz", + "integrity": "sha512-bkUy2g4v1i+3FeUf5fXMLbxmV95eG4/sS7lYE32GrUeVgQRfQEk39gpskksFunyaxQgTIdrvYbnuNbO/pSUSqw==", "license": "Apache-2.0", "bin": { "ncu": "build/cli.js", @@ -5808,20 +5821,20 @@ } }, "node_modules/synckit": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz", - "integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.3.tgz", + "integrity": "sha512-szhWDqNNI9etJUvbZ1/cx1StnZx8yMmFxme48SwR4dty4ioSY50KEZlpv0qAfgc1fpRzuh9hBXEzoCpJ779dLg==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.0", + "@pkgr/core": "^0.2.1", "tslib": "^2.8.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/synckit" } }, "node_modules/tinyexec": { @@ -5831,9 +5844,9 @@ "license": "MIT" }, "node_modules/tinymce": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-7.7.2.tgz", - "integrity": "sha512-GX7Jd0ac9ph3QM2yei4uOoxytKX096CyG6VkkgQNikY39T6cDldoNgaqzHHlcm62WtdBMCd7Ch+PYaRnQo+NLA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-7.8.0.tgz", + "integrity": "sha512-MUER5MWV9mkOB4expgbWknh/C5ZJvOXQlMVSx4tJxTuYtcUCDB6bMZ34fWNOIc8LvrnXmGHGj0eGQuxjQyRgrA==", "license": "GPL-2.0-or-later" }, "node_modules/to-regex-range": { @@ -5983,9 +5996,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5997,9 +6010,9 @@ } }, "node_modules/ufo": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", - "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", "license": "MIT" }, "node_modules/unbox-primitive": { @@ -6022,9 +6035,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, diff --git a/client/package.json b/client/package.json index d48b721d..251f34af 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "codechat-editor-client", - "version": "0.1.15", + "version": "0.1.16", "description": "The CodeChat Editor Client, part of a web-based literate programming editor (the CodeChat Editor).", "homepage": "https://github.com/bjones1/CodeChat_Editor", "type": "module", diff --git a/docs/changelog.md b/docs/changelog.md index 2e49090e..b2d5d2c2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -21,6 +21,10 @@ Changelog * [Github master](https://github.com/bjones1/CodeChat_Editor): * No changes. +* v0.1.16, 2025-Apr-11: + * Fix to allow running inside a GitHub Codespace. + * Add: new command-line option to open a file/directory -- + `codechat-editor-server start [filename/diretory]`. * v0.1.15, 2025-Mar-31: * Correctly view binary files (images, PDFs, etc.) within a project. * Include support for viewing PDF files in VSCode. diff --git a/docs/design.md b/docs/design.md index f999604e..865b9d5e 100644 --- a/docs/design.md +++ b/docs/design.md @@ -13,11 +13,10 @@ To build from source 4. In theĀ `server/` directory: 1. Run `./bt install --dev`. 2. Run `./bt build`. -5. OpenĀ `http://localhost:8080` in your browser. -6. Open the file `README.md`. + 3. Run `cargo run -- start ../README.md`. -Use `./bt` tool's options update all libraries (`updated`), run all tests -(`check`), and more. +Use `./bt` tool's options update all libraries (`update`), run all tests +(`test`), and more. Vision ------------------------- diff --git a/extensions/VSCode/package-lock.json b/extensions/VSCode/package-lock.json index cc6c7e3f..243bed49 100644 --- a/extensions/VSCode/package-lock.json +++ b/extensions/VSCode/package-lock.json @@ -1,12 +1,12 @@ { "name": "codechat-editor-client", - "version": "0.1.15", + "version": "0.1.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codechat-editor-client", - "version": "0.1.15", + "version": "0.1.16", "license": "GPL-3.0-only", "dependencies": { "escape-html": "^1", @@ -167,22 +167,22 @@ } }, "node_modules/@azure/msal-browser": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.9.1.tgz", - "integrity": "sha512-GTKj/2xvgD918xULWRwoJ3kiCCZNzeopxa/nDfMC4o6KzrnuWbT3K1AtIFUxok9yC6VrUOgIZXMygky06xDA1g==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.10.0.tgz", + "integrity": "sha512-48X2VwOtHk8A1CI00E8tAqko0+3qQh53u5bOPySzdojL3T/Ad4GgRnN0c0oLJ1/PcTm4D4QybHYG3LBOX0l3/g==", "dev": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "15.4.0" + "@azure/msal-common": "15.5.0" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.4.0.tgz", - "integrity": "sha512-reeIUDXt6Xc+FpCBDEbUFQWvJ6SjE0JwsGYIfa3ZCR6Tpzjc9J1v+/InQgfCeJzfTRd7PDJVxI6TSzOmOd7+Ag==", + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.5.0.tgz", + "integrity": "sha512-u97AJ6m4PB24/Plms9e9iydRcOaxxrHWkan1px5GeWGJfakY1D/r1DmY1+Typ8zWC/5JbNzH1GYpXrorPymz5g==", "dev": true, "license": "MIT", "engines": { @@ -190,13 +190,13 @@ } }, "node_modules/@azure/msal-node": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.4.1.tgz", - "integrity": "sha512-VlW6ygnKBIqUKIHnA/ubQ+F3rZ8aW3K6VA1bpZ90Ln0vlE4XaA6yGB/FibPJxet7gWinAG1oSpQqPN/PL9AqIw==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.5.0.tgz", + "integrity": "sha512-9cLUmcOZ5FODz3uAhS2C9A1U7xDUTCHVcaNQBYpOd5qCKdKM6ft/ydAfw27vEntuaDgnh5jytOAKsEzEbtoQ1Q==", "dev": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "15.4.0", + "@azure/msal-common": "15.5.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" }, @@ -234,9 +234,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -273,9 +273,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz", - "integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -344,9 +344,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", - "integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", + "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", "dev": true, "license": "MIT", "engines": { @@ -364,19 +364,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.12.0", + "@eslint/core": "^0.13.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -535,26 +548,26 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", - "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", + "version": "22.14.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", + "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/vscode": { - "version": "1.98.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.98.0.tgz", - "integrity": "sha512-+KuiWhpbKBaG2egF+51KjbGWatTH5BbmWQjSLMDCssb4xF8FJnW4nGH4nuAdOOfMbpD0QlHtI+C3tPq+DoKElg==", + "version": "1.99.1", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.99.1.tgz", + "integrity": "sha512-cQlqxHZ040ta6ovZXnXRxs3fJiTmlurkIWOfZVcLSZPcm9J4ikFpXuB7gihofGn5ng+kDVma5EmJIclfk0trPQ==", "dev": true, "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, "license": "MIT", "dependencies": { @@ -562,17 +575,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", - "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.1.tgz", + "integrity": "sha512-ba0rr4Wfvg23vERs3eB+P3lfj2E+2g3lhWcCVukUuhtcdUx5lSIFZlGFEBHKr+3zizDa/TvZTptdNHVZWAkSBg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/type-utils": "8.28.0", - "@typescript-eslint/utils": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/scope-manager": "8.29.1", + "@typescript-eslint/type-utils": "8.29.1", + "@typescript-eslint/utils": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -592,16 +605,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz", - "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.1.tgz", + "integrity": "sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/typescript-estree": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/scope-manager": "8.29.1", + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/typescript-estree": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1", "debug": "^4.3.4" }, "engines": { @@ -617,14 +630,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz", - "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.1.tgz", + "integrity": "sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0" + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -635,14 +648,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz", - "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.1.tgz", + "integrity": "sha512-DkDUSDwZVCYN71xA4wzySqqcZsHKic53A4BLqmrWFFpOpNSoxX233lwGu/2135ymTCR04PoKiEEEvN1gFYg4Tw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.28.0", - "@typescript-eslint/utils": "8.28.0", + "@typescript-eslint/typescript-estree": "8.29.1", + "@typescript-eslint/utils": "8.29.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -659,9 +672,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz", - "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.1.tgz", + "integrity": "sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ==", "dev": true, "license": "MIT", "engines": { @@ -673,14 +686,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz", - "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.1.tgz", + "integrity": "sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/visitor-keys": "8.28.0", + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/visitor-keys": "8.29.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -700,16 +713,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz", - "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.1.tgz", + "integrity": "sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.28.0", - "@typescript-eslint/types": "8.28.0", - "@typescript-eslint/typescript-estree": "8.28.0" + "@typescript-eslint/scope-manager": "8.29.1", + "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/typescript-estree": "8.29.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -724,13 +737,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz", - "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==", + "version": "8.29.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.1.tgz", + "integrity": "sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/types": "8.29.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -755,9 +768,9 @@ } }, "node_modules/@vscode/vsce": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.3.1.tgz", - "integrity": "sha512-aZ+0UilLsSidWcY6qjQRmsNgJXfFvE3ihj0u9oNLMejFGBBYORTRua0KHx3kx+RW5pxvza2OepzTpct2OdhTUA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.3.2.tgz", + "integrity": "sha512-XQ4IhctYalSTMwLnMS8+nUaGbU7v99Qm2sOoGfIEf2QC7jpiLXZZMh7NwArEFsKX4gHTJLx0/GqAUlCdC3gKCw==", "dev": true, "license": "MIT", "dependencies": { @@ -2112,19 +2125,19 @@ } }, "node_modules/eslint": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz", - "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", + "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", + "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.0", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.23.0", + "@eslint/js": "9.24.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2173,9 +2186,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz", - "integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz", + "integrity": "sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA==", "dev": true, "license": "MIT", "bin": { @@ -5793,9 +5806,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5850,9 +5863,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, diff --git a/extensions/VSCode/package.json b/extensions/VSCode/package.json index e6fb53b7..0f05b251 100644 --- a/extensions/VSCode/package.json +++ b/extensions/VSCode/package.json @@ -1,6 +1,6 @@ { "name": "codechat-editor-client", - "version": "0.1.15", + "version": "0.1.16", "publisher": "CodeChat", "engines": { "vscode": "^1.61.0" diff --git a/server/Cargo.lock b/server/Cargo.lock index d8097cba..e01b0488 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -458,9 +458,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.11.3" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", "regex-automata", @@ -499,9 +499,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.17" +version = "1.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" dependencies = [ "jobserver", "libc", @@ -530,9 +530,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.34" +version = "4.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" +checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" dependencies = [ "clap_builder", "clap_derive", @@ -540,9 +540,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.34" +version = "4.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" +checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" dependencies = [ "anstream", "anstyle", @@ -570,7 +570,7 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "codechat-editor-server" -version = "0.1.15" +version = "0.1.16" dependencies = [ "actix-files", "actix-http", @@ -708,9 +708,9 @@ checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" [[package]] name = "deranged" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] @@ -823,9 +823,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", "windows-sys 0.59.0", @@ -866,9 +866,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "miniz_oxide", @@ -1118,9 +1118,9 @@ checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" [[package]] name = "iana-time-zone" -version = "0.1.62" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1303,9 +1303,9 @@ checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] name = "indexmap" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown", @@ -1370,10 +1370,11 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ + "getrandom 0.3.2", "libc", ] @@ -1438,9 +1439,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" @@ -1552,9 +1553,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] @@ -1975,9 +1976,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ "bitflags 2.9.0", ] @@ -2034,9 +2035,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ "bitflags 2.9.0", "errno", @@ -2199,9 +2200,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" @@ -2402,9 +2403,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.1" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", @@ -2829,11 +2830,37 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.52.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-targets", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] @@ -2842,6 +2869,24 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/server/Cargo.toml b/server/Cargo.toml index 733e6bb7..ff56c08d 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -31,7 +31,7 @@ license = "GPL-3.0-only" name = "codechat-editor-server" readme = "../README.md" repository = "https://github.com/bjones1/CodeChat_Editor" -version = "0.1.15" +version = "0.1.16" # This library allows other packages to use core CodeChat Editor features. [lib] diff --git a/server/src/capture.rs b/server/src/capture.rs index 50f3a6e8..14e5efd0 100644 --- a/server/src/capture.rs +++ b/server/src/capture.rs @@ -15,7 +15,7 @@ // [http://www.gnu.org/licenses](http://www.gnu.org/licenses). /// # `Capture.rs` -- Capture CodeChat Editor Events // ## Submodules - +// // ## Imports // // Standard library diff --git a/server/src/main.rs b/server/src/main.rs index 8e90cbc8..f8606dfa 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -20,8 +20,9 @@ // // ### Standard library use std::{ - env, - io::Read, + env, fs, + io::{self, Read}, + path::PathBuf, process::{Child, Command, Stdio}, time::SystemTime, }; @@ -33,7 +34,7 @@ use clap::{Parser, Subcommand}; use log::LevelFilter; // ### Local -use code_chat_editor::webserver::{self, IP_ADDRESS}; +use code_chat_editor::webserver::{self, GetServerUrlError, IP_ADDRESS, path_to_url}; // Data structures // --------------- @@ -73,7 +74,10 @@ enum Commands { log: Option, }, /// Start the webserver in a child process then exit. - Start, + Start { + /// Open a web browser, showing the provided file or directory. + open: Option, + }, /// Stop the webserver child process. Stop, } @@ -96,7 +100,7 @@ impl Cli { webserver::configure_logger(log.unwrap_or(LevelFilter::Info)); webserver::main(self.port).unwrap(); } - Commands::Start => { + Commands::Start { open } => { // Poll the server to ensure it starts. let mut process: Option = None; let now = SystemTime::now(); @@ -111,6 +115,15 @@ impl Cli { let body = response.as_str().unwrap_or("Non-text body"); if status_code == 200 && body == "pong" { println!("Server started."); + // Open a web browser if requested. + if let Some(open_path) = open { + let address = get_server_url(self.port)?; + let open_path = fs::canonicalize(open_path)?; + let open_path = + path_to_url(&format!("{address}/fw/fsb"), None, &open_path); + open::that_detached(&open_path)?; + } + return Ok(()); } else { eprintln!( @@ -119,7 +132,17 @@ impl Cli { } } Err(err) => { - eprintln!("Failed to start server: {err}"); + // Use this to skip the print from a nested if statement. + 'err_print: { + // Ignore a connection refused error. + if let minreq::Error::IoError(io_error) = &err { + if io_error.kind() == io::ErrorKind::ConnectionRefused { + break 'err_print; + } + } + eprintln!("Failed to connect to server: {err}"); + break 'err_print; + } } } @@ -242,6 +265,11 @@ fn main() -> Result<(), Box> { Ok(()) } +#[tokio::main] +async fn get_server_url(port: u16) -> Result { + return code_chat_editor::webserver::get_server_url(port).await; +} + #[cfg(test)] mod test { use super::Cli; diff --git a/server/src/testing_logger.rs b/server/src/testing_logger.rs index 60b39ce1..9555dd46 100644 --- a/server/src/testing_logger.rs +++ b/server/src/testing_logger.rs @@ -140,7 +140,7 @@ static TEST_LOGGER: TestingLogger = TestingLogger {}; pub fn setup() { FIRST_TEST.call_once(|| { log::set_logger(&TEST_LOGGER) - .map(|()| log::set_max_level(LevelFilter::Trace)) + .map(|()| log::set_max_level(LevelFilter::Debug)) .unwrap(); }); LOG_RECORDS.with(|records| { diff --git a/server/src/webserver.rs b/server/src/webserver.rs index 5e82249c..f41381e5 100644 --- a/server/src/webserver.rs +++ b/server/src/webserver.rs @@ -61,6 +61,7 @@ use serde_json; use tokio::{ fs::File, io::AsyncReadExt, + process::Command, select, sync::{ mpsc::{Receiver, Sender}, @@ -1506,7 +1507,7 @@ fn try_canonicalize(file_path: &str) -> Result { } // Given a file path, convert it to a URL, encoding as necessary. -fn path_to_url(prefix: &str, connection_id: &str, file_path: &Path) -> String { +pub fn path_to_url(prefix: &str, connection_id: Option<&str>, file_path: &Path) -> String { // First, convert the path to use forward slashes. let pathname = simplified(file_path) .to_slash() @@ -1521,7 +1522,12 @@ fn path_to_url(prefix: &str, connection_id: &str, file_path: &Path) -> String { // On Windows, path names start with a drive letter. On Linux/OS X, they // start with a forward slash -- don't put a double forward slash in the // resulting path. - format!("{prefix}/{connection_id}/{}", drop_leading_slash(&pathname)) + let pathname = drop_leading_slash(&pathname); + if let Some(connection_id) = connection_id { + format!("{prefix}/{connection_id}/{pathname}") + } else { + format!("{prefix}/{pathname}") + } } // Given a string (which is probably a pathname), drop the leading slash if it's @@ -1575,3 +1581,44 @@ fn escape_html(unsafe_text: &str) -> String { .replace('<', "<") .replace('>', ">") } + +// This lists all errors produced by calling `get_server_url`. TODO: rework and re-think the overall error framework. How should I group errors? +#[derive(Debug, thiserror::Error)] +pub enum GetServerUrlError { + #[error("Expected environment variable not found.")] + Io(#[from] env::VarError), + #[error("Error running process.")] + Process(#[from] std::io::Error), + #[error("Process exit status {0:?} indicates error.")] + NonZeroExitStatus(Option), +} + +// Determine the URL for this server; supports running locally and in a GitHub Codespace. +pub async fn get_server_url(port: u16) -> Result { + // This is always true in a GitHub Codespace per the [docs](https://docs.github.com/en/codespaces/developing-in-codespaces/default-environment-variables-for-your-codespace#list-of-default-environment-variables). + if env::var("CODESPACES") == Ok("true".to_string()) { + let codespace_name = env::var("CODESPACE_NAME")?; + let codespace_domain = env::var("GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN")?; + // Use the GitHub CLI to [forward this port](https://docs.github.com/en/codespaces/developing-in-a-codespace/using-github-codespaces-with-github-cli#modify-ports-in-a-codespace). + let status = Command::new("gh") + .args([ + "codespace", + "ports", + "visibility", + &format!("{port}:public"), + "-c", + &codespace_name, + ]) + .status() + .await?; + if !status.success() { + Err(GetServerUrlError::NonZeroExitStatus(status.code())) + } else { + Ok(format!( + "https://{codespace_name}-{port}.{codespace_domain}" + )) + } + } else { + Ok(format!("http://{IP_ADDRESS}:{port}")) + } +} diff --git a/server/src/webserver/filewatcher.rs b/server/src/webserver/filewatcher.rs index e1c190f9..ff97cebf 100644 --- a/server/src/webserver/filewatcher.rs +++ b/server/src/webserver/filewatcher.rs @@ -396,7 +396,7 @@ async fn processing_task(file_path: &Path, app_state: web::Data, conne // Provide it a file to open. let mut id: f64 = 0.0; if let Some(cfp) = ¤t_filepath { - let url_pathbuf = path_to_url("/fw/fsc", &connection_id.to_string(), cfp); + let url_pathbuf = path_to_url("/fw/fsc", Some(&connection_id.to_string()), cfp); queue_send!(to_websocket_tx.send(EditorMessage { id, message: EditorMessageContents::CurrentFile(url_pathbuf, None) diff --git a/server/src/webserver/tests.rs b/server/src/webserver/tests.rs index b49c3a4a..edba672a 100644 --- a/server/src/webserver/tests.rs +++ b/server/src/webserver/tests.rs @@ -13,8 +13,8 @@ // You should have received a copy of the GNU General Public License along with // the CodeChat Editor. If not, see // [http://www.gnu.org/licenses](http://www.gnu.org/licenses). -/// `test.rs` -- Unit tests for the webserver -/// ========================================= +/// `test.rs` -- Unit tests for the vscode interface +/// ================================================ // Imports // ------- use std::{ @@ -103,7 +103,7 @@ fn test_path_to_url() { let mut file_path = test_dir.clone(); file_path.push("test spaces.py"); - let url = path_to_url("/a/b", "conn1", &file_path); + let url = path_to_url("/a/b", Some("conn1"), &file_path); assert_starts_with!(url, "/a/b/conn1/"); assert_ends_with!(url, "test_path_to_url/test%20spaces.py"); // There shouldn't be a double forward slash in the name. diff --git a/server/src/webserver/vscode.rs b/server/src/webserver/vscode.rs index 9f5495ee..e3a29d75 100644 --- a/server/src/webserver/vscode.rs +++ b/server/src/webserver/vscode.rs @@ -17,6 +17,11 @@ /// `vscode.rs` -- Implement server-side functionality for the Visual Studio /// Code IDE /// ======================================================================== +// Submodules +// ---------- +#[cfg(test)] +pub mod tests; + // Imports // ------- // @@ -48,8 +53,9 @@ use crate::{ queue_send, webserver::{ INITIAL_MESSAGE_ID, MESSAGE_ID_INCREMENT, ProcessingTaskHttpRequest, ResultOkTypes, - UpdateMessageContents, escape_html, file_to_response, filesystem_endpoint, html_wrapper, - make_simple_http_response, path_to_url, try_canonicalize, try_read_as_text, url_to_path, + UpdateMessageContents, escape_html, file_to_response, filesystem_endpoint, get_server_url, + html_wrapper, make_simple_http_response, path_to_url, try_canonicalize, try_read_as_text, + url_to_path, }, }; @@ -196,6 +202,13 @@ pub async fn vscode_ide_websocket( // Send the HTML for the internal browser. let port = app_state_task.port; + let address = match get_server_url(port).await { + Ok(address) => address, + Err(err) => { + error!("{err:?}"); + break 'task; + } + }; let client_html = formatdoc!( r#" @@ -203,7 +216,7 @@ pub async fn vscode_ide_websocket( - + "# ); @@ -457,7 +470,7 @@ pub async fn vscode_ide_websocket( queue_send!(to_client_tx.send(EditorMessage { id: ide_message.id, message: EditorMessageContents::CurrentFile( - path_to_url("/vsc/fs", &connection_id_task, &clean_file_path), Some(true) + path_to_url("/vsc/fs", Some(&connection_id_task), &clean_file_path), Some(true) ) })); current_file = file_path.into(); @@ -709,942 +722,3 @@ async fn serve_vscode_fs( ) -> HttpResponse { filesystem_endpoint(request_path, &req, &app_state).await } - -// Tests -// ----- -#[cfg(test)] -mod test { - use std::{ - fs, - io::Error, - path::{self, Path, PathBuf}, - thread, - time::{Duration, SystemTime}, - }; - - use actix_rt::task::JoinHandle; - use assert_fs::TempDir; - use assertables::{assert_ends_with, assert_starts_with}; - use dunce::simplified; - use futures_util::{SinkExt, StreamExt}; - use lazy_static::lazy_static; - use minreq; - use path_slash::PathExt; - use tokio::{ - io::{AsyncRead, AsyncWrite}, - net::TcpStream, - select, - time::sleep, - }; - use tokio_tungstenite::{ - MaybeTlsStream, WebSocketStream, connect_async, - tungstenite::{http::StatusCode, protocol::Message}, - }; - - use super::super::{ - EditorMessage, EditorMessageContents, IP_ADDRESS, IdeType, run_server, tests::IP_PORT, - }; - use crate::{ - cast, - processing::{CodeChatForWeb, CodeMirror, SourceFileMetadata}, - test_utils::{_prep_test_dir, check_logger_errors, configure_testing_logger}, - webserver::{ResultOkTypes, UpdateMessageContents, drop_leading_slash}, - }; - - lazy_static! { - // Run a single webserver for all tests. - static ref WEBSERVER_HANDLE: JoinHandle> = - actix_rt::spawn(async move { run_server(IP_PORT).await }); - } - - // Send a message via a websocket. - async fn send_message( - ws_stream: &mut WebSocketStream, - message: &EditorMessage, - ) { - ws_stream - .send(Message::Text( - serde_json::to_string(message).unwrap().into(), - )) - .await - .unwrap(); - } - - // Read a message from a websocket. - async fn read_message( - ws_stream: &mut WebSocketStream, - ) -> EditorMessage { - let now = SystemTime::now(); - let msg_txt = loop { - let msg = select! { - data = ws_stream.next() => data.unwrap().unwrap(), - _ = sleep(Duration::from_secs(3) - now.elapsed().unwrap()) => panic!("Timeout waiting for message") - }; - match msg { - Message::Close(_) => panic!("Unexpected close message."), - Message::Ping(_) => ws_stream.send(Message::Pong(vec![].into())).await.unwrap(), - Message::Pong(_) => panic!("Unexpected pong message."), - Message::Text(txt) => break txt, - Message::Binary(_) => panic!("Unexpected binary message."), - Message::Frame(_) => panic!("Unexpected frame message."), - } - }; - serde_json::from_str(&msg_txt) - .unwrap_or_else(|_| panic!("Unable to convert '{msg_txt}' to JSON.")) - } - - type WebSocketStreamTcp = WebSocketStream>; - - async fn connect_async_server(prefix: &str, connection_id: &str) -> WebSocketStreamTcp { - connect_async(format!( - "ws://{IP_ADDRESS}:{IP_PORT}{prefix}/{connection_id}", - )) - .await - .expect("Failed to connect") - .0 - } - - async fn connect_async_ide(connection_id: &str) -> WebSocketStreamTcp { - connect_async_server("/vsc/ws-ide", connection_id).await - } - - async fn connect_async_client(connection_id: &str) -> WebSocketStreamTcp { - connect_async_server("/vsc/ws-client", connection_id).await - } - - // Open the Client in the VSCode browser. (Although, for testing, the Client - // isn't opened at all.) - // - // Message ids at function end: IDE - 4, Server - 3, Client - 2. - async fn open_client(ws_ide: &mut WebSocketStream) { - // 1. Send the `Opened` message. - // - // Message ids: IDE - 1->4, Server - 0, Client - 2. - send_message( - ws_ide, - &EditorMessage { - id: 1.0, - message: EditorMessageContents::Opened(IdeType::VSCode(true)), - }, - ) - .await; - - // Get the response. It should be success. - assert_eq!( - read_message(ws_ide).await, - EditorMessage { - id: 1.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), - } - ); - - // 2. Next, wait for the next message -- the HTML. - // - // Message ids: IDE - 4, Server - 0->3, Client - 2. - let em = read_message(ws_ide).await; - assert_starts_with!( - cast!(&em.message, EditorMessageContents::ClientHtml), - "" - ); - assert_eq!(em.id, 0.0); - - // Send a success response to this message. - send_message( - ws_ide, - &EditorMessage { - id: 0.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), - }, - ) - .await; - } - - // Perform all the setup for testing the Server via IDE and Client - // websockets. This should be invoked by the `prep_test!` macro; otherwise, - // test files won't be found. - async fn _prep_test( - connection_id: &str, - test_full_name: &str, - ) -> (TempDir, PathBuf, WebSocketStreamTcp, WebSocketStreamTcp) { - configure_testing_logger(); - let (temp_dir, test_dir) = _prep_test_dir(test_full_name); - // Ensure the webserver is running. - let _ = &*WEBSERVER_HANDLE; - let now = SystemTime::now(); - while now.elapsed().unwrap().as_millis() < 100 { - if minreq::get(format!("http://{IP_ADDRESS}:{IP_PORT}/ping",)) - .send() - .is_ok() - { - break; - } - sleep(Duration::from_millis(10)).await; - } - - // Connect to the VSCode IDE websocket. - let ws_ide = connect_async_ide(connection_id).await; - let ws_client = connect_async_client(connection_id).await; - - (temp_dir, test_dir, ws_ide, ws_client) - } - - // This calls `_prep_test` with the current function name. It must be a - // macro, so that it's called with the test function's name; calling it - // inside `_prep_test` would give the wrong name. - macro_rules! prep_test { - ($connection_id: ident) => {{ - use crate::function_name; - _prep_test($connection_id, function_name!()) - }}; - } - - // Test incorrect inputs: two connections with the same ID, sending the - // wrong first message. - #[actix_web::test] - async fn test_vscode_ide_websocket1() { - let connection_id = "test-connection-id1"; - let (_, _, mut ws_ide, _) = prep_test!(connection_id).await; - - // Start a second connection; verify that it fails. - let err = connect_async(format!( - "ws://{IP_ADDRESS}:{IP_PORT}/vsc/ws-ide/{connection_id}", - )) - .await - .expect_err("Should fail to connect"); - let response = cast!(err, tokio_tungstenite::tungstenite::Error::Http); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - - // Note: we can't check the logs, since the server runs in a separate - // thread. Changing the logger to log across threads means we get logs - // from other tests (which run in parallel by default). The benefit of - // running all tests single-threaded plus fixing the logger is low. - // - // Send a message that's not an `Opened` message. - send_message( - &mut ws_ide, - &EditorMessage { - id: 0.0, - message: EditorMessageContents::Update(UpdateMessageContents { - file_path: "".to_string(), - contents: None, - cursor_position: None, - scroll_position: None, - }), - }, - ) - .await; - - // Get the response. It should be an error. - let em = read_message(&mut ws_ide).await; - let result = cast!(em.message, EditorMessageContents::Result); - - assert_starts_with!(cast!(&result, Err), "Unexpected message"); - - // Next, expect the websocket to be closed. - let err = &ws_ide.next().await.unwrap().unwrap(); - assert_eq!(*err, Message::Close(None)); - - check_logger_errors(0); - } - - // Test opening the Client in an external browser. - #[actix_web::test] - async fn test_vscode_ide_websocket2() { - let connection_id = "test-connection-id2"; - let (_, _, mut ws_ide, _) = prep_test!(connection_id).await; - - // Send the `Opened` message. - send_message( - &mut ws_ide, - &EditorMessage { - id: 0.0, - message: EditorMessageContents::Opened(IdeType::VSCode(false)), - }, - ) - .await; - - // Get the response. It should be success. - let em = read_message(&mut ws_ide).await; - assert_eq!( - cast!(em.message, EditorMessageContents::Result), - Ok(ResultOkTypes::Void) - ); - - check_logger_errors(0); - } - - // Fetch a non-existent file and verify the response returns an error. - #[actix_web::test] - async fn test_vscode_ide_websocket3() { - let connection_id = "test-connection-id3"; - let (temp_dir, test_dir, mut ws_ide, _) = prep_test!(connection_id).await; - open_client(&mut ws_ide).await; - - let file_path = test_dir.join("none.py"); - let file_path_str = drop_leading_slash(&file_path.to_slash().unwrap()).to_string(); - - // Do this is a thread, since the request generates a message that - // requires a response in order to complete. - let file_path_str_thread = file_path_str.clone(); - let join_handle = thread::spawn(move || { - assert_eq!( - minreq::get(format!( - "http://localhost:8080/vsc/fs/{connection_id}/{}", - file_path_str_thread - )) - .send() - .unwrap() - .status_code, - 404 - ) - }); - - // The HTTP request produces a `LoadFile` message. - // - // Message ids: IDE - 4, Server - 3->6, Client - 2. - let em = read_message(&mut ws_ide).await; - let msg = cast!(em.message, EditorMessageContents::LoadFile); - // Compare these as strings -- we want to ensure the path separator is - // correct for the current platform. - assert_eq!(file_path.to_string_lossy(), msg.to_string_lossy()); - assert_eq!(em.id, 3.0); - - // Reply to the `LoadFile` message -- the file isn't present. - send_message( - &mut ws_ide, - &EditorMessage { - id: 3.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::LoadFile(None))), - }, - ) - .await; - - // This should cause the HTTP request to complete by receiving the - // response (file not found). - join_handle.join().unwrap(); - - check_logger_errors(0); - // Report any errors produced when removing the temporary directory. - temp_dir.close().unwrap(); - } - - // Fetch a file that exists, but using backslashes. This should still fail, - // even on Windows. - #[actix_web::test] - async fn test_vscode_ide_websocket3a() { - let connection_id = "test-connection-id3a"; - let (temp_dir, test_dir, mut ws_ide, _) = prep_test!(connection_id).await; - open_client(&mut ws_ide).await; - - let file_path = test_dir.join("test.py"); - // Force the path separator to be Window-style for this test, even on - // non-Windows platforms. - let file_path_str = file_path.to_str().unwrap().to_string().replace("/", "\\"); - - // Do this is a thread, since the request generates a message that - // requires a response in order to complete. - let file_path_str_thread = file_path_str.clone(); - let join_handle = thread::spawn(move || { - assert_eq!( - minreq::get(format!( - "http://localhost:8080/vsc/fs/{connection_id}/{}", - file_path_str_thread - )) - .send() - .unwrap() - .status_code, - 404 - ) - }); - - // The HTTP request produces a `LoadFile` message. - // - // Message ids: IDE - 4, Server - 3->6, Client - 2. - let em = read_message(&mut ws_ide).await; - cast!(em.message, EditorMessageContents::LoadFile); - // Skip comparing the file names, due to the backslash encoding. - assert_eq!(em.id, 3.0); - - // Reply to the `LoadFile` message -- the file isn't present. - send_message( - &mut ws_ide, - &EditorMessage { - id: 3.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::LoadFile(None))), - }, - ) - .await; - - // This should cause the HTTP request to complete by receiving the - // response (file not found). - join_handle.join().unwrap(); - - check_logger_errors(0); - // Report any errors produced when removing the temporary directory. - temp_dir.close().unwrap(); - } - - // Send a `CurrentFile` message with a file to edit that exists only in the - // IDE. - #[actix_web::test] - async fn test_vscode_ide_websocket8() { - let connection_id = "test-connection-id8"; - let (temp_dir, test_dir, mut ws_ide, mut ws_client) = prep_test!(connection_id).await; - open_client(&mut ws_ide).await; - - // Message ids: IDE - 4->7, Server - 3, Client - 2. - let file_path = test_dir.join("only-in-ide.py"); - let file_path_str = file_path.to_str().unwrap().to_string(); - send_message( - &mut ws_ide, - &EditorMessage { - id: 4.0, - message: EditorMessageContents::CurrentFile(file_path_str.clone(), None), - }, - ) - .await; - - // This should be passed to the Client. - let em = read_message(&mut ws_client).await; - assert_eq!(em.id, 4.0); - assert_ends_with!( - cast!( - &em.message, - EditorMessageContents::CurrentFile, - file_name, - is_text - ) - .0, - "/only-in-ide.py" - ); - - // The Client should send a response. - send_message( - &mut ws_client, - &EditorMessage { - id: 4.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), - }, - ) - .await; - - // The IDE should receive it. - assert_eq!( - read_message(&mut ws_ide).await, - EditorMessage { - id: 4.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) - } - ); - - // The Client should send a GET request for this file. - let file_path_thread = file_path.clone(); - let join_handle = thread::spawn(move || { - assert_eq!( - minreq::get(format!( - "http://localhost:8080/vsc/fs/{connection_id}/{}", - drop_leading_slash(&file_path_thread.to_slash().unwrap()) - )) - .send() - .unwrap() - .status_code, - 200 - ) - }); - - // This should produce a `LoadFile` message. - // - // Message ids: IDE - 7, Server - 3->6, Client - 2. - let em = read_message(&mut ws_ide).await; - let msg = cast!(em.message, EditorMessageContents::LoadFile); - assert_eq!( - path::absolute(Path::new(&msg)).unwrap(), - path::absolute(&file_path).unwrap() - ); - assert_eq!(em.id, 3.0); - - // Reply to the `LoadFile` message with the file's contents. - send_message( - &mut ws_ide, - &EditorMessage { - id: 3.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::LoadFile(Some( - "# testing".to_string(), - )))), - }, - ) - .await; - join_handle.join().unwrap(); - - // This should also produce an `Update` message sent from the Server. - // - // Message ids: IDE - 7, Server - 6->9, Client - 2. - assert_eq!( - read_message(&mut ws_client).await, - EditorMessage { - id: 6.0, - message: EditorMessageContents::Update(UpdateMessageContents { - file_path: file_path_str.clone(), - contents: Some(CodeChatForWeb { - metadata: SourceFileMetadata { - mode: "python".to_string(), - }, - source: CodeMirror { - doc: "\n".to_string(), - doc_blocks: vec![( - 0, - 0, - "".to_string(), - "#".to_string(), - "

testing

\n".to_string() - )], - }, - }), - cursor_position: None, - scroll_position: None, - }) - } - ); - send_message( - &mut ws_client, - &EditorMessage { - id: 6.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), - }, - ) - .await; - - // The message, though a result for the `Update` sent by the Server, - // will still be echoed back to the IDE. - assert_eq!( - read_message(&mut ws_ide).await, - EditorMessage { - id: 6.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) - } - ); - - check_logger_errors(0); - // Report any errors produced when removing the temporary directory. - temp_dir.close().unwrap(); - } - - // Send an `Update` message from the IDE. - #[actix_web::test] - async fn test_vscode_ide_websocket7() { - let connection_id = "test-connection-id7"; - let (temp_dir, test_dir, mut ws_ide, mut ws_client) = prep_test!(connection_id).await; - open_client(&mut ws_ide).await; - - // Set the current file, so a subsequent `Update` message can be - // translated. - // - // Message ids: IDE - 4, Server - 3, Client - 2->5. - let file_path = test_dir.join("test.py"); - let file_path_str = file_path.to_str().unwrap().to_string(); - send_message( - &mut ws_client, - &EditorMessage { - id: 2.0, - message: EditorMessageContents::CurrentFile( - format!( - "http://localhost:8080/vsc/fs/{connection_id}/{}", - &file_path.to_slash().unwrap(), - ), - None, - ), - }, - ) - .await; - let em = read_message(&mut ws_ide).await; - let (cf, is_text) = cast!( - em.message, - EditorMessageContents::CurrentFile, - file_name, - is_text - ); - assert_eq!(path::absolute(Path::new(&cf)).unwrap(), file_path); - // Since the file doesn't exist, it's classified as binary by default. - assert_eq!(is_text, Some(false)); - assert_eq!(em.id, 2.0); - - send_message( - &mut ws_ide, - &EditorMessage { - id: 2.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), - }, - ) - .await; - assert_eq!( - read_message(&mut ws_client).await, - EditorMessage { - id: 2.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) - } - ); - - // Send an `Update` message. - // - // Message ids: IDE - 4->7, Server - 3, Client - 5. - send_message( - &mut ws_ide, - &EditorMessage { - id: 4.0, - message: EditorMessageContents::Update(UpdateMessageContents { - file_path: file_path_str.clone(), - contents: Some(CodeChatForWeb { - metadata: SourceFileMetadata { - mode: "python".to_string(), - }, - source: CodeMirror { - doc: "# more".to_string(), - doc_blocks: vec![], - }, - }), - cursor_position: None, - scroll_position: None, - }), - }, - ) - .await; - assert_eq!( - read_message(&mut ws_client).await, - EditorMessage { - id: 4.0, - message: EditorMessageContents::Update(UpdateMessageContents { - file_path: file_path_str.clone(), - contents: Some(CodeChatForWeb { - metadata: SourceFileMetadata { - mode: "python".to_string(), - }, - source: CodeMirror { - doc: "\n".to_string(), - doc_blocks: vec![( - 0, - 0, - "".to_string(), - "#".to_string(), - "

more

\n".to_string() - )], - }, - }), - cursor_position: None, - scroll_position: None, - }) - } - ); - send_message( - &mut ws_client, - &EditorMessage { - id: 4.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), - }, - ) - .await; - assert_eq!( - read_message(&mut ws_ide).await, - EditorMessage { - id: 4.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) - } - ); - - check_logger_errors(0); - // Report any errors produced when removing the temporary directory. - temp_dir.close().unwrap(); - } - - // Send an `Update` message from the Client. - #[actix_web::test] - async fn test_vscode_ide_websocket6() { - let connection_id = "test-connection-id6"; - let (temp_dir, test_dir, mut ws_ide, mut ws_client) = prep_test!(connection_id).await; - open_client(&mut ws_ide).await; - - // Message ids: IDE - 4, Server - 3, Client - 2->5. - let file_path = test_dir.join("foo.py").to_string_lossy().to_string(); - send_message( - &mut ws_client, - &EditorMessage { - id: 2.0, - message: EditorMessageContents::Update(UpdateMessageContents { - file_path: file_path.clone(), - contents: Some(CodeChatForWeb { - metadata: SourceFileMetadata { - mode: "python".to_string(), - }, - source: CodeMirror { - doc: "\n".to_string(), - doc_blocks: vec![( - 0, - 0, - "".to_string(), - "#".to_string(), - "less\n".to_string(), - )], - }, - }), - cursor_position: None, - scroll_position: None, - }), - }, - ) - .await; - assert_eq!( - read_message(&mut ws_ide).await, - EditorMessage { - id: 2.0, - message: EditorMessageContents::Update(UpdateMessageContents { - file_path, - contents: Some(CodeChatForWeb { - metadata: SourceFileMetadata { - mode: "python".to_string(), - }, - source: CodeMirror { - doc: "# less\n".to_string(), - doc_blocks: vec![], - }, - }), - cursor_position: None, - scroll_position: None, - }) - } - ); - send_message( - &mut ws_ide, - &EditorMessage { - id: 2.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), - }, - ) - .await; - assert_eq!( - read_message(&mut ws_client).await, - EditorMessage { - id: 2.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) - } - ); - - check_logger_errors(0); - // Report any errors produced when removing the temporary directory. - temp_dir.close().unwrap(); - } - - // Send a `CurrentFile` message from the Client, requesting a file that - // exists on disk, but not in the IDE. - #[actix_web::test] - async fn test_vscode_ide_websocket4() { - let connection_id = "test-connection-id4"; - let (temp_dir, test_dir, mut ws_ide, mut ws_client) = prep_test!(connection_id).await; - open_client(&mut ws_ide).await; - - // Message ids: IDE - 4, Server - 3, Client - 2->5. - let file_path_temp = fs::canonicalize(test_dir.join("test.py")).unwrap(); - let file_path = simplified(&file_path_temp); - send_message( - &mut ws_client, - &EditorMessage { - id: 2.0, - message: EditorMessageContents::CurrentFile( - format!( - "http://localhost:8080/vsc/fs/{connection_id}/{}", - &file_path.to_slash().unwrap() - ), - None, - ), - }, - ) - .await; - - let em = read_message(&mut ws_ide).await; - let (cf, _) = cast!( - em.message, - EditorMessageContents::CurrentFile, - file_name, - is_text - ); - assert_eq!(cf, file_path.to_str().unwrap().to_string()); - assert_eq!(em.id, 2.0); - - send_message( - &mut ws_ide, - &EditorMessage { - id: 2.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), - }, - ) - .await; - assert_eq!( - read_message(&mut ws_client).await, - EditorMessage { - id: 2.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) - } - ); - - // The Client should send a GET request for this file. - let test_dir_thread = test_dir.clone(); - let join_handle = thread::spawn(move || { - assert_eq!( - minreq::get(format!( - "http://localhost:8080/vsc/fs/{connection_id}/{}/{}", - test_dir_thread.to_slash().unwrap(), - // On Windows, send incorrect case for this file; the server - // should correct it. - if cfg!(windows) { "Test.py" } else { "test.py" } - )) - .send() - .unwrap() - .status_code, - 200 - ) - }); - - // This should produce a `LoadFile` message. - // - // Message ids: IDE - 4, Server - 3->6, Client - 5. - let em = read_message(&mut ws_ide).await; - let msg = cast!(em.message, EditorMessageContents::LoadFile); - assert_eq!(fs::canonicalize(&msg).unwrap(), file_path_temp); - assert_eq!(em.id, 3.0); - - // Reply to the `LoadFile` message: the IDE doesn't have the file. - send_message( - &mut ws_ide, - &EditorMessage { - id: 3.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::LoadFile(None))), - }, - ) - .await; - join_handle.join().unwrap(); - - // This should also produce an `Update` message sent from the Server. - // - // Message ids: IDE - 4, Server - 6->9, Client - 5. - assert_eq!( - read_message(&mut ws_client).await, - EditorMessage { - id: 6.0, - message: EditorMessageContents::Update(UpdateMessageContents { - file_path: file_path.to_str().unwrap().to_string(), - contents: Some(CodeChatForWeb { - metadata: SourceFileMetadata { - mode: "python".to_string(), - }, - source: CodeMirror { - doc: "\n".to_string(), - doc_blocks: vec![( - 0, - 0, - "".to_string(), - "#".to_string(), - "

test.py

\n".to_string() - )], - }, - }), - cursor_position: None, - scroll_position: None, - }) - } - ); - send_message( - &mut ws_client, - &EditorMessage { - id: 6.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), - }, - ) - .await; - assert_eq!( - read_message(&mut ws_ide).await, - EditorMessage { - id: 6.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), - } - ); - - check_logger_errors(0); - // Report any errors produced when removing the temporary directory. - temp_dir.close().unwrap(); - } - - // Send a `RequestClose` message to the Client, then close the Client. - #[actix_web::test] - async fn test_vscode_ide_websocket5() { - let connection_id = "test-connection-id5"; - let (temp_dir, _, mut ws_ide, mut ws_client) = prep_test!(connection_id).await; - open_client(&mut ws_ide).await; - - // Message ids: IDE - 4->7, Server - 3, Client - 2. - // - // Send the `RequestClose` message. - send_message( - &mut ws_ide, - &EditorMessage { - id: 4.0, - message: EditorMessageContents::RequestClose, - }, - ) - .await; - assert_eq!( - read_message(&mut ws_client).await, - EditorMessage { - id: 4.0, - message: EditorMessageContents::RequestClose - } - ); - send_message( - &mut ws_client, - &EditorMessage { - id: 4.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), - }, - ) - .await; - assert_eq!( - read_message(&mut ws_ide).await, - EditorMessage { - id: 4.0, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), - } - ); - - // Close the Client websocket. - ws_client.close(None).await.unwrap(); - loop { - match ws_ide.next().await.unwrap().unwrap() { - Message::Ping(_) => ws_ide.send(Message::Pong(vec![].into())).await.unwrap(), - Message::Close(_) => break, - _ => panic!("Unexpected message."), - } - } - - check_logger_errors(0); - // Report any errors produced when removing the temporary directory. - temp_dir.close().unwrap(); - } - - // Close the IDE. - #[actix_web::test] - async fn test_vscode_ide_websocket9() { - let connection_id = "test-connection-id9"; - let (temp_dir, _, mut ws_ide, mut ws_client) = prep_test!(connection_id).await; - open_client(&mut ws_ide).await; - - ws_ide.close(None).await.unwrap(); - loop { - match ws_client.next().await.unwrap().unwrap() { - Message::Ping(_) => ws_client.send(Message::Pong(vec![].into())).await.unwrap(), - Message::Close(_) => break, - _ => panic!("Unexpected message."), - } - } - - check_logger_errors(0); - // Report any errors produced when removing the temporary directory. - temp_dir.close().unwrap(); - } -} diff --git a/server/src/webserver/vscode/tests.rs b/server/src/webserver/vscode/tests.rs new file mode 100644 index 00000000..69918224 --- /dev/null +++ b/server/src/webserver/vscode/tests.rs @@ -0,0 +1,1058 @@ +// Copyright (C) 2023 Bryan A. Jones. +// +// This file is part of the CodeChat Editor. The CodeChat Editor is free +// software: you can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// The CodeChat Editor is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// the CodeChat Editor. If not, see +// [http://www.gnu.org/licenses](http://www.gnu.org/licenses). +/// `test.rs` -- Unit tests for the vscode interface +/// ================================================ +// Imports +// ------- +use std::{ + fs::{self, File}, + io::{Error, Read}, + path::{self, Path, PathBuf}, + thread, + time::{Duration, SystemTime}, +}; + +use actix_rt::task::JoinHandle; +use assert_fs::TempDir; +use assertables::{assert_ends_with, assert_starts_with}; +use dunce::simplified; +use futures_util::{SinkExt, StreamExt}; +use lazy_static::lazy_static; +use minreq; +use path_slash::PathExt; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + net::TcpStream, + select, + time::sleep, +}; +use tokio_tungstenite::{ + MaybeTlsStream, WebSocketStream, connect_async, + tungstenite::{http::StatusCode, protocol::Message}, +}; + +use super::super::{ + EditorMessage, EditorMessageContents, IP_ADDRESS, IdeType, run_server, tests::IP_PORT, +}; +use crate::{ + cast, + processing::{CodeChatForWeb, CodeMirror, SourceFileMetadata}, + test_utils::{_prep_test_dir, check_logger_errors, configure_testing_logger}, + webserver::{ResultOkTypes, UpdateMessageContents, drop_leading_slash}, +}; + +// Globals +// ------- +lazy_static! { + // Run a single webserver for all tests. + static ref WEBSERVER_HANDLE: JoinHandle> = + actix_rt::spawn(async move { run_server(IP_PORT).await }); +} + +// Send a message via a websocket. +async fn send_message( + ws_stream: &mut WebSocketStream, + message: &EditorMessage, +) { + ws_stream + .send(Message::Text( + serde_json::to_string(message).unwrap().into(), + )) + .await + .unwrap(); +} + +// Support functions +// ----------------- +// Read a message from a websocket. +async fn read_message( + ws_stream: &mut WebSocketStream, +) -> EditorMessage { + let now = SystemTime::now(); + let msg_txt = loop { + let msg = select! { + data = ws_stream.next() => data.unwrap().unwrap(), + _ = sleep(Duration::from_secs(3) - now.elapsed().unwrap()) => panic!("Timeout waiting for message") + }; + match msg { + Message::Close(_) => panic!("Unexpected close message."), + Message::Ping(_) => ws_stream.send(Message::Pong(vec![].into())).await.unwrap(), + Message::Pong(_) => panic!("Unexpected pong message."), + Message::Text(txt) => break txt, + Message::Binary(_) => panic!("Unexpected binary message."), + Message::Frame(_) => panic!("Unexpected frame message."), + } + }; + serde_json::from_str(&msg_txt) + .unwrap_or_else(|_| panic!("Unable to convert '{msg_txt}' to JSON.")) +} + +type WebSocketStreamTcp = WebSocketStream>; + +async fn connect_async_server(prefix: &str, connection_id: &str) -> WebSocketStreamTcp { + connect_async(format!( + "ws://{IP_ADDRESS}:{IP_PORT}{prefix}/{connection_id}", + )) + .await + .expect("Failed to connect") + .0 +} + +async fn connect_async_ide(connection_id: &str) -> WebSocketStreamTcp { + connect_async_server("/vsc/ws-ide", connection_id).await +} + +async fn connect_async_client(connection_id: &str) -> WebSocketStreamTcp { + connect_async_server("/vsc/ws-client", connection_id).await +} + +// Open the Client in the VSCode browser. (Although, for testing, the Client +// isn't opened at all.) +// +// Message ids at function end: IDE - 4, Server - 3, Client - 2. +async fn open_client(ws_ide: &mut WebSocketStream) { + // 1. Send the `Opened` message. + // + // Message ids: IDE - 1->4, Server - 0, Client - 2. + send_message( + ws_ide, + &EditorMessage { + id: 1.0, + message: EditorMessageContents::Opened(IdeType::VSCode(true)), + }, + ) + .await; + + // Get the response. It should be success. + assert_eq!( + read_message(ws_ide).await, + EditorMessage { + id: 1.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), + } + ); + + // 2. Next, wait for the next message -- the HTML. + // + // Message ids: IDE - 4, Server - 0->3, Client - 2. + let em = read_message(ws_ide).await; + assert_starts_with!( + cast!(&em.message, EditorMessageContents::ClientHtml), + "" + ); + assert_eq!(em.id, 0.0); + + // Send a success response to this message. + send_message( + ws_ide, + &EditorMessage { + id: 0.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), + }, + ) + .await; +} + +// Perform all the setup for testing the Server via IDE and Client +// websockets. This should be invoked by the `prep_test!` macro; otherwise, +// test files won't be found. +async fn _prep_test( + connection_id: &str, + test_full_name: &str, +) -> (TempDir, PathBuf, WebSocketStreamTcp, WebSocketStreamTcp) { + configure_testing_logger(); + let (temp_dir, test_dir) = _prep_test_dir(test_full_name); + // Ensure the webserver is running. + let _ = &*WEBSERVER_HANDLE; + let now = SystemTime::now(); + while now.elapsed().unwrap().as_millis() < 100 { + if minreq::get(format!("http://{IP_ADDRESS}:{IP_PORT}/ping",)) + .send() + .is_ok() + { + break; + } + sleep(Duration::from_millis(10)).await; + } + + // Connect to the VSCode IDE websocket. + let ws_ide = connect_async_ide(connection_id).await; + let ws_client = connect_async_client(connection_id).await; + + (temp_dir, test_dir, ws_ide, ws_client) +} + +// This calls `_prep_test` with the current function name. It must be a +// macro, so that it's called with the test function's name; calling it +// inside `_prep_test` would give the wrong name. +macro_rules! prep_test { + ($connection_id: ident) => {{ + use crate::function_name; + _prep_test($connection_id, function_name!()) + }}; +} + +// Tests +// ----- +// Test incorrect inputs: two connections with the same ID, sending the +// wrong first message. +#[actix_web::test] +async fn test_vscode_ide_websocket1() { + let connection_id = "test-connection-id1"; + let (_, _, mut ws_ide, _) = prep_test!(connection_id).await; + + // Start a second connection; verify that it fails. + let err = connect_async(format!( + "ws://{IP_ADDRESS}:{IP_PORT}/vsc/ws-ide/{connection_id}", + )) + .await + .expect_err("Should fail to connect"); + let response = cast!(err, tokio_tungstenite::tungstenite::Error::Http); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + // Note: we can't check the logs, since the server runs in a separate + // thread. Changing the logger to log across threads means we get logs + // from other tests (which run in parallel by default). The benefit of + // running all tests single-threaded plus fixing the logger is low. + // + // Send a message that's not an `Opened` message. + send_message( + &mut ws_ide, + &EditorMessage { + id: 0.0, + message: EditorMessageContents::Update(UpdateMessageContents { + file_path: "".to_string(), + contents: None, + cursor_position: None, + scroll_position: None, + }), + }, + ) + .await; + + // Get the response. It should be an error. + let em = read_message(&mut ws_ide).await; + let result = cast!(em.message, EditorMessageContents::Result); + + assert_starts_with!(cast!(&result, Err), "Unexpected message"); + + // Next, expect the websocket to be closed. + let err = &ws_ide.next().await.unwrap().unwrap(); + assert_eq!(*err, Message::Close(None)); + + check_logger_errors(0); +} + +// Test opening the Client in an external browser. +#[actix_web::test] +async fn test_vscode_ide_websocket2() { + let connection_id = "test-connection-id2"; + let (_, _, mut ws_ide, _) = prep_test!(connection_id).await; + + // Send the `Opened` message. + send_message( + &mut ws_ide, + &EditorMessage { + id: 0.0, + message: EditorMessageContents::Opened(IdeType::VSCode(false)), + }, + ) + .await; + + // Get the response. It should be success. + let em = read_message(&mut ws_ide).await; + assert_eq!( + cast!(em.message, EditorMessageContents::Result), + Ok(ResultOkTypes::Void) + ); + + check_logger_errors(0); +} + +// Fetch a non-existent file and verify the response returns an error. +#[actix_web::test] +async fn test_vscode_ide_websocket3() { + let connection_id = "test-connection-id3"; + let (temp_dir, test_dir, mut ws_ide, _) = prep_test!(connection_id).await; + open_client(&mut ws_ide).await; + + let file_path = test_dir.join("none.py"); + let file_path_str = drop_leading_slash(&file_path.to_slash().unwrap()).to_string(); + + // Do this is a thread, since the request generates a message that + // requires a response in order to complete. + let file_path_str_thread = file_path_str.clone(); + let join_handle = thread::spawn(move || { + assert_eq!( + minreq::get(format!( + "http://localhost:8080/vsc/fs/{connection_id}/{}", + file_path_str_thread + )) + .send() + .unwrap() + .status_code, + 404 + ) + }); + + // The HTTP request produces a `LoadFile` message. + // + // Message ids: IDE - 4, Server - 3->6, Client - 2. + let em = read_message(&mut ws_ide).await; + let msg = cast!(em.message, EditorMessageContents::LoadFile); + // Compare these as strings -- we want to ensure the path separator is + // correct for the current platform. + assert_eq!(file_path.to_string_lossy(), msg.to_string_lossy()); + assert_eq!(em.id, 3.0); + + // Reply to the `LoadFile` message -- the file isn't present. + send_message( + &mut ws_ide, + &EditorMessage { + id: 3.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::LoadFile(None))), + }, + ) + .await; + + // This should cause the HTTP request to complete by receiving the + // response (file not found). + join_handle.join().unwrap(); + + check_logger_errors(0); + // Report any errors produced when removing the temporary directory. + temp_dir.close().unwrap(); +} + +// Fetch a file that exists, but using backslashes. This should still fail, +// even on Windows. +#[actix_web::test] +async fn test_vscode_ide_websocket3a() { + let connection_id = "test-connection-id3a"; + let (temp_dir, test_dir, mut ws_ide, _) = prep_test!(connection_id).await; + open_client(&mut ws_ide).await; + + let file_path = test_dir.join("test.py"); + // Force the path separator to be Window-style for this test, even on + // non-Windows platforms. + let file_path_str = file_path.to_str().unwrap().to_string().replace("/", "\\"); + + // Do this is a thread, since the request generates a message that + // requires a response in order to complete. + let file_path_str_thread = file_path_str.clone(); + let join_handle = thread::spawn(move || { + assert_eq!( + minreq::get(format!( + "http://localhost:8080/vsc/fs/{connection_id}/{}", + file_path_str_thread + )) + .send() + .unwrap() + .status_code, + 404 + ) + }); + + // The HTTP request produces a `LoadFile` message. + // + // Message ids: IDE - 4, Server - 3->6, Client - 2. + let em = read_message(&mut ws_ide).await; + cast!(em.message, EditorMessageContents::LoadFile); + // Skip comparing the file names, due to the backslash encoding. + assert_eq!(em.id, 3.0); + + // Reply to the `LoadFile` message -- the file isn't present. + send_message( + &mut ws_ide, + &EditorMessage { + id: 3.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::LoadFile(None))), + }, + ) + .await; + + // This should cause the HTTP request to complete by receiving the + // response (file not found). + join_handle.join().unwrap(); + + check_logger_errors(0); + // Report any errors produced when removing the temporary directory. + temp_dir.close().unwrap(); +} + +// Send a `CurrentFile` message with a file to edit that exists only in the +// IDE. +#[actix_web::test] +async fn test_vscode_ide_websocket8() { + let connection_id = "test-connection-id8"; + let (temp_dir, test_dir, mut ws_ide, mut ws_client) = prep_test!(connection_id).await; + open_client(&mut ws_ide).await; + + // Message ids: IDE - 4->7, Server - 3, Client - 2. + let file_path = test_dir.join("only-in-ide.py"); + let file_path_str = file_path.to_str().unwrap().to_string(); + send_message( + &mut ws_ide, + &EditorMessage { + id: 4.0, + message: EditorMessageContents::CurrentFile(file_path_str.clone(), None), + }, + ) + .await; + + // This should be passed to the Client. + let em = read_message(&mut ws_client).await; + assert_eq!(em.id, 4.0); + assert_ends_with!( + cast!( + &em.message, + EditorMessageContents::CurrentFile, + file_name, + is_text + ) + .0, + "/only-in-ide.py" + ); + + // The Client should send a response. + send_message( + &mut ws_client, + &EditorMessage { + id: 4.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), + }, + ) + .await; + + // The IDE should receive it. + assert_eq!( + read_message(&mut ws_ide).await, + EditorMessage { + id: 4.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) + } + ); + + // The Client should send a GET request for this file. + let file_path_thread = file_path.clone(); + let join_handle = thread::spawn(move || { + assert_eq!( + minreq::get(format!( + "http://localhost:8080/vsc/fs/{connection_id}/{}", + drop_leading_slash(&file_path_thread.to_slash().unwrap()) + )) + .send() + .unwrap() + .status_code, + 200 + ) + }); + + // This should produce a `LoadFile` message. + // + // Message ids: IDE - 7, Server - 3->6, Client - 2. + let em = read_message(&mut ws_ide).await; + let msg = cast!(em.message, EditorMessageContents::LoadFile); + assert_eq!( + path::absolute(Path::new(&msg)).unwrap(), + path::absolute(&file_path).unwrap() + ); + assert_eq!(em.id, 3.0); + + // Reply to the `LoadFile` message with the file's contents. + send_message( + &mut ws_ide, + &EditorMessage { + id: 3.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::LoadFile(Some( + "# testing".to_string(), + )))), + }, + ) + .await; + join_handle.join().unwrap(); + + // This should also produce an `Update` message sent from the Server. + // + // Message ids: IDE - 7, Server - 6->9, Client - 2. + assert_eq!( + read_message(&mut ws_client).await, + EditorMessage { + id: 6.0, + message: EditorMessageContents::Update(UpdateMessageContents { + file_path: file_path_str.clone(), + contents: Some(CodeChatForWeb { + metadata: SourceFileMetadata { + mode: "python".to_string(), + }, + source: CodeMirror { + doc: "\n".to_string(), + doc_blocks: vec![( + 0, + 0, + "".to_string(), + "#".to_string(), + "

testing

\n".to_string() + )], + }, + }), + cursor_position: None, + scroll_position: None, + }) + } + ); + send_message( + &mut ws_client, + &EditorMessage { + id: 6.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), + }, + ) + .await; + + // The message, though a result for the `Update` sent by the Server, + // will still be echoed back to the IDE. + assert_eq!( + read_message(&mut ws_ide).await, + EditorMessage { + id: 6.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) + } + ); + + check_logger_errors(0); + // Report any errors produced when removing the temporary directory. + temp_dir.close().unwrap(); +} + +// Send an `Update` message from the IDE. +#[actix_web::test] +async fn test_vscode_ide_websocket7() { + let connection_id = "test-connection-id7"; + let (temp_dir, test_dir, mut ws_ide, mut ws_client) = prep_test!(connection_id).await; + open_client(&mut ws_ide).await; + + // Set the current file, so a subsequent `Update` message can be + // translated. + // + // Message ids: IDE - 4, Server - 3, Client - 2->5. + let file_path = test_dir.join("test.py"); + let file_path_str = file_path.to_str().unwrap().to_string(); + send_message( + &mut ws_client, + &EditorMessage { + id: 2.0, + message: EditorMessageContents::CurrentFile( + format!( + "http://localhost:8080/vsc/fs/{connection_id}/{}", + &file_path.to_slash().unwrap(), + ), + None, + ), + }, + ) + .await; + let em = read_message(&mut ws_ide).await; + let (cf, is_text) = cast!( + em.message, + EditorMessageContents::CurrentFile, + file_name, + is_text + ); + assert_eq!(path::absolute(Path::new(&cf)).unwrap(), file_path); + // Since the file doesn't exist, it's classified as binary by default. + assert_eq!(is_text, Some(false)); + assert_eq!(em.id, 2.0); + + send_message( + &mut ws_ide, + &EditorMessage { + id: 2.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), + }, + ) + .await; + assert_eq!( + read_message(&mut ws_client).await, + EditorMessage { + id: 2.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) + } + ); + + // Send an `Update` message. + // + // Message ids: IDE - 4->7, Server - 3, Client - 5. + send_message( + &mut ws_ide, + &EditorMessage { + id: 4.0, + message: EditorMessageContents::Update(UpdateMessageContents { + file_path: file_path_str.clone(), + contents: Some(CodeChatForWeb { + metadata: SourceFileMetadata { + mode: "python".to_string(), + }, + source: CodeMirror { + doc: "# more".to_string(), + doc_blocks: vec![], + }, + }), + cursor_position: None, + scroll_position: None, + }), + }, + ) + .await; + assert_eq!( + read_message(&mut ws_client).await, + EditorMessage { + id: 4.0, + message: EditorMessageContents::Update(UpdateMessageContents { + file_path: file_path_str.clone(), + contents: Some(CodeChatForWeb { + metadata: SourceFileMetadata { + mode: "python".to_string(), + }, + source: CodeMirror { + doc: "\n".to_string(), + doc_blocks: vec![( + 0, + 0, + "".to_string(), + "#".to_string(), + "

more

\n".to_string() + )], + }, + }), + cursor_position: None, + scroll_position: None, + }) + } + ); + send_message( + &mut ws_client, + &EditorMessage { + id: 4.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), + }, + ) + .await; + assert_eq!( + read_message(&mut ws_ide).await, + EditorMessage { + id: 4.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) + } + ); + + check_logger_errors(0); + // Report any errors produced when removing the temporary directory. + temp_dir.close().unwrap(); +} + +// Send an `Update` message from the Client. +#[actix_web::test] +async fn test_vscode_ide_websocket6() { + let connection_id = "test-connection-id6"; + let (temp_dir, test_dir, mut ws_ide, mut ws_client) = prep_test!(connection_id).await; + open_client(&mut ws_ide).await; + + // Message ids: IDE - 4, Server - 3, Client - 2->5. + let file_path = test_dir.join("foo.py").to_string_lossy().to_string(); + send_message( + &mut ws_client, + &EditorMessage { + id: 2.0, + message: EditorMessageContents::Update(UpdateMessageContents { + file_path: file_path.clone(), + contents: Some(CodeChatForWeb { + metadata: SourceFileMetadata { + mode: "python".to_string(), + }, + source: CodeMirror { + doc: "\n".to_string(), + doc_blocks: vec![( + 0, + 0, + "".to_string(), + "#".to_string(), + "less\n".to_string(), + )], + }, + }), + cursor_position: None, + scroll_position: None, + }), + }, + ) + .await; + assert_eq!( + read_message(&mut ws_ide).await, + EditorMessage { + id: 2.0, + message: EditorMessageContents::Update(UpdateMessageContents { + file_path, + contents: Some(CodeChatForWeb { + metadata: SourceFileMetadata { + mode: "python".to_string(), + }, + source: CodeMirror { + doc: "# less\n".to_string(), + doc_blocks: vec![], + }, + }), + cursor_position: None, + scroll_position: None, + }) + } + ); + send_message( + &mut ws_ide, + &EditorMessage { + id: 2.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), + }, + ) + .await; + assert_eq!( + read_message(&mut ws_client).await, + EditorMessage { + id: 2.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) + } + ); + + check_logger_errors(0); + // Report any errors produced when removing the temporary directory. + temp_dir.close().unwrap(); +} + +// Send a `CurrentFile` message from the Client, requesting a file that +// exists on disk, but not in the IDE. +#[actix_web::test] +async fn test_vscode_ide_websocket4() { + let connection_id = "test-connection-id4"; + let (temp_dir, test_dir, mut ws_ide, mut ws_client) = prep_test!(connection_id).await; + open_client(&mut ws_ide).await; + + // Message ids: IDE - 4, Server - 3, Client - 2->5. + let file_path_temp = fs::canonicalize(test_dir.join("test.py")).unwrap(); + let file_path = simplified(&file_path_temp); + send_message( + &mut ws_client, + &EditorMessage { + id: 2.0, + message: EditorMessageContents::CurrentFile( + format!( + "http://localhost:8080/vsc/fs/{connection_id}/{}", + &file_path.to_slash().unwrap() + ), + None, + ), + }, + ) + .await; + assert_eq!( + read_message(&mut ws_ide).await, + EditorMessage { + id: 2.0, + message: EditorMessageContents::CurrentFile( + file_path.to_str().unwrap().to_string(), + Some(true) + ) + } + ); + + send_message( + &mut ws_ide, + &EditorMessage { + id: 2.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), + }, + ) + .await; + assert_eq!( + read_message(&mut ws_client).await, + EditorMessage { + id: 2.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) + } + ); + + // The Client should send a GET request for this file. + let test_dir_thread = test_dir.clone(); + let join_handle = thread::spawn(move || { + assert_eq!( + minreq::get(format!( + "http://localhost:8080/vsc/fs/{connection_id}/{}/{}", + test_dir_thread.to_slash().unwrap(), + // On Windows, send incorrect case for this file; the server + // should correct it. + if cfg!(windows) { "Test.py" } else { "test.py" } + )) + .send() + .unwrap() + .status_code, + 200 + ) + }); + + // This should produce a `LoadFile` message. + // + // Message ids: IDE - 4, Server - 3->6, Client - 5. + let em = read_message(&mut ws_ide).await; + let msg = cast!(em.message, EditorMessageContents::LoadFile); + assert_eq!(fs::canonicalize(&msg).unwrap(), file_path_temp); + assert_eq!(em.id, 3.0); + + // Reply to the `LoadFile` message: the IDE doesn't have the file. + send_message( + &mut ws_ide, + &EditorMessage { + id: 3.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::LoadFile(None))), + }, + ) + .await; + join_handle.join().unwrap(); + + // This should also produce an `Update` message sent from the Server. + // + // Message ids: IDE - 4, Server - 6->9, Client - 5. + assert_eq!( + read_message(&mut ws_client).await, + EditorMessage { + id: 6.0, + message: EditorMessageContents::Update(UpdateMessageContents { + file_path: file_path.to_str().unwrap().to_string(), + contents: Some(CodeChatForWeb { + metadata: SourceFileMetadata { + mode: "python".to_string(), + }, + source: CodeMirror { + doc: "\n".to_string(), + doc_blocks: vec![( + 0, + 0, + "".to_string(), + "#".to_string(), + "

test.py

\n".to_string() + )], + }, + }), + cursor_position: None, + scroll_position: None, + }) + } + ); + send_message( + &mut ws_client, + &EditorMessage { + id: 6.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), + }, + ) + .await; + assert_eq!( + read_message(&mut ws_ide).await, + EditorMessage { + id: 6.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), + } + ); + + check_logger_errors(0); + // Report any errors produced when removing the temporary directory. + temp_dir.close().unwrap(); +} + +// Send a `CurrentFile` message from the Client, requesting a binary file that +// exists on disk, but not in the IDE. +#[actix_web::test] +async fn test_vscode_ide_websocket4a() { + let connection_id = "test-connection-id4a"; + let (temp_dir, test_dir, mut ws_ide, mut ws_client) = prep_test!(connection_id).await; + open_client(&mut ws_ide).await; + + // Message ids: IDE - 4, Server - 3, Client - 2->5. + let hw = "helloworld.pdf"; + let file_path_temp = fs::canonicalize(test_dir.join(hw)).unwrap(); + let file_path = simplified(&file_path_temp); + send_message( + &mut ws_client, + &EditorMessage { + id: 2.0, + message: EditorMessageContents::CurrentFile( + format!( + "http://localhost:8080/vsc/fs/{connection_id}/{}", + &file_path.to_slash().unwrap() + ), + None, + ), + }, + ) + .await; + + assert_eq!( + read_message(&mut ws_ide).await, + EditorMessage { + id: 2.0, + message: EditorMessageContents::CurrentFile( + file_path.to_str().unwrap().to_string(), + // `helloworld.pdf` is a text file! (But perhaps should mark all PDFs as binary, regardless?) + Some(true) + ) + } + ); + + send_message( + &mut ws_ide, + &EditorMessage { + id: 2.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), + }, + ) + .await; + assert_eq!( + read_message(&mut ws_client).await, + EditorMessage { + id: 2.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) + } + ); + + // The Client should send a GET request for this file. + let mut test_dir_thread = test_dir.clone(); + let join_handle = thread::spawn(move || { + // Read the file. + let response = minreq::get(format!( + "http://localhost:8080/vsc/fs/{connection_id}/{}/{hw}", + test_dir_thread.to_slash().unwrap(), + )) + .send() + .unwrap(); + assert_eq!(response.status_code, 200); + // Since this isn't a project, the response should be just the image. + test_dir_thread.push(hw); + let mut helloworld_pdf_data = vec![]; + File::open(test_dir_thread) + .unwrap() + .read_to_end(&mut helloworld_pdf_data) + .unwrap(); + assert_eq!(response.as_bytes().to_vec(), helloworld_pdf_data); + }); + + // This should produce a `LoadFile` message. + // + // Message ids: IDE - 4, Server - 3->6, Client - 5. + let em = read_message(&mut ws_ide).await; + let msg = cast!(em.message, EditorMessageContents::LoadFile); + assert_eq!(fs::canonicalize(&msg).unwrap(), file_path_temp); + assert_eq!(em.id, 3.0); + + // Reply to the `LoadFile` message: the IDE doesn't have the file. + send_message( + &mut ws_ide, + &EditorMessage { + id: 3.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::LoadFile(None))), + }, + ) + .await; + join_handle.join().unwrap(); + + check_logger_errors(0); + // Report any errors produced when removing the temporary directory. + temp_dir.close().unwrap(); +} + +// Send a `RequestClose` message to the Client, then close the Client. +#[actix_web::test] +async fn test_vscode_ide_websocket5() { + let connection_id = "test-connection-id5"; + let (temp_dir, _, mut ws_ide, mut ws_client) = prep_test!(connection_id).await; + open_client(&mut ws_ide).await; + + // Message ids: IDE - 4->7, Server - 3, Client - 2. + // + // Send the `RequestClose` message. + send_message( + &mut ws_ide, + &EditorMessage { + id: 4.0, + message: EditorMessageContents::RequestClose, + }, + ) + .await; + assert_eq!( + read_message(&mut ws_client).await, + EditorMessage { + id: 4.0, + message: EditorMessageContents::RequestClose + } + ); + send_message( + &mut ws_client, + &EditorMessage { + id: 4.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), + }, + ) + .await; + assert_eq!( + read_message(&mut ws_ide).await, + EditorMessage { + id: 4.0, + message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)), + } + ); + + // Close the Client websocket. + ws_client.close(None).await.unwrap(); + loop { + match ws_ide.next().await.unwrap().unwrap() { + Message::Ping(_) => ws_ide.send(Message::Pong(vec![].into())).await.unwrap(), + Message::Close(_) => break, + _ => panic!("Unexpected message."), + } + } + + check_logger_errors(0); + // Report any errors produced when removing the temporary directory. + temp_dir.close().unwrap(); +} + +// Close the IDE. +#[actix_web::test] +async fn test_vscode_ide_websocket9() { + let connection_id = "test-connection-id9"; + let (temp_dir, _, mut ws_ide, mut ws_client) = prep_test!(connection_id).await; + open_client(&mut ws_ide).await; + + ws_ide.close(None).await.unwrap(); + loop { + match ws_client.next().await.unwrap().unwrap() { + Message::Ping(_) => ws_client.send(Message::Pong(vec![].into())).await.unwrap(), + Message::Close(_) => break, + _ => panic!("Unexpected message."), + } + } + + check_logger_errors(0); + // Report any errors produced when removing the temporary directory. + temp_dir.close().unwrap(); +} diff --git a/server/tests/fixtures/webserver/vscode/test/test_vscode_ide_websocket3a/test.py b/server/tests/fixtures/webserver/vscode/tests/test_vscode_ide_websocket3a/test.py similarity index 100% rename from server/tests/fixtures/webserver/vscode/test/test_vscode_ide_websocket3a/test.py rename to server/tests/fixtures/webserver/vscode/tests/test_vscode_ide_websocket3a/test.py diff --git a/server/tests/fixtures/webserver/vscode/test/test_vscode_ide_websocket4/test.py b/server/tests/fixtures/webserver/vscode/tests/test_vscode_ide_websocket4/test.py similarity index 100% rename from server/tests/fixtures/webserver/vscode/test/test_vscode_ide_websocket4/test.py rename to server/tests/fixtures/webserver/vscode/tests/test_vscode_ide_websocket4/test.py diff --git a/server/tests/fixtures/webserver/vscode/tests/test_vscode_ide_websocket4a/black.png b/server/tests/fixtures/webserver/vscode/tests/test_vscode_ide_websocket4a/black.png new file mode 100644 index 00000000..3b9a317b Binary files /dev/null and b/server/tests/fixtures/webserver/vscode/tests/test_vscode_ide_websocket4a/black.png differ diff --git a/server/tests/fixtures/webserver/vscode/tests/test_vscode_ide_websocket4a/helloworld.pdf b/server/tests/fixtures/webserver/vscode/tests/test_vscode_ide_websocket4a/helloworld.pdf new file mode 100644 index 00000000..d98b4e1d Binary files /dev/null and b/server/tests/fixtures/webserver/vscode/tests/test_vscode_ide_websocket4a/helloworld.pdf differ