diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 053aeddd..818f55a3 100755 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,6 +4,10 @@ "recommendations": [ "connor4312.esbuild-problem-matchers", "dbaeumer.vscode-eslint", + "bradlc.vscode-tailwindcss", + "Vue.volar", + "Vue.vscode-typescript-vue-plugin", + "antfu.iconify", ], // Please make sure you disable the following extensions *for this workspace* "unwantedRecommendations": [ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d18fd33..6e925ed8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,31 +6,46 @@ - **patch-detail:** implement new Patch Detail webview showing in-depth info for a specific Patch - can be opened via a new button "View Patch Details" on each item in the list of Patches - - panel's title shows the patch description in full if it's short, otherwise truncated to the nearest full word + - panel's title shows the patch description in full if it's short, otherwise truncated to the nearest full word fitting the limit - the following Patch info are shown in the new view - status + - the status badge's background color is a dynamic color mix of the patch status color and the dynamic editor-foreground inherited from vscode's current theme so as to ensure text contrast reaching at least [WCAAG AA](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Perceivable/Color_contrast) level of accessibility at all times while also retaining a relative consistency of the colors across all our UIs - major events like "created", "last updated", "merged" and related info with logic crafting optimal copy for each case (see similar tooltip improvements below) - id (with on-hover button to copy Patch identifier to clipboard) - revision authors - labels - title - description - - where applicable the above have on-hover indicators hinting that they come with a tooltip showing addtional info such as author's DID, full Id in case it's shortened or full localised time in case it's a "time-ago" + - where applicable the Patch info have on-hover indicators hinting that they come with a tooltip which shows additional info such as author's DID, full Id in case it's shortened or localised time (including the timezone used) in full text in case it's a "time-ago" + - "time-ago" for major patch events gets auto-updated to remain accurate as time goes by + - a "Refresh" button that refetches from httpd all data of that patch and updating all views that depend on it + - a "Check Out" button that checks out the Git branch associated with the Radicle Patch shown in the view + - shown only if the Patch is not checked out + - a "Check Out Default" button that checks out the Git branch marked as default for the Radicle project + - shown only if the Patch is checked out + - Patch check-out status remains in sync across all views and the actual underlying Git state as the latter changes +- **commands**: add new command to check out the current Radicle project's default Git branch +- **patch-list:** show button to "Check Out Default Git Branch" for the currently checked-out Patch on the list - **patch-list:** auto-retry fetching list of Patches from httpd (with geometric backoff) if an error occured -- **patch-list:** Patch tooltip improvements +- **patch-list:** improve Patch tooltip with the following - show merge revision id and commit hash (if not already shown in revision event's copy) for merged Patches - show latest revision id and commit hash for Patches with more than the initial revision - **patch-list:** prioritize Patch merge event over latest revision when deriving author and "time-ago" for item description - **patch-list:** improve legibility of time when Patch events (e.g. created, last updated, merged) happened - - only "time-ago" is shown now; the full date is still available in the Patch Details view - - use custom "time-ago" logic producing more informative results with fewer collisions e.g. "35 days ago" instead of "1 month ago" + - don't show full dates to make the copy less noisy. The full dates are still available in the new Patch Details view. + - use custom "time-ago" logic producing more informative results with fewer collisions e.g. "35 days ago" instead of "1 month ago" etc - **patch-list:** move button for command "Copy Patch Identifier to Clipboard" into Patch item's context menu - **patch-list:** use smaller dot as separator between data in the description of a Patch item -- the initial size of the Patches view (e.g. for new projects) will now be 4x that of the CLI Commands view, instead of having the area allocation split 50:50 resulting in wasted empty space allocated to the later view while the former may have the need for more area to show more content. Subsequent adjustments by the user will be respected and not get overwritten by the initial size. +- the initial height of the Patches view (e.g. for new projects) will now be 4x that of the CLI Commands view, instead of having the area allocation split 50:50 which resulted in wasted empty space allocated to the later view while the former may have the need for more area to show more content. Subsequent adjustments by the user will be respected and not get overwritten by the initial size. + +### 🏎️ Performance + +- **patch-list:** only re-render the affected Patch item(s) when checking out a(nother) patch (or a non-patch) branch. Previously all patches had to be re-fetched, parsed and all their list items (and their tooltips!) needed to be instantiated and rendered every time a different git branch got checked out. ### 🏡 Chores -- **webview:** implement infrastructure for Webviews, enabling the creation of bespoke custom views with the following powerful features +- **webview:** implement infrastructure for Webviews, effectively individual websites inside a WebviewPanel, enabling the creation of bespoke custom views with the following powerful features + - webviews can be full blown web-apps powered by Vue.js, Vite, VueUse, TailwindCSS and other great tech - UI in Webviews seamlessly blends with VS Code's familiar look'n'feel, even adjusting to each user's color theme - Webviews can have bi-directional communication with the host VS Code extension - initial state can be injected into a Webview, allowing reuse of already fetched data and reducing the need for loading spinners on init @@ -40,6 +55,12 @@ - Webview panel gets reused without being destroyed if it is re-invoked when the user has a ViewColumn active which isn't the one already containing the running Webview - text content in Webviews can be searched with Ctrl + F and additional actions Copy/Paste/Cut are available on right click or by using their common keyboard shortcuts - Webviews are secured with strict Content Security Policy (CSP) +- **state:** rewrite shared state management across the entire extension from simplistic, localised, highly interdependent and brittle, procedural approach to a new declarative, reactive, global, scalable architecture powered by [`pinia`](https://pinia.vuejs.org/) and [`@vue/reactivity`](https://www.npmjs.com/package/@vue/reactivity). This enables sharing state across sibling views/entities that was previously too hard, enabling more performant solutions and features that were previously too impractical to tackle, while the code for them can be much more maintainable and less likely to regress in the future. + +### 📖 Documentation + +- **readme:** fix typos, improve title and intro copy, update milestone link +- **contributing:** document recommended extensions for development with VS Code in [.vscode/extensions.json](.vscode/extensions.json) and add related section in the repo's contribution guide. ----- @@ -58,7 +79,7 @@ - **patch-list:** use new better-fitting icon for merged Patches ([#75](https://github.com/cytechmobile/radicle-vscode-extension/issues/75)) - **patch-list:** improve the contrast of the colors used by Patch status icons for light themes ([#75](https://github.com/cytechmobile/radicle-vscode-extension/issues/75)) -### 🔥 Performance +### 🏎️ Performance - **app:** heavily speed up most procedures by memoizing the resolution of the reference to the rad CLI ([#75](https://github.com/cytechmobile/radicle-vscode-extension/issues/75)) - **patch-list:** heavily speed up (re-)loading of Patches list ([#75](https://github.com/cytechmobile/radicle-vscode-extension/issues/75)) @@ -173,6 +194,7 @@ - ❤️🪵 Initial ["Heartwood"](https://app.radicle.xyz/seeds/seed.radicle.xyz/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5) support - 🔐 Integrated authentication + - 📥 Cloning of tracked Radicle projects - 🏗️ Improved development tooling and infrastructure for maintainers diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab253a53..9e7226d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,10 +48,15 @@ pnpm install a-new-pkg # if this is what you need to do npx pnpm install a-new-pkg # you can achieve it like this ``` +## Developing with VS Code + +While not required, for development it's strongly advised to use VS Code with all the [recommended extensions](.vscode/extensions.json) installed and enabled in the workspace of this repo. + ## Conventions ### Paths + Because of module resolution restrictions, all paths must be relative. e.g. instead of `src/utils` (even if import auto-completion writes that) it should be corrected to `../utils`. ### Getting user input diff --git a/README.md b/README.md index 37b6f961..dec0c3d1 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ ![radicle-screenshot](./assets/for-md/hero.png) -# Radicle +# Radicle VS Code Extension A VS Code extension bringing support for the Radicle network to your IDE. -[Radicle](https://radicle.xyz/) is a Github alternative bringing familiar features (e.g. Pull Requests, Issues, etc) over a free, open-source and peer-to-peer network, built on top of Git. +[Radicle](https://radicle.xyz/) is a GitHub alternative bringing familiar features like Pull Requests, Issues, etc, via a peer-to-peer, free and open-source network built on top of Git. -> **NOTE:** The extension currently offers partial support for the latest version of the Radicle CLI (codename "Heartwood"). Support for Radicle Patches is [under active develpment](https://github.com/cytechmobile/radicle-vscode-extension/milestone/4) and further features like Issues are next on the pipeline. +> **NOTE:** The extension currently offers partial support for the latest version of the Radicle CLI (codename "Heartwood"). Support for Radicle Patches is [under active development](https://github.com/cytechmobile/radicle-vscode-extension/milestone/4) and further features like Issues are next on the pipeline. ## Features diff --git a/package.json b/package.json index 7b777c9a..07f44615 100644 --- a/package.json +++ b/package.json @@ -127,10 +127,17 @@ { "command": "radicle.checkoutPatch", "title": "Check Out Patch Branch", - "shortTitle": "Check Out", + "shortTitle": "Check Out Patch", "category": "Radicle", "icon": "$(check)" }, + { + "command": "radicle.checkoutDefaultBranch", + "title": "Check Out Project's Default Git Branch", + "shortTitle": "Check Out Default", + "category": "Radicle", + "icon": "$(home)" + }, { "command": "radicle.copyPatchId", "title": "Copy Patch Identifier to Clipboard", @@ -209,6 +216,11 @@ "when": "view == patches-view && viewItem =~ /patch:checked-out-false/", "group": "inline@2" }, + { + "command": "radicle.checkoutDefaultBranch", + "when": "view == patches-view && viewItem =~ /patch:checked-out-true/", + "group": "inline@2" + }, { "command": "radicle.copyPatchId", "when": "view == patches-view && viewItem =~ /patch/" @@ -396,7 +408,7 @@ { "id": "patch.archived", "defaults": { - "dark": "#eb554d", + "dark": "#dd655f", "light": "#c74942", "highContrast": "editor.foreground", "highContrastLight": "editor.foreground" @@ -454,9 +466,11 @@ "*.{vue,ts,tsx,js,jsx}": "eslint --fix --max-warnings 0 --cache --cache-location node_modules/.cache/eslint" }, "dependencies": { + "@vue/reactivity": "^3.4.5", "javascript-time-ago": "^2.5.9", "lodash": "^4.17.21", - "ofetch": "^1.2.1" + "ofetch": "^1.2.1", + "pinia": "^2.1.7" }, "devDependencies": { "@antfu/eslint-config": "^0.39.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30036600..d7b12a1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@vue/reactivity': + specifier: ^3.4.5 + version: 3.4.5 javascript-time-ago: specifier: ^2.5.9 version: 2.5.9 @@ -14,6 +17,9 @@ dependencies: ofetch: specifier: ^1.2.1 version: 1.2.1 + pinia: + specifier: ^2.1.7 + version: 2.1.7(typescript@5.1.6)(vue@3.4.5) devDependencies: '@antfu/eslint-config': @@ -183,12 +189,10 @@ packages: /@babel/helper-string-parser@7.22.5: resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-validator-identifier@7.22.5: resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} engines: {node: '>=6.9.0'} - dev: true /@babel/highlight@7.22.5: resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==} @@ -207,6 +211,14 @@ packages: '@babel/types': 7.22.5 dev: true + /@babel/parser@7.23.6: + resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.22.5 + dev: false + /@babel/types@7.22.5: resolution: {integrity: sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==} engines: {node: '>=6.9.0'} @@ -214,7 +226,6 @@ packages: '@babel/helper-string-parser': 7.22.5 '@babel/helper-validator-identifier': 7.22.5 to-fast-properties: 2.0.0 - dev: true /@esbuild/android-arm64@0.19.9: resolution: {integrity: sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ==} @@ -496,7 +507,6 @@ packages: /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: true /@jridgewell/trace-mapping@0.3.18: resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} @@ -771,6 +781,16 @@ packages: source-map-js: 1.0.2 dev: true + /@vue/compiler-core@3.4.5: + resolution: {integrity: sha512-Daka7P1z2AgKjzuueWXhwzIsKu0NkLB6vGbNVEV2iJ8GJTrzraZo/Sk4GWCMRtd/qVi3zwnk+Owbd/xSZbwHtQ==} + dependencies: + '@babel/parser': 7.23.6 + '@vue/shared': 3.4.5 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.0.2 + dev: false + /@vue/compiler-dom@3.3.4: resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==} dependencies: @@ -778,6 +798,13 @@ packages: '@vue/shared': 3.3.4 dev: true + /@vue/compiler-dom@3.4.5: + resolution: {integrity: sha512-J8YlxknJVd90SXFJ4HwGANSAXsx5I0lK30sO/zvYV7s5gXf7gZR7r/1BmZ2ju7RGH1lnc6bpBc6nL61yW+PsAQ==} + dependencies: + '@vue/compiler-core': 3.4.5 + '@vue/shared': 3.4.5 + dev: false + /@vue/compiler-sfc@3.3.4: resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==} dependencies: @@ -793,6 +820,20 @@ packages: source-map-js: 1.0.2 dev: true + /@vue/compiler-sfc@3.4.5: + resolution: {integrity: sha512-jauvkDuSSUbP0ebhfNqljhShA90YEfX/0wZ+w40oZF43IjGyWYjqYaJbvMJwGOd+9+vODW6eSvnk28f0SGV7OQ==} + dependencies: + '@babel/parser': 7.23.6 + '@vue/compiler-core': 3.4.5 + '@vue/compiler-dom': 3.4.5 + '@vue/compiler-ssr': 3.4.5 + '@vue/shared': 3.4.5 + estree-walker: 2.0.2 + magic-string: 0.30.5 + postcss: 8.4.33 + source-map-js: 1.0.2 + dev: false + /@vue/compiler-ssr@3.3.4: resolution: {integrity: sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==} dependencies: @@ -800,6 +841,17 @@ packages: '@vue/shared': 3.3.4 dev: true + /@vue/compiler-ssr@3.4.5: + resolution: {integrity: sha512-DDdEcDzj2lWTMfUMMtEpLDhURai9LhM0zSZ219jCt7b2Vyl0/jy3keFgCPMitG0V1S1YG4Cmws3lWHWdxHQOpg==} + dependencies: + '@vue/compiler-dom': 3.4.5 + '@vue/shared': 3.4.5 + dev: false + + /@vue/devtools-api@6.5.1: + resolution: {integrity: sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==} + dev: false + /@vue/reactivity-transform@3.3.4: resolution: {integrity: sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==} dependencies: @@ -810,10 +862,45 @@ packages: magic-string: 0.30.2 dev: true + /@vue/reactivity@3.4.5: + resolution: {integrity: sha512-BcWkKvjdvqJwb7BhhFkXPLDCecX4d4a6GATvCduJQDLv21PkPowAE5GKuIE5p6RC07/Lp9FMkkq4AYCTVF5KlQ==} + dependencies: + '@vue/shared': 3.4.5 + dev: false + + /@vue/runtime-core@3.4.5: + resolution: {integrity: sha512-wh9ELIOQKeWT9SaUPdLrsxRkZv14jp+SJm9aiQGWio+/MWNM3Lib0wE6CoKEqQ9+SCYyGjDBhTOTtO47kCgbkg==} + dependencies: + '@vue/reactivity': 3.4.5 + '@vue/shared': 3.4.5 + dev: false + + /@vue/runtime-dom@3.4.5: + resolution: {integrity: sha512-n5ewvOjyG3IEpqGBahdPXODFSpVlSz3H4LF76Sx0XAqpIOqyJ5bIb2PrdYuH2ogBMAQPh+o5tnoH4nJpBr8U0Q==} + dependencies: + '@vue/runtime-core': 3.4.5 + '@vue/shared': 3.4.5 + csstype: 3.1.3 + dev: false + + /@vue/server-renderer@3.4.5(vue@3.4.5): + resolution: {integrity: sha512-jOFc/VE87yvifQpNju12VcqimH8pBLxdcT+t3xMeiED1K6DfH9SORyhFEoZlW5TG2Vwfn3Ul5KE+1aC99xnSBg==} + peerDependencies: + vue: 3.4.5 + dependencies: + '@vue/compiler-ssr': 3.4.5 + '@vue/shared': 3.4.5 + vue: 3.4.5(typescript@5.1.6) + dev: false + /@vue/shared@3.3.4: resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==} dev: true + /@vue/shared@3.4.5: + resolution: {integrity: sha512-6XptuzlMvN4l4cDnDw36pdGEV+9njYkQ1ZE0Q6iZLwrKefKaOJyiFmcP3/KBDHbt72cJZGtllAc1GaHe6XGAyg==} + dev: false + /acorn-jsx@5.3.2(acorn@8.10.0): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1088,6 +1175,10 @@ packages: hasBin: true dev: true + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + dev: false + /debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -1201,7 +1292,6 @@ packages: /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - dev: true /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -1732,7 +1822,6 @@ packages: /estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - dev: true /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} @@ -2350,6 +2439,13 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /magic-string@0.30.5: + resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: false + /mdast-util-from-markdown@0.8.5: resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==} dependencies: @@ -2426,6 +2522,11 @@ packages: hasBin: true dev: true + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} dev: true @@ -2640,7 +2741,6 @@ packages: /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: true /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} @@ -2663,6 +2763,24 @@ packages: engines: {node: '>=4'} dev: true + /pinia@2.1.7(typescript@5.1.6)(vue@3.4.5): + resolution: {integrity: sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==} + peerDependencies: + '@vue/composition-api': ^1.4.0 + typescript: '>=4.4.4' + vue: ^2.6.14 || ^3.3.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + typescript: + optional: true + dependencies: + '@vue/devtools-api': 6.5.1 + typescript: 5.1.6 + vue: 3.4.5(typescript@5.1.6) + vue-demi: 0.14.6(vue@3.4.5) + dev: false + /pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -2673,29 +2791,29 @@ packages: engines: {node: '>=4'} dev: true - /postcss-import@15.1.0(postcss@8.4.27): + /postcss-import@15.1.0(postcss@8.4.33): resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} peerDependencies: postcss: ^8.0.0 dependencies: - postcss: 8.4.27 + postcss: 8.4.33 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.3 dev: true - /postcss-js@4.0.1(postcss@8.4.27): + /postcss-js@4.0.1(postcss@8.4.33): resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} engines: {node: ^12 || ^14 || >= 16} peerDependencies: postcss: ^8.4.21 dependencies: camelcase-css: 2.0.1 - postcss: 8.4.27 + postcss: 8.4.33 dev: true - /postcss-load-config@4.0.1(postcss@8.4.27): + /postcss-load-config@4.0.1(postcss@8.4.33): resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} engines: {node: '>= 14'} peerDependencies: @@ -2708,17 +2826,17 @@ packages: optional: true dependencies: lilconfig: 2.1.0 - postcss: 8.4.27 + postcss: 8.4.33 yaml: 2.3.1 dev: true - /postcss-nested@6.0.1(postcss@8.4.27): + /postcss-nested@6.0.1(postcss@8.4.33): resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} engines: {node: '>=12.0'} peerDependencies: postcss: ^8.2.14 dependencies: - postcss: 8.4.27 + postcss: 8.4.33 postcss-selector-parser: 6.0.13 dev: true @@ -2774,6 +2892,14 @@ packages: source-map-js: 1.0.2 dev: true + /postcss@8.4.33: + resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.0.2 + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3020,7 +3146,6 @@ packages: /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} - dev: true /source-map-resolve@0.6.0: resolution: {integrity: sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==} @@ -3186,11 +3311,11 @@ packages: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.0 - postcss: 8.4.27 - postcss-import: 15.1.0(postcss@8.4.27) - postcss-js: 4.0.1(postcss@8.4.27) - postcss-load-config: 4.0.1(postcss@8.4.27) - postcss-nested: 6.0.1(postcss@8.4.27) + postcss: 8.4.33 + postcss-import: 15.1.0(postcss@8.4.33) + postcss-js: 4.0.1(postcss@8.4.33) + postcss-load-config: 4.0.1(postcss@8.4.33) + postcss-nested: 6.0.1(postcss@8.4.33) postcss-selector-parser: 6.0.13 resolve: 1.22.3 sucrase: 3.34.0 @@ -3218,7 +3343,6 @@ packages: /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} - dev: true /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -3322,7 +3446,6 @@ packages: resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} engines: {node: '>=14.17'} hasBin: true - dev: true /ufo@1.2.0: resolution: {integrity: sha512-RsPyTbqORDNDxqAdQPQBpgqhWle1VcTSou/FraClYlHf6TZnQcGslpLcAphNR+sQW4q5lLWLbOsRlh9j24baQg==} @@ -3360,6 +3483,21 @@ packages: spdx-expression-parse: 3.0.1 dev: true + /vue-demi@0.14.6(vue@3.4.5): + resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + dependencies: + vue: 3.4.5(typescript@5.1.6) + dev: false + /vue-eslint-parser@9.3.1(eslint@8.45.0): resolution: {integrity: sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==} engines: {node: ^14.17.0 || >=16.0.0} @@ -3378,6 +3516,22 @@ packages: - supports-color dev: true + /vue@3.4.5(typescript@5.1.6): + resolution: {integrity: sha512-VH6nHFhLPjgu2oh5vEBXoNZxsGHuZNr3qf4PHClwJWw6IDqw6B3x+4J+ABdoZ0aJuT8Zi0zf3GpGlLQCrGWHrw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@vue/compiler-dom': 3.4.5 + '@vue/compiler-sfc': 3.4.5 + '@vue/runtime-dom': 3.4.5 + '@vue/server-renderer': 3.4.5(vue@3.4.5) + '@vue/shared': 3.4.5 + typescript: 5.1.6 + dev: false + /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: diff --git a/src/extension.ts b/src/extension.ts index d93f1982..f5633a14 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,5 @@ import type { ExtensionContext } from 'vscode' -import { initExtensionContext } from './store' +import { initExtensionContext } from './stores' import { logExtensionActivated, registerAllCommands, @@ -14,6 +14,7 @@ import { validateRadicleIdentityAuthentication, } from './ux' +// TODO: maninak show blank (or "Activating extension...") welcome screen while whenClauseContext vars are not yet set? export function activate(ctx: ExtensionContext) { initExtensionContext(ctx) diff --git a/src/helpers/command.ts b/src/helpers/command.ts index 646d4675..a64aae81 100644 --- a/src/helpers/command.ts +++ b/src/helpers/command.ts @@ -1,13 +1,13 @@ import { type TextDocumentShowOptions, Uri, commands, window } from 'vscode' -import { getExtensionContext } from '../store' +import { type AugmentedPatch, getExtensionContext, usePatchStore } from '../stores' import { exec, log, showLog } from '../utils' import { type FilechangeNode, + checkOutDefaultBranch, checkOutPatch, copyToClipboardAndNotify, deAuthCurrentRadicleIdentity, launchAuthenticationFlow, - refreshPatchesEventEmitter, selectAndCloneRadicleProject, } from '../ux' import type { Patch } from '../types' @@ -92,9 +92,10 @@ export function registerAllCommands(): void { commands.executeCommand('workbench.actions.treeView.patches-view.collapseAll') }) registerVsCodeCmd('radicle.refreshPatches', () => { - refreshPatchesEventEmitter.fire(undefined) + usePatchStore().resetAllPatches() }) registerVsCodeCmd('radicle.checkoutPatch', checkOutPatch) + registerVsCodeCmd('radicle.checkoutDefaultBranch', checkOutDefaultBranch) registerVsCodeCmd('radicle.copyPatchId', async (patch: Partial | undefined) => { typeof patch?.id === 'string' && (await copyToClipboardAndNotify(patch.id)) }) @@ -140,7 +141,7 @@ export function registerAllCommands(): void { } }, ) - registerVsCodeCmd('radicle.viewPatchDetails', (patch: Patch) => { + registerVsCodeCmd('radicle.viewPatchDetails', (patch: AugmentedPatch) => { createOrShowWebview(getExtensionContext(), patch) }) } diff --git a/src/helpers/config.ts b/src/helpers/config.ts index 1bdcab24..a832510a 100644 --- a/src/helpers/config.ts +++ b/src/helpers/config.ts @@ -65,6 +65,8 @@ export function setConfig( */ export function getDefaultPathToRadBinary(): string | undefined { const homeDir = exec('echo $HOME') + // TODO: maninak can I use `~` instead and skip the shell command? + // TODO: maninak what if it's a mac?? const defaultPath = homeDir ? `${homeDir}/.radicle/bin/rad` : undefined return defaultPath @@ -76,7 +78,7 @@ export function getDefaultPathToRadBinary(): string | undefined { * * @returns The path if successfully resolved, otherwise `undefined` */ -export function getValidatedDefaultPathToRadBinary(): string | undefined { +export function getValidatedPathToDefaultRadBinaryLocation(): string | undefined { const defaultPath = getDefaultPathToRadBinary() if (!defaultPath) { @@ -94,7 +96,7 @@ export function getValidatedDefaultPathToRadBinary(): string | undefined { * * @returns The path if successfully resolved, otherwise `undefined` */ -export function getValidatedAliasedPathToRadBinary(): string | undefined { +export function getValidatedPathToRadBinaryWhenAliased(): string | undefined { const aliasedPath = exec('which rad') if (!aliasedPath) { return undefined @@ -112,6 +114,7 @@ export function getValidatedAliasedPathToRadBinary(): string | undefined { * @returns The path if successfully resolved, otherwise `undefined` * @see https://radicle.xyz/install */ +// TODO: maninak memoize export function getDefaultPathToNodeHome(): string | undefined { const homeDir = exec('echo $HOME') const defaultPath = homeDir ? `${homeDir}/.radicle` : undefined diff --git a/src/helpers/configWatcher.ts b/src/helpers/configWatcher.ts index bed4ec3a..e496e4f4 100644 --- a/src/helpers/configWatcher.ts +++ b/src/helpers/configWatcher.ts @@ -1,11 +1,10 @@ import { workspace } from 'vscode' import { - refreshPatchesEventEmitter, validateHttpdConnection, validateRadCliInstallation, validateRadicleIdentityAuthentication, } from '../ux/' -import { getExtensionContext } from '../store' +import { getExtensionContext, usePatchStore } from '../stores' import { type ExtensionConfig, resetHttpdConnection } from '.' function onConfigChange( @@ -26,6 +25,7 @@ interface OnConfigChangeParam { onChangeCallback: Parameters['1'] } +// TODO: maninak instead of calling stuff directly to change, onChange set the values in a new configStore and have other things depend on it const configWatchers = [ { configKey: 'radicle.advanced.pathToRadBinary', @@ -39,7 +39,7 @@ const configWatchers = [ onChangeCallback: () => { // no need to notify since we check AND notify on rad command execution validateRadicleIdentityAuthentication({ minimizeUserNotifications: true }) - refreshPatchesEventEmitter.fire(undefined) + usePatchStore().resetAllPatches() }, }, { @@ -47,7 +47,7 @@ const configWatchers = [ onChangeCallback: () => { resetHttpdConnection() validateHttpdConnection() - refreshPatchesEventEmitter.fire(undefined) + usePatchStore().resetAllPatches() }, }, ] satisfies OnConfigChangeParam[] diff --git a/src/helpers/fetchFromHttpd.ts b/src/helpers/fetchFromHttpd.ts index e7d9869c..4cdaaa9a 100644 --- a/src/helpers/fetchFromHttpd.ts +++ b/src/helpers/fetchFromHttpd.ts @@ -7,6 +7,7 @@ import { getConfig } from './config' // if we ever use a proper reactive global store like pinia, `doFetch()` should move in there // and a watcher should run `resetHttpdConnection()` upon change of config // `radicle.advanced.httpApiEndpoint` (as of writing this, at least ^_^) +// TODO: maninak either do this in configStore, or consider having an httpdStore where we export fetchFromHttpd as `useHttpd().fetch()` and internally doFetch is a computed that depends on `useConfigStore().resolvedHttpdRootUrl` or `radicle.advanced.httpApiEndpoint` with some other name let doFetch: $Fetch /** @@ -15,7 +16,7 @@ let doFetch: $Fetch * Should be run each time any of the dependencies get updated for them to take effect. */ export function resetHttpdConnection(): void { - doFetch = ofetch.create({ baseURL: getResolvedHttpdRootUrl(), query: { perPage: 100 } }) + doFetch = ofetch.create({ baseURL: getResolvedHttpdRootUrl(), query: { perPage: 200 } }) } type FetchFromHttpdReturn = Promise< diff --git a/src/helpers/fileWatcher.ts b/src/helpers/fileWatcher.ts index 29154451..769d71c4 100644 --- a/src/helpers/fileWatcher.ts +++ b/src/helpers/fileWatcher.ts @@ -5,11 +5,8 @@ import { isGitRepo, setWhenClauseContext, } from '../utils' -import { - notifyUserRadCliNotResolvedAndMaybeTroubleshoot, - refreshPatchesEventEmitter, -} from '../ux' -import { getExtensionContext } from '../store' +import { notifyUserRadCliNotResolvedAndMaybeTroubleshoot } from '../ux' +import { getExtensionContext, useGitStore } from '../stores' import { isRadCliInstalled, isRadInitialized } from '.' // A very hacky and specialized wrapper. If it doesn't meet your use case, consider @@ -37,6 +34,7 @@ interface WatchFileNotInWorkspaceParam { handler: Parameters['1'] } +// TODO: maninak replace `getRepoRoot()` with gitStore access? const notInWorkspaceFileWatchers = [ { glob: () => @@ -67,10 +65,11 @@ const notInWorkspaceFileWatchers = [ 'HEAD', ), handler: () => { - refreshPatchesEventEmitter.fire(undefined) + useGitStore().refreshCurentBranch() }, }, (() => { + // TODO: maninak verify those still apply and align with getDefaultPathToRadBinary() switch (process.platform) { case 'linux': return { @@ -90,13 +89,13 @@ const notInWorkspaceFileWatchers = [ return undefined } })(), -] satisfies (WatchFileNotInWorkspaceParam | undefined)[] +].filter(Boolean) satisfies WatchFileNotInWorkspaceParam[] /** * Registers all handlers to be called whenever specific files get changed. */ -export function registerAllFileWatchers(): void { - notInWorkspaceFileWatchers.forEach((fw) => { - fw && watchFileNotInWorkspace(fw.glob, fw.handler) +export function registerAllFileWatchers() { + notInWorkspaceFileWatchers.forEach((fileWatcher) => { + watchFileNotInWorkspace(fileWatcher.glob, fileWatcher.handler) }) } diff --git a/src/helpers/logExtensionActivated.ts b/src/helpers/logExtensionActivated.ts index 113d3f84..8c057f34 100644 --- a/src/helpers/logExtensionActivated.ts +++ b/src/helpers/logExtensionActivated.ts @@ -1,4 +1,4 @@ -import { getExtensionContext } from '../store' +import { getExtensionContext } from '../stores' import { log } from '../utils' interface PackageJson { diff --git a/src/helpers/radCli.ts b/src/helpers/radCli.ts index cb36fa2e..11a20274 100644 --- a/src/helpers/radCli.ts +++ b/src/helpers/radCli.ts @@ -2,8 +2,8 @@ import { assertUnreachable, exec, memoizeWithDebouncedCacheClear } from '../util import { getConfig, getResolvedPathToNodeHome, - getValidatedAliasedPathToRadBinary, - getValidatedDefaultPathToRadBinary, + getValidatedPathToDefaultRadBinaryLocation, + getValidatedPathToRadBinaryWhenAliased, } from '.' /** @@ -19,8 +19,8 @@ export function getRadCliRefNow(): string { const radCliRef = getConfig('radicle.advanced.pathToRadBinary') || - (Boolean(getValidatedAliasedPathToRadBinary()) && 'rad') || - getValidatedDefaultPathToRadBinary() + (Boolean(getValidatedPathToRadBinaryWhenAliased()) && 'rad') || + getValidatedPathToDefaultRadBinaryLocation() if (!radCliRef) { throw new Error('Failed resolving reference to Radicle Cli binary') } @@ -61,8 +61,8 @@ export function getRadCliPath(): string | undefined { if (isRadCliInstalled()) { radCliPath = getConfig('radicle.advanced.pathToRadBinary') ?? - getValidatedDefaultPathToRadBinary() ?? - getValidatedAliasedPathToRadBinary() + getValidatedPathToDefaultRadBinaryLocation() ?? + getValidatedPathToRadBinaryWhenAliased() } return radCliPath @@ -86,6 +86,7 @@ export function getRadCliVersion(): string | undefined { * * @returns `true` if found, otherwise `false`. */ +// TODO: maninak memoize export function isRadCliInstalled(): boolean { const isInstalled = Boolean(exec(getRadCliRef())) @@ -99,7 +100,7 @@ export function isRadCliInstalled(): boolean { * @returns `true` if the workspace is a rad-initialized repo, otherwise `false`. */ export function isRadInitialized(): boolean { - const isInitialized = Boolean(exec(`${getRadCliRef()} inspect`, { cwd: '$workspaceDir' })) + const isInitialized = Boolean(getCurrentProjectId()) return isInitialized } @@ -188,6 +189,7 @@ export function getRadicleIdentity(format: 'DID' | 'NID') { assertUnreachable(format) } + // TODO: maninak reuse shell for second exec const id = exec(`${getRadCliRef()} self ${flag}`) const alias = exec(`${getRadCliRef()} self --alias`) @@ -200,11 +202,12 @@ export function getRadicleIdentity(format: 'DID' | 'NID') { } /** - * Resolves the Repository Identifier (RID) of the currently open workspace directory. + * Resolves the Radicle project id (RID) of the currently open workspace + * directory. * * @returns The RID if resolved, otherwise `undefined`. */ -export function getRepoId(): `rad:${string}` | undefined { +export function getCurrentProjectId(): `rad:${string}` | undefined { const maybeRid = exec(`${getRadCliRef()} inspect --rid`, { cwd: '$workspaceDir' }) function isStrARid(str: string | undefined): str is `rad:${string}` { @@ -214,9 +217,9 @@ export function getRepoId(): `rad:${string}` | undefined { return isStrARid(maybeRid) ? maybeRid : undefined } export const { - memoizedFunc: memoizedGetRepoId, - debouncedClearMemoizedFuncCache: debouncedClearMemoizedgetRepoIdCache, -} = memoizeWithDebouncedCacheClear(getRepoId, 10_000) + memoizedFunc: memoizedGetCurrentProjectId, + debouncedClearMemoizedFuncCache: debouncedClearMemoizedGetCurrentProjectIdCache, +} = memoizeWithDebouncedCacheClear(getCurrentProjectId, 10_000) /** * Resolves the cryptographic public key of the Radicle identity found in the resolved diff --git a/src/helpers/webview.ts b/src/helpers/webview.ts index 97e6f0c6..3fbcbdf9 100644 --- a/src/helpers/webview.ts +++ b/src/helpers/webview.ts @@ -3,24 +3,26 @@ import { Uri, ViewColumn, type Webview, - type WebviewOptions, type WebviewPanel, - type WebviewPanelOptions, window, } from 'vscode' -import { getExtensionContext } from '../store' +import { + type AugmentedPatch, + getExtensionContext, + usePatchStore, + useWebviewStore, +} from '../stores' import { getNonce, truncateKeepWords } from '../utils' import { type notifyExtension, notifyWebview as notifyWebviewBase, } from '../utils/webview-messaging' import type { Patch, PatchDetailInjectedState } from '../types' -import { copyToClipboardAndNotify } from '../ux' +import { checkOutDefaultBranch, checkOutPatch, copyToClipboardAndNotify } from '../ux' +// TODO: maninak move into store export const webviewId = 'webview-patch-detail' -let panel: WebviewPanel | undefined - // TODO: maninak make the solution in file more generic, not only useful to a specific webview // TODO: maninak move this file (and other found in helpers) to "/services" or "/providers" @@ -32,54 +34,45 @@ let panel: WebviewPanel | undefined * * @param [title] - The title shown on the webview panel's tab */ -export function createOrShowWebview(ctx: ExtensionContext, patch: Patch) { - const webviewState: PatchDetailInjectedState = { - kind: 'patchDetail', - id: patch.id, - ts: Date.now(), - state: patch, - } - +export function createOrShowWebview(ctx: ExtensionContext, patch: AugmentedPatch) { const column = window.activeTextEditor ? window.activeTextEditor.viewColumn : undefined - let isPanelDisposed - try { - // eslint-disable-next-line no-unused-expressions - panel?.webview // getter will throw if panel is disposed - isPanelDisposed = false - } catch { - isPanelDisposed = true - } + const webviewStore = useWebviewStore() + const foundPanel = webviewStore.findPanel(webviewId) - if (panel && !isPanelDisposed) { - notifyWebview({ command: 'updateState', payload: webviewState }) - panel.title = getPanelTitle(patch) + // If panel already exists and is usable then re-use it - panel.reveal(column) + if (foundPanel && !webviewStore.isPanelDisposed(foundPanel)) { + notifyWebview({ command: 'updateState', payload: getStateForWebview(patch) }) + foundPanel.title = getPanelTitle(patch) + + foundPanel.reveal(column) return } - const webviewOptions: WebviewPanelOptions & WebviewOptions = { - enableScripts: true, - localResourceRoots: [ - Uri.joinPath(getExtensionContext().extensionUri, 'dist'), - Uri.joinPath(getExtensionContext().extensionUri, 'assets'), - Uri.joinPath(getExtensionContext().extensionUri, 'src', 'webviews', 'dist'), - ], - enableFindWidget: true, - } - panel = window.createWebviewPanel( + // Otherwise create new panel from scratch + + const newPanel = window.createWebviewPanel( webviewId, getPanelTitle(patch), column || ViewColumn.One, - webviewOptions, + { + enableScripts: true, + localResourceRoots: [ + Uri.joinPath(getExtensionContext().extensionUri, 'dist'), + Uri.joinPath(getExtensionContext().extensionUri, 'assets'), + Uri.joinPath(getExtensionContext().extensionUri, 'src', 'webviews', 'dist'), + ], + enableFindWidget: true, + }, ) + webviewStore.trackPanel(newPanel) - panel.webview.html = getWebviewHtml(panel.webview, webviewState) + newPanel.webview.html = getWebviewHtml(newPanel.webview, getStateForWebview(patch)) - panel.webview.onDidReceiveMessage( - (message: Parameters['0']) => { + newPanel.webview.onDidReceiveMessage( + async (message: Parameters['0']) => { switch (message.command) { case 'showInfoNotification': { const button = 'Reset Count' @@ -91,16 +84,32 @@ export function createOrShowWebview(ctx: ExtensionContext, patch: Patch) { } case 'copyToClipboardAndNotify': copyToClipboardAndNotify(message.payload.textToCopy) + break + case 'refreshPatchData': + usePatchStore().refetchPatch(message.payload.patchId) + break + case 'checkOutPatchBranch': + checkOutPatch(message.payload.patch) + break + case 'checkOutDefaultBranch': + await checkOutDefaultBranch() + break } }, undefined, ctx.subscriptions, ) - panel.onDidDispose(() => (panel = undefined), undefined, ctx.subscriptions) + + newPanel.onDidDispose( + () => webviewStore.untrackPanel(newPanel), + undefined, + ctx.subscriptions, + ) } export function notifyWebview(message: Parameters['0']): void { + const panel = useWebviewStore().patchDetailPanel panel && notifyWebviewBase(message, panel.webview) } @@ -111,8 +120,8 @@ export function registerAllWebviewRestorators() { window.registerWebviewPanelSerializer(webviewId, { // eslint-disable-next-line @typescript-eslint/require-await, require-await deserializeWebviewPanel: async (_panel: WebviewPanel, _state: unknown) => { - panel = _panel - panel.webview.html = getWebviewHtml(panel.webview) + _panel.webview.html = getWebviewHtml(_panel.webview) + useWebviewStore().trackPanel(_panel) }, }), ) @@ -169,6 +178,19 @@ function getWebviewHtml(webview: Webview, state?: State) { return html } +function getStateForWebview(patch: AugmentedPatch): PatchDetailInjectedState { + const isCheckedOut = patch.id === usePatchStore().checkedOutPatch?.id + const state: PatchDetailInjectedState = { + kind: webviewId, + id: patch.id, + state: { + patch: { ...patch, isCheckedOut }, + }, + } + + return state +} + function getPanelTitle(patch: Patch) { const truncatedTitle = truncateKeepWords(patch.title, 30) diff --git a/src/store/index.ts b/src/store/index.ts deleted file mode 100644 index 630b4d7e..00000000 --- a/src/store/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './context' diff --git a/src/store/context.ts b/src/stores/context.ts similarity index 92% rename from src/store/context.ts rename to src/stores/context.ts index 7b210fac..5f4e84f1 100644 --- a/src/store/context.ts +++ b/src/stores/context.ts @@ -1,5 +1,7 @@ import type { ExtensionContext } from 'vscode' +// TODO: maninak move these into an envStore + let ctx: ExtensionContext /** diff --git a/src/stores/gitStore.ts b/src/stores/gitStore.ts new file mode 100644 index 00000000..1bde6b4c --- /dev/null +++ b/src/stores/gitStore.ts @@ -0,0 +1,29 @@ +import { createPinia, defineStore, setActivePinia } from 'pinia' +import { ref } from '@vue/reactivity' +import { getCurrentGitBranch } from '../utils' + +setActivePinia(createPinia()) + +/* + *TODO: maninak see if I can use native git plugin's getters instead of my own utils + * and if they are faster (mine are slow ~20-40ms for each util call) + * + * Resources: + * - https://github.com/walles/git-commit-message-plus/blob/83bebe0321e99f616a08d9a043f2cdd790d5e26b/src/extension.ts#L82-L118 + * - https://github.com/microsoft/vscode/blob/08d383346c18f6b20cb74219611f7c1b590c35b1/extensions/git/README.md#git-integration-for-visual-studio-code + * - https://github.com/microsoft/vscode/blob/main/extensions/git/src/api/api1.ts#L160 + * - https://github.com/microsoft/vscode-pull-request-github/blob/0068c135d1c3e5ce601c1d5c7f7007904e59901e/src/extension.ts#L53 + * - https://github.com/Microsoft/vscode/blob/main/extensions/git/src/api/git.d.ts + * - https://code.visualstudio.com/api/references/vscode-api#extensions + * - https://stackoverflow.com/a/60238771/5015955 + */ + +export const useGitStore = defineStore('gitStore', () => { + const currentBranch = ref() + + function refreshCurentBranch() { + currentBranch.value = getCurrentGitBranch() + } + + return { currentBranch, refreshCurentBranch } +}) diff --git a/src/stores/index.ts b/src/stores/index.ts new file mode 100644 index 00000000..69a2bc54 --- /dev/null +++ b/src/stores/index.ts @@ -0,0 +1,4 @@ +export * from './context' +export * from './gitStore' +export * from './patchStore' +export * from './webviewStore' diff --git a/src/stores/patchStore.ts b/src/stores/patchStore.ts new file mode 100644 index 00000000..ad3aee21 --- /dev/null +++ b/src/stores/patchStore.ts @@ -0,0 +1,140 @@ +import { createPinia, defineStore, setActivePinia } from 'pinia' +import { computed, effect, ref } from '@vue/reactivity' +import { rerenderAllItemsInPatchesView, rerenderSomeItemsInPatchesView } from 'src/ux' +import { fetchFromHttpd, memoizedGetCurrentProjectId } from '../helpers' +import type { Patch } from '../types' +import { useGitStore } from '.' + +setActivePinia(createPinia()) + +export interface AugmentedPatch extends Patch { + lastFetchedTs: number +} + +export const usePatchStore = defineStore('patch', () => { + const patches = ref() + + effect(() => { + // TODO: diff prev-vs-new and re-render only those patches that got updated? + patches.value + ? rerenderSomeItemsInPatchesView(patches.value) + : rerenderAllItemsInPatchesView() + }) + + const prevCheckedOutPatch = ref() + const checkedOutPatch = computed((_prevCheckedOutPatch) => { + prevCheckedOutPatch.value = _prevCheckedOutPatch + const matchHexCharsAfterPatchRegex = /patch\/([0-9A-Fa-f]*)/ + const checkedOutPatchPartialId = useGitStore().currentBranch?.match( + matchHexCharsAfterPatchRegex, + )?.[1] + + const newCheckedOutPatch = checkedOutPatchPartialId + ? findPatchById(checkedOutPatchPartialId) + : undefined + + return newCheckedOutPatch + }) + effect(() => { + rerenderSomeItemsInPatchesView( + [prevCheckedOutPatch.value, checkedOutPatch.value].filter(Boolean), + ) + }) + + function resetAllPatches() { + patches.value = undefined + } + + async function refetchPatch(patchId: Patch['id']) { + const rid = memoizedGetCurrentProjectId() // TODO: maninak get from a store instead + if (!rid) { + return { error: new Error('Failed resolving RID') } + } + + const { data: fetchedPatch, error } = await fetchFromHttpd( + `/projects/${rid}/patches/${patchId}`, + ) + if (error) { + return { error } + } + + const outdatedPatch = findPatchById(fetchedPatch.id) + const augmentedFetchedPatch = { ...fetchedPatch, ...{ lastFetchedTs: Date.now() } } + if (outdatedPatch) { + // we use `Object.assign()` to keep the same object ref + Object.assign(outdatedPatch, augmentedFetchedPatch) + } else { + if (!patches.value) { + patches.value = [] + } + patches.value.push(augmentedFetchedPatch) + } + + return {} + } + + function findPatchById(partialOrWholeId: string) { + const foundPatch = patches.value?.find((patch) => patch.id.includes(partialOrWholeId)) + + return foundPatch + } + + let inProgressRequest: Promise | undefined + async function fetchAllPatches() { + if (inProgressRequest) { + await inProgressRequest + + return true + } + + const rid = memoizedGetCurrentProjectId() // TODO: maninak get from a store instead + if (!rid) { + return false + } + const nowTs = Date.now() + // TODO: refactor to make only a single request when https://radicle.zulipchat.com/#narrow/stream/369873-support/topic/fetch.20all.20patches.20in.20one.20req is resolved + const promisedResponses = Promise.all([ + fetchFromHttpd(`/projects/${rid}/patches`, 'GET', undefined, { + query: { state: 'draft' }, + }), + fetchFromHttpd(`/projects/${rid}/patches`, 'GET', undefined, { + query: { state: 'open' }, + }), + fetchFromHttpd(`/projects/${rid}/patches`, 'GET', undefined, { + query: { state: 'archived' }, + }), + fetchFromHttpd(`/projects/${rid}/patches`, 'GET', undefined, { + query: { state: 'merged' }, + }), + ]).finally(() => (inProgressRequest = undefined)) + inProgressRequest = promisedResponses + + const responses = await promisedResponses + const errors = responses.map((response) => response.error).filter(Boolean) + if (errors.length) { + return false + } + + const fetchedPatches = responses + .flatMap((response) => response.data) + .filter(Boolean) + .map((fetchedPatch) => ({ ...fetchedPatch, ...{ lastFetchedTs: nowTs } })) + + patches.value = fetchedPatches + + return true + } + + async function initStoreIfNeeded() { + return !patches.value && (await fetchAllPatches()) + } + + return { + patches, + checkedOutPatch, + findPatchById, + resetAllPatches, + refetchPatch, + initStoreIfNeeded, + } +}) diff --git a/src/stores/webviewStore.ts b/src/stores/webviewStore.ts new file mode 100644 index 00000000..cdac3d42 --- /dev/null +++ b/src/stores/webviewStore.ts @@ -0,0 +1,43 @@ +import type { WebviewPanel } from 'vscode' +import { createPinia, defineStore, setActivePinia } from 'pinia' +import { computed, reactive } from '@vue/reactivity' +import { webviewId } from '../helpers' + +setActivePinia(createPinia()) + +// TODO: maninak check why webviews are not reused +// TODO: maninak on shift/alt + click on item button to open webview, open always in newtab + +export const useWebviewStore = defineStore('webviewStore', () => { + const panels = reactive>(new Map()) + + const patchDetailPanel = computed(() => panels.get(webviewId)) + + function trackPanel(panel: WebviewPanel) { + // TODO: maninak create a `const stateForWebview = computed(...)` and store it along with this panel? + // Then call `effect(...)` that notifiesWebview to update its state whenever that computed is updated? + // Maybe move `getStateForWebview()` from webview.ts in here and rename it `getComputedStateForPatchDetailWebview()`? + return panels.set(panel.viewType, panel) + } + + function untrackPanel(panel: WebviewPanel) { + return panels.delete(panel.viewType) + } + + function findPanel(id: string) { + return panels.get(id) + } + + function isPanelDisposed(panel: WebviewPanel) { + try { + // eslint-disable-next-line no-unused-expressions + panel?.webview // getter will throw if panel is disposed + + return false + } catch { + return true + } + } + + return { patchDetailPanel, trackPanel, untrackPanel, findPanel, isPanelDisposed } +}) diff --git a/src/types/webviewInjectedState.ts b/src/types/webviewInjectedState.ts index d316f11f..694f432d 100644 --- a/src/types/webviewInjectedState.ts +++ b/src/types/webviewInjectedState.ts @@ -1,8 +1,12 @@ +import type { webviewId } from '../helpers' +import type { AugmentedPatch } from '../stores' import type { Patch } from './httpd' +// TODO: maninak rename as PatchDetailWebviewState? export interface PatchDetailInjectedState { - kind: 'patchDetail' + kind: typeof webviewId id: Patch['id'] - ts: number - state: Patch + state: { + patch: AugmentedPatch & { isCheckedOut: boolean } + } } diff --git a/src/utils/git.ts b/src/utils/git.ts index a027102f..285474c0 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -14,6 +14,7 @@ export function isGitRepo(): boolean { /** * Resolves the root directory of the git repo of the opened workspace folder. */ +// TODO: maninak memoize export function getRepoRoot(): string | undefined { const gitRepoRootDir = exec('git rev-parse --show-toplevel', { cwd: '$workspaceDir' }) diff --git a/src/utils/webview-messaging.ts b/src/utils/webview-messaging.ts index ad0a0c36..540cb1d0 100644 --- a/src/utils/webview-messaging.ts +++ b/src/utils/webview-messaging.ts @@ -1,6 +1,6 @@ import type { Webview } from 'vscode' import { getVscodeRef } from '../webviews/src/utils/getVscodeRef' -import type { PatchDetailInjectedState } from '../types' +import type { Patch, PatchDetailInjectedState } from '../types' interface Message { command: Command @@ -8,19 +8,34 @@ interface Message | Message<'updateState', PatchDetailInjectedState> + | Message<'resetCount'> type MessageToExtension = | Message<'showInfoNotification', { text: string }> | Message<'copyToClipboardAndNotify', { textToCopy: string }> + | Message<'refreshPatchData', { patchId: Patch['id'] }> + | Message<'checkOutPatchBranch', { patch: Patch }> + | Message<'checkOutDefaultBranch'> -/** Sends a message, usually from the host window, to the provided webview. */ +/** + * Sends a message, usually from the host window, to the provided webview. + * + * Note: only supports whatever Window.structuredClone() supports. This means that Proxys + * and specifically Vue objects like `Ref` and `Reactive` that use them are not supported. + * Make sure to wrap them with Vue.toRaw() before passing them as a payload. + */ export function notifyWebview(message: MessageToWebview, webview: Webview): void { webview.postMessage(message) } -/** Sends a message from within a webview to the VS Code extension hosting it. */ +/** + * Sends a message from within a webview to the VS Code extension hosting it. + * + * Note: only supports whatever Window.structuredClone() supports. This means that Proxys + * and specifically Vue objects like `Ref` and `Reactive` that use them are not supported. + * Make sure to wrap them with Vue.toRaw() before passing them as a payload. + */ export function notifyExtension(message: MessageToExtension): void { getVscodeRef().postMessage(message) } diff --git a/src/ux/checkOut.ts b/src/ux/checkOut.ts new file mode 100644 index 00000000..ee50d44a --- /dev/null +++ b/src/ux/checkOut.ts @@ -0,0 +1,77 @@ +import { window } from 'vscode' +import { fetchFromHttpd, getRadCliRef, memoizedGetCurrentProjectId } from '../helpers' +import type { Patch } from '../types' +import { exec, log, shortenHash, showLog } from '../utils' +import { notifyUserAboutFetchError } from '.' + +/** + * Checks out the default Git branch of the Radicle project currently open in the workspace. + * + * @returns A promise that resolves to `true` if successful, otherwise `false` + */ +export async function checkOutDefaultBranch(): Promise { + const rid = memoizedGetCurrentProjectId() + + if (!rid) { + log('Failed resolving RID', 'error') + + return false + } + + // TODO: maninak move into gitStore? + const { data: project, error } = await fetchFromHttpd(`/projects/${rid}`) + if (error) { + notifyUserAboutFetchError(error) + + return false + } + + const defaultBranch = project.defaultBranch + const didCheckoutDefaultBranch = + exec(`git checkout ${defaultBranch}`, { cwd: '$workspaceDir', shouldLog: true }) !== + undefined + if (!didCheckoutDefaultBranch) { + notifyUserGitCheckoutFailed(`Failed checking out branch "${defaultBranch}"`) + + return false + } + + return true +} + +/** + * Checks out the Git branch associated with the given Radicle `patch`. + * + * @returns `true` if successful, otherwise `false` + */ +export function checkOutPatch(patch: Pick): boolean { + const didCheckoutPatchBranch = Boolean( + exec(`${getRadCliRef()} patch checkout ${patch.id}`, { + cwd: '$workspaceDir', + shouldLog: true, + }), + ) + if (!didCheckoutPatchBranch) { + notifyUserGitCheckoutFailed(`Failed checking out Patch "${shortenHash(patch.id)}"`) + + return false + } + + return true +} + +function notifyUserGitCheckoutFailed(mainErrorMessage: string) { + const hasUncommitedChanges = + exec('git update-index --refresh && git diff-index --quiet HEAD --', { + cwd: '$workspaceDir', + }) === undefined // https://stackoverflow.com/a/3879077/5015955 + + const button = 'Show output' + const msg = `${mainErrorMessage}${ + hasUncommitedChanges ? '. Please stash or commit your changes and try again.' : '' + }` + log(msg, 'error') + window.showErrorMessage(msg, button).then((userSelection) => { + userSelection === button && showLog() + }) +} diff --git a/src/ux/httpdConnection.ts b/src/ux/httpdConnection.ts index 04b4c71f..9cc2040f 100644 --- a/src/ux/httpdConnection.ts +++ b/src/ux/httpdConnection.ts @@ -46,6 +46,7 @@ export async function validateHttpdConnection( * * @param error The error that was thrown when the recuest to httpd failed. */ +// TODO: maninak revisit all cases because the errors seem to have changed (due to Fetch v2?) and most/all cases seem to not match anymore export async function notifyUserAboutFetchError(error: FetchError): Promise { const requestUrl = error?.request?.toString() const buttonOutput = 'Show Output' diff --git a/src/ux/index.ts b/src/ux/index.ts index 437db357..138be690 100644 --- a/src/ux/index.ts +++ b/src/ux/index.ts @@ -1,8 +1,8 @@ +export * from './checkOut' export * from './clipboard' export * from './httpdConnection' export * from './patchesView' export * from './radCliInstallation' export * from './radicleIdentityAuth' -export * from './radiclePatches' export * from './radicleProject' export * from './settings' diff --git a/src/ux/patchesView.ts b/src/ux/patchesView.ts index 2f021d98..3e123b75 100644 --- a/src/ux/patchesView.ts +++ b/src/ux/patchesView.ts @@ -12,21 +12,20 @@ import { TreeItemCollapsibleState, Uri, } from 'vscode' +import { type AugmentedPatch, usePatchStore } from '../stores' import { - debouncedClearMemoizedgetRepoIdCache, + debouncedClearMemoizedGetCurrentProjectIdCache, fetchFromHttpd, - memoizedGetRepoId, + memoizedGetCurrentProjectId, } from '../helpers' import { type Patch, type Unarray, isPatch } from '../types' import { assertUnreachable, capitalizeFirstLetter, - getCurrentGitBranch, getFirstAndLatestRevisions, getIdentityAliasOrId, getTimeAgo, log, - memoizeWithDebouncedCacheClear, shortenHash, } from '../utils' @@ -47,42 +46,55 @@ export interface FilechangeNode { /** * Event emitter dedicated to refreshing the Patch view's tree data. */ -export const refreshPatchesEventEmitter = new EventEmitter< - string | Patch | (string | Patch)[] | undefined +const rerenderPatchesViewEventEmitter = new EventEmitter< + string | AugmentedPatch | (string | AugmentedPatch)[] | undefined >() -export const patchesTreeDataProvider: TreeDataProvider = { +export function rerenderSomeItemsInPatchesView( + patchesMatchingItems: AugmentedPatch | AugmentedPatch[], +) { + rerenderPatchesViewEventEmitter.fire(patchesMatchingItems) +} + +export function rerenderAllItemsInPatchesView() { + rerenderPatchesViewEventEmitter.fire(undefined) +} + +export const patchesTreeDataProvider: TreeDataProvider< + string | AugmentedPatch | FilechangeNode +> = { getTreeItem: (elem) => { if (typeof elem === 'string') { return { description: elem } } else if (isPatch(elem)) { - const isCheckedOut = isPatchCheckedOut(elem) - const edgeRevisions = getFirstAndLatestRevisions(elem) + const patch = elem + const isCheckedOut = patch.id === usePatchStore().checkedOutPatch?.id + const edgeRevisions = getFirstAndLatestRevisions(patch) const treeItem: TreeItem = { - id: elem.id, + id: patch.id, contextValue: `patch:checked-out-${isCheckedOut}`, - iconPath: getThemeIconForPatch(elem), - label: `${isCheckedOut ? `❬${checkmark}❭ ` : ''}${elem.title}`, - description: getPatchTreeItemDescription(elem, edgeRevisions), - tooltip: getPatchTreeItemTooltip(elem, edgeRevisions, isCheckedOut), + iconPath: getThemeIconForPatch(patch), + label: `${isCheckedOut ? `❬${checkmark}❭ ` : ''}${patch.title}`, + description: getPatchTreeItemDescription(patch, edgeRevisions), + tooltip: getPatchTreeItemTooltip(patch, edgeRevisions, isCheckedOut), collapsibleState: TreeItemCollapsibleState.Collapsed, } return treeItem - } - // elem is FilechangeNode - else { + } else { + const filechangeNode = elem + // We can't put the code to construct the filechange TreeItem inside getTreeItem() // because we need to perform operations on the whole collection (e.g. sort, search for // and handle items with same filename differently, etc). Thus we define a "constructor" // inside getChildren() and call that in here. - return elem.getTreeItem() + return filechangeNode.getTreeItem() } }, getChildren: async (elem) => { - debouncedClearMemoizedgetRepoIdCache() - const rid = memoizedGetRepoId() + debouncedClearMemoizedGetCurrentProjectIdCache() + const rid = memoizedGetCurrentProjectId() if (!rid) { // This trap should theoretically never be reached, // because `patches.view` has `"when": "radicle.isRadInitialized"`. @@ -91,37 +103,23 @@ export const patchesTreeDataProvider: TreeDataProvider response.error).filter(Boolean) - const patches = responses.flatMap((response) => response.data).filter(Boolean) - - if (errors.length) { + const patchStore = usePatchStore() + await patchStore.initStoreIfNeeded() + const patches = patchStore.patches + + if (!patches) { setTimeout(() => { - refreshPatchesEventEmitter.fire(undefined) + rerenderAllItemsInPatchesView() }, 3_000 * ++timesPatchListFetchErroredConsecutively) - return errors.length === responses.length - ? ['Please ensure `radicle-httpd` is running and accessible!'] - : ['Not all patches may be listed due to an error!', ...patches] + // TODO: maninak add button linking to see output? + // TODO: maninak add button linking to specific config? + return ['Please ensure `radicle-httpd` is running and accessible!'] } timesPatchListFetchErroredConsecutively = 0 if (!patches.length) { - return [`0 Radicle Patches found`] + return [`0 Radicle Patches`] } const patchesSortedByRevisionTsPerStatus = [ @@ -136,7 +134,8 @@ export const patchesTreeDataProvider: TreeDataProvider ({ - id: `${elem.id} ${oldVersionCommitSha}..${newVersionCommitSha} ${filechange.newPath}`, + id: `${patch.id} ${oldVersionCommitSha}..${newVersionCommitSha} ${filechange.newPath}`, label: filename, description: shouldShowPathInDescription ? Path.dirname(filechange.newPath) @@ -335,6 +334,7 @@ before-the-Patch version and its latest version committed in the Radicle Patch`, ), ) + // TODO: maninak always show path except if in project root hasSameFilenameWithAnotherFile && filechangeNode.enableShowingPathInDescription() return filechangeNode @@ -352,26 +352,9 @@ before-the-Patch version and its latest version committed in the Radicle Patch`, return undefined }, - onDidChangeTreeData: refreshPatchesEventEmitter.event, + onDidChangeTreeData: rerenderPatchesViewEventEmitter.event, } as const -const { - memoizedFunc: memoizedGetCurrentGitBranch, - debouncedClearMemoizedFuncCache: debouncedClearMemoizedGetCurrentGitBranchCache, -} = memoizeWithDebouncedCacheClear(getCurrentGitBranch, 200) - -/** - * Answers whether the associated branch of the provided radicle `patch` is the - * currenctly checked out git branch. - */ -function isPatchCheckedOut(patch: Pick): boolean { - debouncedClearMemoizedGetCurrentGitBranchCache() - const branchName = memoizedGetCurrentGitBranch() - const isCheckedOut = Boolean(branchName?.includes(shortenHash(patch.id))) - - return isCheckedOut -} - function getPatchTreeItemDescription( patch: Patch, { latestRevision }: ReturnType, @@ -460,10 +443,10 @@ function dat(str: string): string { return `${formatingMarker}${str}${formatingMarker}` } -function getPatchesOfStatusSortedByLatestRevisionFirst( - patches: Patch[], - patchStatus: Patch['state']['status'], -): Patch[] { +function getPatchesOfStatusSortedByLatestRevisionFirst

( + patches: P[], + patchStatus: P['state']['status'], +): P[] { const sortedPatches = patches .filter((patch) => patch.state.status === patchStatus) .sort((p1, p2) => { @@ -480,7 +463,7 @@ function getPatchesOfStatusSortedByLatestRevisionFirst( } // eslint-disable-next-line consistent-return -function getThemeIconForPatch(patch: Patch): ThemeIcon { +function getThemeIconForPatch

(patch: P): ThemeIcon { switch (patch.state.status) { case 'draft': return new ThemeIcon('git-pull-request-draft', new ThemeColor('patch.draft')) @@ -495,7 +478,7 @@ function getThemeIconForPatch(patch: Patch): ThemeIcon { } } -function getHtmlIconForPatch(patch: Patch): string { +function getHtmlIconForPatch

(patch: P): string { const icon = getThemeIconForPatch(patch) return `$(${icon.id})` diff --git a/src/ux/radicleIdentityAuth.ts b/src/ux/radicleIdentityAuth.ts index b150a8a4..81fdb265 100644 --- a/src/ux/radicleIdentityAuth.ts +++ b/src/ux/radicleIdentityAuth.ts @@ -1,6 +1,6 @@ import { InputBoxValidationSeverity, window } from 'vscode' import { askUser, exec, log, showLog } from '../utils' -import { getExtensionContext } from '../store' +import { getExtensionContext } from '../stores' import { composeNodeHomePathMsg, getNodeSshKey, diff --git a/src/ux/radiclePatches.ts b/src/ux/radiclePatches.ts deleted file mode 100644 index cfdea610..00000000 --- a/src/ux/radiclePatches.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { window } from 'vscode' -import { getRadCliRef } from '../helpers' -import type { Patch } from '../types' -import { exec, log, shortenHash, showLog } from '../utils' - -export function checkOutPatch(patch: Patch): void { - const didCheckoutPatch = Boolean( - exec(`${getRadCliRef()} patch checkout ${patch.id}`, { - cwd: '$workspaceDir', - shouldLog: true, - }), - ) - if (!didCheckoutPatch) { - const hasUncommitedChanges = - exec('git update-index --refresh && git diff-index --quiet HEAD --', { - cwd: '$workspaceDir', - }) === undefined // https://stackoverflow.com/a/3879077/5015955 - - const button = 'Show output' - const msg = `Failed checking out Patch "${shortenHash(patch.id)}".${ - hasUncommitedChanges ? ' Please stash or commit your changes first.' : '' - }` - window.showErrorMessage(msg, button).then((userSelection) => { - userSelection === button && showLog() - }) - log(msg, 'error') - } - - // TODO: if we ever use a proper reactive global store like pinia, where we store all - // current patches, we should just do a - // patchesRefreshEventEmitter.fire(patchesStore.patches.findById(curPatch)) - // and - // patchesRefreshEventEmitter.fire(patch) - // so that we only update two items in place instead of triggering a fresh - // fetch and parse and render of all patches. - // We should also similarly adjust the fileWatcher of .git/HEAD to only update the - // current branch in the global store and any subscribers of that should react as needed - // (again, not refetching everything every time the HEAD changes!). Also get any current - // callers of getCurrentGitBranch() should just subscribe to the global stored cur branch. -} diff --git a/src/ux/settings.ts b/src/ux/settings.ts index 8b31678b..eb76ffa0 100644 --- a/src/ux/settings.ts +++ b/src/ux/settings.ts @@ -1,5 +1,5 @@ -import type { getConfig } from 'src/helpers' import { commands, workspace } from 'vscode' +import type { getConfig } from '../helpers' /** * Opens the Settings UI with the options filtered to show only the specified config diff --git a/src/webviews/src/components/PatchDetail.vue b/src/webviews/src/components/PatchDetail.vue index ede0730f..f59c4110 100644 --- a/src/webviews/src/components/PatchDetail.vue +++ b/src/webviews/src/components/PatchDetail.vue @@ -1,17 +1,28 @@ @@ -22,10 +33,36 @@ function refetchPatchData() { -