├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── workflows │ ├── ci.yml │ └── codesee-arch-diagram.yml ├── .gitignore ├── .mocharc.yml ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── .yarnrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cspell.json ├── img ├── codemod-icon-mask.svg ├── codemod-square.png ├── codemod-square.svg ├── intuita.svg └── intuita_square128.png ├── intuita-webview ├── .npmrc ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public │ └── index.html ├── src │ ├── CreateIssue │ │ ├── index.tsx │ │ ├── style.module.css │ │ └── tiptap.css │ ├── assets │ │ ├── Split.svg │ │ ├── Unified.svg │ │ ├── arrow-down.svg │ │ ├── case.svg │ │ ├── copy.svg │ │ ├── intuita_square128.png │ │ ├── material-icons │ │ │ ├── check_box.svg │ │ │ ├── check_box_outline_blank.svg │ │ │ └── indeterminate_check_box.svg │ │ ├── slack.svg │ │ └── youtube.svg │ ├── campaignManager │ │ ├── App.tsx │ │ └── style.module.css │ ├── codemodList │ │ ├── App.tsx │ │ ├── CodemodArguments │ │ │ ├── FormField │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.css │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── CodemodNodeRenderer │ │ │ ├── Codemod.tsx │ │ │ ├── Directory.tsx │ │ │ ├── index.tsx │ │ │ └── style.module.css │ │ ├── TreeView │ │ │ ├── ActionButton.tsx │ │ │ ├── InfiniteProgress.module.css │ │ │ ├── InfiniteProgress.tsx │ │ │ ├── ProgressBar.tsx │ │ │ ├── index.tsx │ │ │ └── style.module.css │ │ ├── components │ │ │ ├── DirectorySelector.tsx │ │ │ └── style.module.css │ │ ├── style.module.css │ │ └── useProgressBar.tsx │ ├── communityTab │ │ ├── CommunityTab.tsx │ │ └── style.module.css │ ├── errors │ │ ├── App.tsx │ │ ├── index.html │ │ ├── index.tsx │ │ └── style.module.css │ ├── fileExplorer │ │ ├── ActionsFooter │ │ │ ├── index.tsx │ │ │ └── style.module.css │ │ ├── App.tsx │ │ ├── FileExplorerTreeNode.tsx │ │ ├── explorerNodeRenderer.tsx │ │ └── style.module.css │ ├── intuitaTreeView.tsx │ ├── jobDiffView │ │ ├── App.tsx │ │ ├── Components │ │ │ ├── Collapsable.css │ │ │ ├── Collapsable.tsx │ │ │ └── LoadingProgress │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.css │ │ ├── DiffViewer │ │ │ ├── Container.css │ │ │ ├── Container.tsx │ │ │ ├── Diff.tsx │ │ │ ├── DiffItem.css │ │ │ ├── DiffItem.tsx │ │ │ ├── Header │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.css │ │ │ ├── configure.ts │ │ │ └── index.tsx │ │ ├── hooks │ │ │ ├── useElementSize.ts │ │ │ └── useKey.ts │ │ ├── index.css │ │ ├── index.html │ │ ├── index.tsx │ │ ├── style.module.css │ │ └── util.ts │ ├── main │ │ ├── App.css │ │ ├── App.tsx │ │ ├── CodemodRuns.tsx │ │ ├── index.html │ │ └── index.tsx │ ├── react-app-env.d.ts │ └── shared │ │ ├── IntuitaPopover │ │ └── index.tsx │ │ ├── Panel.tsx │ │ ├── Progress │ │ ├── index.tsx │ │ └── style.module.css │ │ ├── SearchBar │ │ ├── index.tsx │ │ └── style.module.css │ │ ├── SectionHeader │ │ ├── index.tsx │ │ └── style.module.css │ │ ├── Snippet │ │ ├── calculateDiff.tsx │ │ ├── detectTheme.ts │ │ └── useTheme.ts │ │ ├── TreeItem │ │ ├── index.tsx │ │ └── style.module.css │ │ ├── WarningMessage │ │ ├── index.tsx │ │ └── style.module.css │ │ ├── constants.ts │ │ ├── index.css │ │ ├── types.ts │ │ ├── util.css │ │ └── utilities │ │ ├── debounce.ts │ │ ├── throttle.ts │ │ └── vscode.ts ├── tsconfig.json ├── vite.config.dev.ts └── vite.config.ts ├── package.json ├── pnpm-lock.yaml ├── resources ├── codicon.css └── codicon.ttf ├── src ├── axios │ └── index.ts ├── cases │ ├── caseManager.ts │ └── types.ts ├── codemods │ └── types.ts ├── commands │ └── clearStateCommand.ts ├── components │ ├── bootstrapExecutablesService.ts │ ├── buildArguments.ts │ ├── downloadService.ts │ ├── engineService.ts │ ├── fileService.ts │ ├── fileSystemUtilities.ts │ ├── jobManager.ts │ ├── messageBus.ts │ ├── textDocumentContentProvider.ts │ ├── userService.ts │ └── webview │ │ ├── CodemodDescriptionProvider.ts │ │ ├── ErrorWebviewProvider.ts │ │ ├── IntuitaPanelProvider.ts │ │ ├── MainProvider.ts │ │ ├── WebviewResolver.ts │ │ ├── panelViewProps.ts │ │ └── webviewEvents.ts ├── configuration.ts ├── container.ts ├── data │ ├── codemodConfigSchema.ts │ ├── index.ts │ ├── privateCodemodsEnvelopeSchema.ts │ ├── readHomeDirectoryCases.ts │ ├── schemata │ │ └── argumentRecordSchema.ts │ ├── slice.ts │ ├── storage.ts │ └── urlParamsEnvelopeSchema.ts ├── errors │ └── types.ts ├── extension.ts ├── github │ └── types.ts ├── jobs │ ├── acceptJobs.ts │ ├── buildJobHash.ts │ └── types.ts ├── leftRightHashes │ └── leftRightHashSetManager.ts ├── packageJsonAnalyzer │ └── types.ts ├── persistedState │ ├── codecs.ts │ └── explorerNodeCodec.ts ├── selectors │ ├── comparePersistedJobs.ts │ ├── selectCodemodRunsTree.ts │ ├── selectCodemodTree.ts │ ├── selectErrorWebviewViewProps.ts │ ├── selectExplorerTree.ts │ ├── selectMainWebviewViewProps.ts │ └── selectSourceControlTabProps.ts ├── telemetry │ ├── hashes.ts │ ├── telemetry.ts │ └── vscodeTelemetry.ts ├── types │ ├── reset.d.ts │ └── vscode.d.ts ├── uris │ ├── buildUriHash.ts │ └── types.ts └── utilities.ts ├── test └── dowloadService.test.ts ├── tsconfig.json └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "prettier" 13 | ], 14 | "plugins": ["@typescript-eslint"], 15 | "ignorePatterns": ["out", "dist", "**/*.d.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[bug] example bug in extension" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | > :warning: Use this to report bugs related to the Intuita VS Code extension, not codemod-specific issues and improvements. For codemod-specific issues, please [submit an issue in the codemod registry](https://github.com/codemod-com/codemod-registry/issues). 11 | 12 | ### Describe the bug 13 | A clear and concise description of what the bug is. 14 | 15 | ### Steps to Reproduce 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | ### Screenshots 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | ### Actual behavior 26 | A clear and concise description of what actually happened. 27 | 28 | ### Expected behavior 29 | A clear and concise description of what you expected to happen. 30 | 31 | ### Environment: 32 | - Intuita extension version: [e.g. v0.36.5] 33 | - OS: [e.g. MacOS 13.0.1] 34 | - Node.js version: [e.g. v16.16.0] 35 | 36 | ### Additional context 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Questions 3 | url: https://intuita.io/community 4 | about: Ask us any questions on our Slack community. 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a feature you want 4 | title: "[feat] example feature request" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Is your feature request related to a problem? Please describe. 11 | A clear and concise description of what the problem is. [e.g. I get frustrated when...] 12 | 13 | ### Describe the solution you'd like 14 | A clear and concise description of what you want to happen. [e.g. I would love to see...] 15 | 16 | ### Describe alternatives you've considered 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | ### Additional context 20 | Add any other context or screenshots about the feature request here. 21 | 22 | ### Documents 23 | Add any other documents that might help describe the feature you want. [e.g. prototypes, design illustrations, POCs, etc.] 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | name: CI 4 | 5 | jobs: 6 | SpellCheck: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/setup-node@v3 10 | with: 11 | node-version: 16 12 | - uses: actions/checkout@v2 13 | - name: Install modules 14 | uses: pnpm/action-setup@v2 15 | with: 16 | version: 8 17 | - name: Install dependencies 18 | run: pnpm install 19 | - name: Check spelling 20 | run: pnpm spellcheck 21 | Build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/setup-node@v3 25 | with: 26 | node-version: 16 27 | - uses: actions/checkout@v2 28 | - name: Install modules 29 | uses: pnpm/action-setup@v2 30 | with: 31 | version: 8 32 | - name: Install vsce 33 | run: pnpm add -g @vscode/vsce 34 | - name: Install dependencies 35 | run: pnpm install 36 | - name: Package using webpack 37 | run: pnpm run build 38 | Lint: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/setup-node@v3 42 | with: 43 | node-version: 16 44 | - uses: actions/checkout@v2 45 | - name: Install modules 46 | uses: pnpm/action-setup@v2 47 | with: 48 | version: 8 49 | - name: Install dependencies 50 | run: pnpm install 51 | - name: Run ESLint 52 | run: pnpm eslint src --ext ts 53 | Prettier: 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/setup-node@v3 57 | with: 58 | node-version: 16 59 | - uses: actions/checkout@v2 60 | - name: Install modules 61 | uses: pnpm/action-setup@v2 62 | with: 63 | version: 8 64 | - name: Install dependencies 65 | run: pnpm install 66 | - name: Ensure Prettier was run 67 | run: pnpm prettier --check . 68 | -------------------------------------------------------------------------------- /.github/workflows/codesee-arch-diagram.yml: -------------------------------------------------------------------------------- 1 | # This workflow was added by CodeSee. Learn more at https://codesee.io/ 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request_target: 7 | types: [opened, synchronize, reopened] 8 | 9 | name: CodeSee 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | codesee: 15 | runs-on: ubuntu-latest 16 | continue-on-error: true 17 | name: Analyze the repo with CodeSee 18 | steps: 19 | - uses: Codesee-io/codesee-action@v2 20 | with: 21 | codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | .idea 7 | *.js 8 | yarn-error.log 9 | .DS_Store 10 | 11 | build 12 | 13 | .trunk -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - 'ts-node/register' 3 | - 'source-map-support/register' 4 | full-trace: true 5 | bail: true 6 | spec: './test/**/*.test.ts' 7 | timeout: 10000 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | img 3 | resources 4 | *.md 5 | pnpm-lock.yaml 6 | yarn.lock 7 | intuita-webview/build 8 | 9 | .trunk -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": true, 4 | "semi": true, 5 | "singleQuote": true, 6 | "quoteProps": "as-needed", 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "arrowParens": "always", 10 | "endOfLine": "lf" 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "amodio.tsl-problem-matcher", 7 | "streetsidesoftware.code-spell-checker" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 13 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 14 | "preLaunchTask": "${defaultBuildTask}" 15 | }, 16 | { 17 | "name": "Extension Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "args": [ 21 | "--extensionDevelopmentPath=${workspaceFolder}", 22 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 23 | ], 24 | "outFiles": [ 25 | "${workspaceFolder}/out/**/*.js", 26 | "${workspaceFolder}/dist/**/*.js" 27 | ], 28 | "preLaunchTask": "tasks: watch-tests" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off", 13 | "typescript.tsdk": "node_modules/typescript/lib" 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": ["$ts-webpack-watch", "$tslint-webpack-watch"], 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "group": "watchers" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch-tests", 23 | "problemMatcher": "$tsc-watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never", 27 | "group": "watchers" 28 | }, 29 | "group": "build" 30 | }, 31 | { 32 | "label": "tasks: watch-tests", 33 | "dependsOn": ["npm: watch", "npm: watch-tests"], 34 | "problemMatcher": [] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | test/** 7 | .gitignore 8 | .yarnrc 9 | webpack.config.js 10 | vsc-extension-quickstart.md 11 | **/tsconfig.json 12 | **/.eslintrc.json 13 | **/*.map 14 | **/*.ts 15 | .idea 16 | .github 17 | 18 | intuita-webview/node_modules/** -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-engines true -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This page lists highlights, releases, bug fixes, and progress for official releases of Intuita VSCode Extension. 4 | 5 | > **Tip:** If there are features you would like to see in the upcoming releases, please let us know though our [feedback page](https://feedback.intuita.io), or directly through our [Slack community](https://intuita.io/community). 6 | 7 | ## Version 0.37.0 8 | 9 | _Release date: December 1, 2023_ 10 | 11 | ### **Highlights** 12 | 13 | - 🔲 Fetch the latest codemods from codemod registry every 15 minutes ([#831](https://github.com/codemod-com/intuita-vscode-extension/pull/831)) 14 | - 🐍 Another item 15 | 16 | **Notable Changes** 17 | 18 | - ⌛ Some time saving highlight ([#1234](https://github.com), [#1234](https://github.com)). 19 | - 🏃 Some performance improvement ([#1234](https://github.com), [#1234](https://github.com)). 20 | - 🛁 Some code cleanup ([#1234](https://github.com), [#1234](https://github.com)). 21 | - 💅 Some visual improvement ([#1234](https://github.com), [#1234](https://github.com)). 22 | 23 | **Bug fixes & other changes** 24 | 25 | - 🦗 Bug fix: Description of bug fix ([#1234](https://github.com/)). Thanks, [Community Member](https://github.com)! 26 | - 🦎 Bug fix: Description of another bug fix ([#1234](https://github.com/), [#1234](https://github.com/), [#1234](https://github.com/)). Thanks, [Community Member](https://github.com)! 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **⚠️ This repository is deprecated and no longer maintained.** 2 | 3 | **Please Note:** This repository was migrated to a monorepo called [codemod](https://github.com/codemod-com/codemod/tree/main/apps/vsce). 4 | 5 | # A better way to discover & run community-led codemods 6 | 7 | Upgrade your projects with the help of codemods created by experts in the community, one framework at a time, starting with Next.js v13.4. 8 | 9 | > 🎁 What frameworks should we support next? [Let us know →](https://feedback.intuita.io/codemod-requests) 10 | 11 | ## 1. Discover 12 | 13 | - Intuita is **a one-stop shop for discovering & sharing quality-governed codemods.** You don't need to install and run many codemod engines for each dependency. Simply search for your framework codemods and click run. With codemod deep links, you can reach & run your target codemod with just one click. 14 | 15 | ![Discover Codemods](https://github.com/codemod-com/intuita-docs/raw/main/static/img/vsce/vsce-discover.gif) 16 | 17 | ## 2. Run 18 | 19 | - **Safely dry-run the codemods**, preview the changes with a user-friendly experience, adjust the changes as necessary, and apply them to your code only when you feel confident. 20 | 21 | ![Running Codemods](https://github.com/codemod-com/intuita-docs/raw/main/static/img/vsce/vsce-run.gif) 22 | 23 | ## 3. Customize & Improve 24 | 25 | - Leverage the **1-click integration with Codemod Studio** and the feedback loop with the **community of Codemod Champions** to continuously improve Codemods and customize them to your needs. 26 | 27 | > 💡 Intuita is in Public Beta and we’re continuously working on improving codemods and solving any compatibility issues. 28 | If you run into an issue while running a codemod, please [let us know →](https://feedback.intuita.io/feature-requests-and-bugs) 29 | 30 | ## Other Features 31 | 32 | - **Out-of-the-box Prettier Integration -** Your favorite code transformation engines such as Meta’s JSCodeshift or TS-morph will mess up the formatting. Intuita will automatically prettify the changes according to your settings, saving you much time and energy for more exciting features. 33 | - **Multi-threading -** Execute codemods faster than you would with vanilla jscodeshift or ts-morph. Intuita's engine uses multi-threading, which is customizable via extension settings, to take full advantage of your machine's computing power and expedite large-scale changes. 34 | - For advanced settings & features, visit the [Intuita docs.](https://docs.intuita.io/docs/vs-code-extension/quickstart) 35 | 36 | # Extension vs. the Platform 37 | 38 | ![Intuita Platform Architecture](https://github.com/codemod-com/intuita-docs/raw/main/static/img/docs/intuita-platform-architecture.png) 39 | 40 | - **To learn more about codemod.studio, registry, and the CLI visit the [Intuita docs here](https://docs.intuita.io/docs/intro).** 41 | 42 | 43 | ## Telemetry 🔭 44 | 45 | - The extension collects telemetry data to help us improve the product for you. 46 | - **We never send PII, OS information, file, or folder names.** 47 | - Telemetry can be disabled in the settings. 48 | - See more details in our [telemetry compliance considerations](https://docs.intuita.io/docs/about-intuita/legal/telemetry-compliance) doc. 49 | 50 | ## Share Feedback 🎁 51 | 52 | - Please share your ideas, questions, and feature requests **[here](https://feedback.intuita.io/)**, or chat with us in [Slack](https://join.slack.com/t/codemod-com/shared_invite/zt-1tvxm6ct0-mLZld_78yguDYOSM7DM7Cw). 53 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePaths": ["**/*.d.ts"], 3 | "dictionaries": [ 4 | "en_US", 5 | "typescript", 6 | "html", 7 | "companies", 8 | "softwareTerms" 9 | ], 10 | "language": "en", 11 | "useGitignore": true, 12 | "words": [ 13 | "Caartaa", 14 | "campaigns", 15 | "codebases", 16 | "codemod", 17 | "codemods", 18 | "coderepair", 19 | "gppd", 20 | "intellicode", 21 | "intuita", 22 | "kase", 23 | "newtype", 24 | "nextjs", 25 | "quickstart", 26 | "pluggable", 27 | "refactorings", 28 | "routerv", 29 | "ripemd", 30 | "roadmap", 31 | "tabnine", 32 | "taskifies", 33 | "Upvote", 34 | "userdata", 35 | "vsix", 36 | "yarnrc", 37 | "cutcopypaste", 38 | "codicons", 39 | "codicon", 40 | "upsert", 41 | "jscodeshift", 42 | "Codeshift", 43 | "unapply", 44 | "redwoodjs", 45 | "repomod", 46 | "hashless", 47 | "interfase", 48 | "Quotify", 49 | "Descendents", 50 | "overscan", 51 | "reactjs", 52 | "Collapsable", 53 | "bpfrpt", 54 | "proptype", 55 | "svgr", 56 | "preact", 57 | "filemod", 58 | "QKEdp-pofR9UnglrKAGDm1Oj6W0", 59 | "Disactivated", 60 | "immutablejsv", 61 | "reactrouterv", 62 | "reduxjs", 63 | "persistor", 64 | "webviews", 65 | "activeid", 66 | "tippyjs", 67 | "fuzzysort", 68 | "redeclared", 69 | "monacoeditorwork", 70 | "whitespace", 71 | "tiptap", 72 | "lowlight", 73 | "lowlights", 74 | "unsanitize", 75 | "toastify", 76 | "Toastify", 77 | "POSTAMBLE", 78 | "INTC", 79 | "INTJ", 80 | "INTE", 81 | "Crossplatform", 82 | "nodir" 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /img/codemod-icon-mask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /img/codemod-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codemod-com/intuita-vscode-extension/cc9b2e9fc70e894b3e92dcd48a980476dca1652d/img/codemod-square.png -------------------------------------------------------------------------------- /img/codemod-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /img/intuita.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /img/intuita_square128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codemod-com/intuita-vscode-extension/cc9b2e9fc70e894b3e92dcd48a980476dca1652d/img/intuita_square128.png -------------------------------------------------------------------------------- /intuita-webview/.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | -------------------------------------------------------------------------------- /intuita-webview/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Intuita 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /intuita-webview/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "intuita-webview", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@monaco-editor/react": "^4.5.0", 7 | "@tippyjs/react": "^4.2.6", 8 | "@tiptap/extension-code-block-lowlight": "^2.1.8", 9 | "@tiptap/pm": "^2.1.8", 10 | "@tiptap/react": "^2.1.8", 11 | "@tiptap/starter-kit": "^2.1.8", 12 | "@vscode/webview-ui-toolkit": "^1.2.2", 13 | "classnames": "^2.3.2", 14 | "fp-ts": "^2.14.0", 15 | "lowlight": "^3.0.0", 16 | "monaco-editor": "^0.37.1", 17 | "preact": "^10.13.2", 18 | "preact-compat": "^3.19.0", 19 | "rc-progress": "^3.4.1", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "react-markdown": "^8.0.7", 23 | "react-resizable-panels": "^0.0.51", 24 | "react-toastify": "^9.1.3", 25 | "tippy.js": "^6.3.7", 26 | "typescript": "^4.9.5" 27 | }, 28 | "scripts": { 29 | "start": "NODE_ENV='development' vite -c 'vite.config.dev.ts'", 30 | "build:main": "NODE_ENV='production' TARGET_APP='main' vite build", 31 | "build:jobDiffView": "NODE_ENV='production' TARGET_APP='jobDiffView' vite build", 32 | "build:errors": "NODE_ENV='production' TARGET_APP='errors' vite build", 33 | "build": "pnpm build:main && pnpm build:jobDiffView && pnpm build:errors" 34 | }, 35 | "eslintConfig": { 36 | "extends": [ 37 | "react-app", 38 | "react-app/jest" 39 | ] 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.2%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version" 51 | ] 52 | }, 53 | "devDependencies": { 54 | "@preact/preset-vite": "^2.5.0", 55 | "@testing-library/jest-dom": "^5.16.5", 56 | "@testing-library/react": "^13.4.0", 57 | "@testing-library/user-event": "^13.5.0", 58 | "@types/jest": "^27.5.2", 59 | "@types/node": "^16.18.21", 60 | "@types/react": "^18.0.30", 61 | "@types/react-dom": "^18.0.11", 62 | "@types/react-treeview": "^0.4.3", 63 | "@types/react-virtualized": "^9.21.21", 64 | "@types/testing-library__jest-dom": "^5.14.5", 65 | "@types/vscode-webview": "^1.57.1", 66 | "@vitejs/plugin-react": "^4.0.0", 67 | "react-scripts": "5.0.1", 68 | "vite": "^4.3.1", 69 | "vite-plugin-monaco-editor": "^1.1.0", 70 | "vite-plugin-svgr": "^2.4.0", 71 | "vite-tsconfig-paths": "^4.2.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /intuita-webview/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Intuita 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /intuita-webview/src/CreateIssue/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEditor, EditorContent } from '@tiptap/react'; 2 | import StarterKit from '@tiptap/starter-kit'; 3 | import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'; 4 | import { common, createLowlight } from 'lowlight'; 5 | import { 6 | VSCodeButton, 7 | VSCodeProgressRing, 8 | VSCodeTextField, 9 | } from '@vscode/webview-ui-toolkit/react'; 10 | import { useEffect, useState } from 'react'; 11 | import { vscode } from '../shared/utilities/vscode'; 12 | import styles from './style.module.css'; 13 | import './tiptap.css'; 14 | 15 | const lowlight = createLowlight(common); 16 | const unsanitizeCodeBlock = (codeBlock: string) => 17 | codeBlock.replace(/</g, '<').replace(/>/g, '>'); 18 | 19 | const convertHTMLCodeBlockToMarkdownString = (htmlSnippet: string) => 20 | htmlSnippet.replace( 21 | /
(.*?)<\/code><\/pre>/gs,
 22 | 		(_match, language, content) =>
 23 | 			`\n\n\`\`\`${language ?? 'typescript'}\n${unsanitizeCodeBlock(
 24 | 				content,
 25 | 			)}\n\`\`\`\n\n`,
 26 | 	);
 27 | 
 28 | type Props = Readonly<{
 29 | 	title: string;
 30 | 	body: string;
 31 | 	loading: boolean;
 32 | }>;
 33 | 
 34 | const CreateIssue = (props: Props) => {
 35 | 	const [title, setTitle] = useState('');
 36 | 
 37 | 	const onChangeTitle = (e: Event | React.FormEvent) => {
 38 | 		const value =
 39 | 			'target' in e && e.target !== null && 'value' in e.target
 40 | 				? String(e.target.value)
 41 | 				: null;
 42 | 
 43 | 		if (value === null) {
 44 | 			return;
 45 | 		}
 46 | 
 47 | 		setTitle(value);
 48 | 	};
 49 | 	const handleSubmit = (e: React.FormEvent) => {
 50 | 		e.preventDefault();
 51 | 
 52 | 		if (editor === null) {
 53 | 			return;
 54 | 		}
 55 | 
 56 | 		const htmlSnippet = editor.getHTML();
 57 | 
 58 | 		vscode.postMessage({
 59 | 			kind: 'webview.sourceControl.createIssue',
 60 | 			data: {
 61 | 				title,
 62 | 				body: convertHTMLCodeBlockToMarkdownString(htmlSnippet),
 63 | 			},
 64 | 		});
 65 | 	};
 66 | 
 67 | 	const extensions = [
 68 | 		StarterKit.configure({
 69 | 			codeBlock: false,
 70 | 		}),
 71 | 		CodeBlockLowlight.configure({
 72 | 			lowlight,
 73 | 			// in our editor, we provide 1 syntax highlighting for all languages;
 74 | 			// once transmitted to Github, the syntax highlighting will be handled there, and therefore
 75 | 			// different syntax highlighting will be applied to different languages there;
 76 | 			defaultLanguage: 'typescript',
 77 | 		}),
 78 | 	];
 79 | 
 80 | 	const editor = useEditor({
 81 | 		extensions,
 82 | 		content: props.body,
 83 | 		editable: true,
 84 | 	});
 85 | 
 86 | 	useEffect(() => {
 87 | 		setTitle(props.title);
 88 | 	}, [props.title]);
 89 | 
 90 | 	useEffect(() => {
 91 | 		if (props.loading) {
 92 | 			return;
 93 | 		}
 94 | 		editor?.commands.setContent(props.body, false, {
 95 | 			preserveWhitespace: true,
 96 | 		});
 97 | 	}, [editor, props.body, props.loading]);
 98 | 
 99 | 	return (
100 | 		
101 |

Report codemod issue

102 |
103 | 109 | Title 110 | 111 | 112 | 113 | 114 |
115 | 124 | {props.loading ? ( 125 |
126 | 129 | Creating... 130 |
131 | ) : ( 132 | 'Create Issue' 133 | )} 134 |
135 |
136 | 137 |
138 | ); 139 | }; 140 | 141 | export default CreateIssue; 142 | -------------------------------------------------------------------------------- /intuita-webview/src/CreateIssue/style.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100%; 3 | padding: 0 0.5rem; 4 | } 5 | 6 | .form { 7 | display: flex; 8 | flex-direction: column; 9 | row-gap: 8px; 10 | padding: 4px; 11 | } 12 | 13 | .actionButton { 14 | height: 24px; 15 | width: 160px; 16 | } 17 | 18 | .formField { 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | 23 | .actions { 24 | display: flex; 25 | column-gap: 8px; 26 | justify-content: flex-end; 27 | } 28 | 29 | .title::part(label) { 30 | margin-bottom: 0.5rem; 31 | cursor: default; 32 | } 33 | 34 | .body::part(label) { 35 | margin-bottom: 0.5rem; 36 | } 37 | 38 | .label { 39 | font-size: var(--type-ramp-base-font-size); 40 | line-height: var(--type-ramp-base-line-height); 41 | display: block; 42 | color: var(--foreground); 43 | user-select: none; 44 | } 45 | 46 | .loadingContainer { 47 | display: flex; 48 | align-items: center; 49 | } 50 | 51 | .progressRing { 52 | width: 16px; 53 | height: 16px; 54 | margin-right: 12px; 55 | } 56 | 57 | .progressRing::part(indeterminate-indicator-1) { 58 | stroke: var(--vscode-icon-foreground); 59 | } 60 | -------------------------------------------------------------------------------- /intuita-webview/src/CreateIssue/tiptap.css: -------------------------------------------------------------------------------- 1 | .tiptap { 2 | background: var(--input-background); 3 | outline: none; 4 | height: 70vh; 5 | overflow: auto; 6 | padding: 1px calc(var(--design-unit) * 2px + 1px); 7 | } 8 | 9 | .tiptap ul, 10 | .tiptap ol { 11 | padding: 0 1rem; 12 | } 13 | 14 | .tiptap h1, 15 | .tiptap h2, 16 | .tiptap h3, 17 | .tiptap h4, 18 | .tiptap h5, 19 | .tiptap h6 { 20 | line-height: 1.1; 21 | } 22 | 23 | .tiptap code { 24 | background-color: transparent; 25 | } 26 | 27 | .tiptap pre { 28 | background: #0d0d0d; 29 | color: #fff; 30 | font-family: 'JetBrainsMono', monospace; 31 | padding: 0.75rem 1rem; 32 | border-radius: 0.5rem; 33 | } 34 | 35 | .tiptap pre code { 36 | color: inherit; 37 | padding: 0; 38 | background: none; 39 | font-size: 0.8rem; 40 | } 41 | 42 | .tiptap img { 43 | max-width: 100%; 44 | height: auto; 45 | } 46 | 47 | .tiptap blockquote { 48 | padding-left: 1rem; 49 | border-left: 2px solid white; 50 | margin-inline-start: 0px; 51 | margin-inline-end: 0px; 52 | background: transparent; 53 | } 54 | 55 | .tiptap hr { 56 | border: none; 57 | border-top: 2px solid white; 58 | margin: 2rem 0; 59 | } 60 | 61 | /* 62 | - styling for syntax highlights; we don't support different syntax highlights for different languages 63 | - copied from .scss file of demo in https://tiptap.dev/api/nodes/code-block-lowlight#usage 64 | */ 65 | 66 | .tiptap .hljs-comment, 67 | .tiptap .hljs-quote { 68 | color: #616161; 69 | } 70 | 71 | .tiptap .hljs-variable, 72 | .tiptap .hljs-template-variable, 73 | .tiptap .hljs-attribute, 74 | .tiptap .hljs-tag, 75 | .tiptap .hljs-name, 76 | .tiptap .hljs-regexp, 77 | .tiptap .hljs-link, 78 | .tiptap .hljs-name, 79 | .tiptap .hljs-selector-id, 80 | .tiptap .hljs-selector-class { 81 | color: #f98181; 82 | } 83 | 84 | .tiptap .hljs-number, 85 | .tiptap .hljs-meta, 86 | .tiptap .hljs-built_in, 87 | .tiptap .hljs-builtin-name, 88 | .tiptap .hljs-literal, 89 | .tiptap .hljs-type, 90 | .tiptap .hljs-params { 91 | color: #fbbc88; 92 | } 93 | 94 | .tiptap .hljs-string, 95 | .tiptap .hljs-symbol, 96 | .tiptap .hljs-bullet { 97 | color: #b9f18d; 98 | } 99 | 100 | .tiptap .hljs-title, 101 | .tiptap .hljs-section { 102 | color: #faf594; 103 | } 104 | 105 | .tiptap .hljs-keyword, 106 | .tiptap .hljs-selector-tag { 107 | color: #70cff8; 108 | } 109 | 110 | .tiptap .hljs-emphasis { 111 | font-style: italic; 112 | } 113 | 114 | .tiptap .hljs-strong { 115 | font-weight: 700; 116 | } 117 | -------------------------------------------------------------------------------- /intuita-webview/src/assets/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /intuita-webview/src/assets/case.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /intuita-webview/src/assets/intuita_square128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codemod-com/intuita-vscode-extension/cc9b2e9fc70e894b3e92dcd48a980476dca1652d/intuita-webview/src/assets/intuita_square128.png -------------------------------------------------------------------------------- /intuita-webview/src/assets/material-icons/check_box.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /intuita-webview/src/assets/material-icons/check_box_outline_blank.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /intuita-webview/src/assets/material-icons/indeterminate_check_box.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /intuita-webview/src/assets/slack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /intuita-webview/src/campaignManager/App.tsx: -------------------------------------------------------------------------------- 1 | import { vscode } from '../shared/utilities/vscode'; 2 | import styles from './style.module.css'; 3 | import '../shared/util.css'; 4 | import { IntuitaTreeView } from '../intuitaTreeView'; 5 | import { CaseHash } from '../../../src/cases/types'; 6 | import { CodemodRunsTree } from '../../../src/selectors/selectCodemodRunsTree'; 7 | import { ReactComponent as CaseIcon } from '../assets/case.svg'; 8 | import TreeItem from '../shared/TreeItem'; 9 | import { MainWebviewViewProps } from '../../../src/selectors/selectMainWebviewViewProps'; 10 | import IntuitaPopover from '../shared/IntuitaPopover'; 11 | import cn from 'classnames'; 12 | import LoadingProgress from '../jobDiffView/Components/LoadingProgress'; 13 | 14 | type InfoIconProps = { 15 | createdAt: number; 16 | path: string; 17 | }; 18 | 19 | const InfoIcon = ({ createdAt, path }: InfoIconProps) => { 20 | return ( 21 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export const App = ( 32 | props: MainWebviewViewProps & { activeTabId: 'codemodRuns' }, 33 | ) => { 34 | if (props.codemodRunsTree === null) { 35 | // no workspace is chosen 36 | return ( 37 |

38 | No change to review! Run some codemods via Codemod Discovery or 39 | VS Code Command & check back later! 40 |

41 | ); 42 | } 43 | 44 | if (props.codemodRunsTree.nodeData.length === 0) { 45 | return props.codemodExecutionInProgress ? ( 46 | // `nodeData.length` can be zero momentarily even if a codemod is actually in progress 47 | 48 | ) : ( 49 |

50 | No change to review! Run some codemods via Codemod Discovery or 51 | VS Code Command & check back later! 52 |

53 | ); 54 | } 55 | 56 | return ( 57 | 58 | focusedNodeHashDigest={props.codemodRunsTree.selectedNodeHashDigest} 59 | collapsedNodeHashDigests={[]} 60 | nodeData={props.codemodRunsTree.nodeData} 61 | nodeRenderer={(props) => { 62 | return ( 63 | } 70 | depth={props.nodeDatum.depth} 71 | indent={props.nodeDatum.depth * 18} 72 | open={false} 73 | focused={props.nodeDatum.focused} 74 | onClick={(event) => { 75 | event.stopPropagation(); 76 | 77 | props.onFocus(props.nodeDatum.node.hashDigest); 78 | }} 79 | endDecorator={ 80 | 84 | } 85 | inlineStyles={{ 86 | root: { 87 | paddingRight: '3px', 88 | }, 89 | }} 90 | /> 91 | ); 92 | }} 93 | onFlip={() => {}} 94 | onFocus={function (hashDigest: CaseHash): void { 95 | vscode.postMessage({ 96 | kind: 'webview.campaignManager.setSelectedCaseHash', 97 | caseHash: hashDigest, 98 | }); 99 | }} 100 | /> 101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /intuita-webview/src/campaignManager/style.module.css: -------------------------------------------------------------------------------- 1 | .welcomeMessage { 2 | padding: 0 20px 1em; 3 | margin-top: 13px; 4 | } 5 | -------------------------------------------------------------------------------- /intuita-webview/src/codemodList/CodemodArguments/FormField/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | VSCodeCheckbox, 3 | VSCodeDropdown, 4 | VSCodeOption, 5 | VSCodeTextField, 6 | } from '@vscode/webview-ui-toolkit/react'; 7 | import styles from './style.module.css'; 8 | import { CodemodArgumentWithValue } from '../../../../../src/selectors/selectCodemodTree'; 9 | 10 | type Props = CodemodArgumentWithValue & { 11 | onChange(value: string): void; 12 | }; 13 | 14 | const FormField = (props: Props) => { 15 | const { name, kind, value, description, required, onChange } = props; 16 | if (kind === 'string' || kind === 'number') { 17 | return ( 18 | 23 | onChange( 24 | (e as React.ChangeEvent).target.value, 25 | ) 26 | } 27 | className={styles.field} 28 | title={description} 29 | > 30 | {name} {required && '(required)'} 31 | 32 | ); 33 | } 34 | 35 | if (kind === 'options') { 36 | return ( 37 | 39 | onChange( 40 | (e as React.ChangeEvent).target.value, 41 | ) 42 | } 43 | value={value} 44 | > 45 | {props.options.map((o) => ( 46 | {o} 47 | ))} 48 | 49 | ); 50 | } 51 | 52 | return ( 53 |
54 | 57 | { 61 | onChange( 62 | String( 63 | (e as React.ChangeEvent).target 64 | .checked, 65 | ), 66 | ); 67 | }} 68 | /> 69 |
70 | ); 71 | }; 72 | 73 | export default FormField; 74 | -------------------------------------------------------------------------------- /intuita-webview/src/codemodList/CodemodArguments/FormField/style.module.css: -------------------------------------------------------------------------------- 1 | .field { 2 | width: 100%; 3 | } 4 | 5 | .fieldLayout { 6 | display: flex; 7 | flex-direction: column; 8 | gap: 8px; 9 | } 10 | 11 | .label { 12 | font-size: var(--type-ramp-base-font-size); 13 | line-height: var(--type-ramp-base-line-height); 14 | display: block; 15 | color: var(--foreground); 16 | user-select: none; 17 | } 18 | -------------------------------------------------------------------------------- /intuita-webview/src/codemodList/CodemodArguments/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | CodemodArgumentWithValue, 3 | CodemodNodeHashDigest, 4 | } from '../../../../src/selectors/selectCodemodTree'; 5 | import { CodemodHash } from '../../shared/types'; 6 | import { vscode } from '../../shared/utilities/vscode'; 7 | 8 | import FormField from './FormField'; 9 | import styles from './styles.module.css'; 10 | 11 | import { DirectorySelector } from '../components/DirectorySelector'; 12 | 13 | import { pipe } from 'fp-ts/lib/function'; 14 | import * as T from 'fp-ts/These'; 15 | import * as O from 'fp-ts/Option'; 16 | 17 | type Props = Readonly<{ 18 | hashDigest: CodemodNodeHashDigest; 19 | arguments: ReadonlyArray; 20 | autocompleteItems: ReadonlyArray; 21 | executionPath: T.These<{ message: string }, string>; 22 | }>; 23 | 24 | const updatePath = (value: string, codemodHash: CodemodHash) => { 25 | vscode.postMessage({ 26 | kind: 'webview.codemodList.updatePathToExecute', 27 | value: { 28 | newPath: value, 29 | codemodHash, 30 | errorMessage: '', 31 | warningMessage: '', 32 | revertToPrevExecutionIfInvalid: false, 33 | }, 34 | }); 35 | }; 36 | 37 | const CodemodArguments = ({ 38 | hashDigest, 39 | arguments: args, 40 | autocompleteItems, 41 | executionPath, 42 | }: Props) => { 43 | const onChangeFormField = (fieldName: string) => (value: string) => { 44 | vscode.postMessage({ 45 | kind: 'webview.global.setCodemodArgument', 46 | hashDigest, 47 | name: fieldName, 48 | value, 49 | }); 50 | }; 51 | 52 | const path: string = pipe( 53 | O.fromNullable(executionPath), 54 | O.fold( 55 | () => '', 56 | T.fold( 57 | () => '', 58 | (p) => p, 59 | (_, p) => p, 60 | ), 61 | ), 62 | ); 63 | 64 | return ( 65 |
66 |
67 | 70 | updatePath(value, hashDigest as unknown as CodemodHash) 71 | } 72 | autocompleteItems={autocompleteItems} 73 | /> 74 | {args.map((props) => ( 75 | 79 | ))} 80 | 81 |
82 | ); 83 | }; 84 | 85 | export default CodemodArguments; 86 | -------------------------------------------------------------------------------- /intuita-webview/src/codemodList/CodemodArguments/styles.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | z-index: 1; 3 | width: 100%; 4 | padding: 1em; 5 | box-sizing: border-box; 6 | } 7 | 8 | .form { 9 | display: flex; 10 | flex-direction: column; 11 | row-gap: 8px; 12 | padding: 4px; 13 | } 14 | -------------------------------------------------------------------------------- /intuita-webview/src/codemodList/CodemodNodeRenderer/Directory.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames'; 2 | import s from './style.module.css'; 3 | import { memo } from 'react'; 4 | 5 | const Directory = ( 6 | props: Readonly<{ 7 | expanded: boolean; 8 | label: string; 9 | }>, 10 | ) => { 11 | return ( 12 | <> 13 | 19 |
20 | 21 | {props.label} 22 | 23 |
24 | 25 | ); 26 | }; 27 | 28 | export default memo(Directory); 29 | -------------------------------------------------------------------------------- /intuita-webview/src/codemodList/CodemodNodeRenderer/style.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | align-items: center; 4 | flex: 1; 5 | text-overflow: ellipsis; 6 | overflow: hidden; 7 | flex-wrap: nowrap; 8 | align-items: center; 9 | min-height: 28px; 10 | } 11 | 12 | .root:not(.focused):hover { 13 | background-color: var(--vscode-list-hoverBackground); 14 | } 15 | 16 | .focused { 17 | background-color: var(--vscode-list-activeSelectionBackground); 18 | } 19 | 20 | .root:focus-visible { 21 | outline: none; 22 | } 23 | 24 | .root .actionGroup { 25 | opacity: 0; 26 | } 27 | 28 | .root:hover .actionGroup, 29 | .focused .actionGroup { 30 | opacity: 1; 31 | } 32 | 33 | .root:hover .description { 34 | display: inline; 35 | } 36 | 37 | .labelContainer { 38 | line-height: 22px; 39 | font-size: 13px; 40 | cursor: pointer; 41 | color: var(--vscode-foreground); 42 | width: 100%; 43 | display: flex; 44 | align-items: center; 45 | } 46 | 47 | .labelContainer.focused { 48 | color: var(--vscode-list-activeSelectionForeground); 49 | } 50 | 51 | .directorySelector { 52 | display: flex; 53 | } 54 | 55 | .description { 56 | color: var(--vscode-descriptionForeground); 57 | font-size: 12px; 58 | line-height: normal; 59 | margin-left: 5px; 60 | display: none; 61 | } 62 | 63 | .actionGroup { 64 | display: flex; 65 | margin-left: auto; 66 | height: 100%; 67 | align-items: center; 68 | min-width: fit-content; 69 | } 70 | 71 | .label { 72 | user-select: none; 73 | text-overflow: ellipsis; 74 | overflow: hidden; 75 | white-space: nowrap; 76 | color: inherit; 77 | } 78 | 79 | .codemodRoot { 80 | display: flex; 81 | gap: 4px; 82 | align-items: center; 83 | width: 100%; 84 | padding: 0 3px; 85 | min-height: 28px; 86 | box-sizing: border-box; 87 | } 88 | 89 | .expandableContent { 90 | background-color: var(--vscode-panel-background); 91 | } 92 | 93 | .progressContainer { 94 | width: 100%; 95 | padding: 0.5em; 96 | padding-top: 0.1em; 97 | box-sizing: border-box; 98 | } 99 | 100 | .progressStatusLabel { 101 | font-size: var(--type-ramp-base-font-size); 102 | line-height: var(--type-ramp-base-line-height); 103 | color: var(--foreground); 104 | user-select: none; 105 | text-overflow: ellipsis; 106 | overflow: hidden; 107 | white-space: nowrap; 108 | } 109 | -------------------------------------------------------------------------------- /intuita-webview/src/codemodList/TreeView/ActionButton.tsx: -------------------------------------------------------------------------------- 1 | import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'; 2 | import IntuitaPopover from '../../shared/IntuitaPopover'; 3 | import cn from 'classnames'; 4 | import s from './style.module.css'; 5 | import { CSSProperties } from 'react'; 6 | 7 | type Props = { 8 | id?: string; 9 | content?: string; 10 | iconName?: string; 11 | children?: React.ReactNode; 12 | onClick(e: React.MouseEvent): void; 13 | disabled?: boolean; 14 | style?: CSSProperties; 15 | active?: boolean; 16 | }; 17 | 18 | const ActionButton = ({ 19 | content, 20 | disabled, 21 | iconName, 22 | children, 23 | style, 24 | id, 25 | active, 26 | onClick, 27 | }: Props) => { 28 | return ( 29 | 30 | { 35 | e.stopPropagation(); 36 | onClick(e); 37 | }} 38 | disabled={disabled} 39 | style={style} 40 | > 41 | {iconName ? ( 42 | 43 | ) : null} 44 | {children} 45 | 46 | 47 | ); 48 | }; 49 | 50 | export default ActionButton; 51 | -------------------------------------------------------------------------------- /intuita-webview/src/codemodList/TreeView/InfiniteProgress.module.css: -------------------------------------------------------------------------------- 1 | /* inspired by https://dev.to/themodernweb/quick-css-make-infinity-loading-animation-for-your-next-website-187k */ 2 | 3 | .progressContainer { 4 | position: relative; 5 | width: 100%; 6 | height: 2.5px; 7 | overflow: hidden; 8 | border-radius: 10px; 9 | } 10 | 11 | .progress { 12 | position: absolute; 13 | width: 100%; 14 | height: 100%; 15 | background: var(--vscode-progressBar-background); 16 | border-radius: 10px; 17 | left: -100%; 18 | animation: loading 1s linear infinite; 19 | } 20 | 21 | @keyframes loading { 22 | 0% { 23 | left: -100%; 24 | } 25 | 100% { 26 | left: 100%; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /intuita-webview/src/codemodList/TreeView/InfiniteProgress.tsx: -------------------------------------------------------------------------------- 1 | import styles from './InfiniteProgress.module.css'; 2 | 3 | export const InfiniteProgress = () => ( 4 |
5 |
6 |
7 | ); 8 | -------------------------------------------------------------------------------- /intuita-webview/src/codemodList/TreeView/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import { Line } from 'rc-progress'; 2 | 3 | const ProgressBar = ( 4 | props: Readonly<{ 5 | percent: number; 6 | }>, 7 | ) => ( 8 |
9 | 17 |
18 | ); 19 | 20 | export default ProgressBar; 21 | -------------------------------------------------------------------------------- /intuita-webview/src/codemodList/TreeView/index.tsx: -------------------------------------------------------------------------------- 1 | import { vscode } from '../../shared/utilities/vscode'; 2 | 3 | import { useProgressBar } from '../useProgressBar'; 4 | 5 | import { 6 | CodemodNode, 7 | CodemodNodeHashDigest, 8 | CodemodTree, 9 | NodeDatum, 10 | } from '../../../../src/selectors/selectCodemodTree'; 11 | 12 | import { IntuitaTreeView } from '../../intuitaTreeView'; 13 | import { getCodemodNodeRenderer } from '../CodemodNodeRenderer'; 14 | 15 | type Props = Readonly<{ 16 | tree: CodemodTree; 17 | screenWidth: number | null; 18 | autocompleteItems: ReadonlyArray; 19 | rootPath: string | null; 20 | }>; 21 | 22 | const onFocus = (hashDigest: CodemodNodeHashDigest) => { 23 | vscode.postMessage({ 24 | kind: 'webview.global.selectCodemodNodeHashDigest', 25 | selectedCodemodNodeHashDigest: hashDigest, 26 | }); 27 | }; 28 | 29 | const onFlip = (hashDigest: CodemodNodeHashDigest) => { 30 | vscode.postMessage({ 31 | kind: 'webview.global.flipCodemodHashDigest', 32 | codemodNodeHashDigest: hashDigest, 33 | }); 34 | 35 | onFocus(hashDigest); 36 | }; 37 | 38 | const TreeView = ({ 39 | tree, 40 | autocompleteItems, 41 | rootPath, 42 | screenWidth, 43 | }: Props) => { 44 | const progress = useProgressBar(); 45 | 46 | return ( 47 | 48 | {...tree} 49 | nodeRenderer={getCodemodNodeRenderer({ 50 | progress, 51 | screenWidth, 52 | autocompleteItems, 53 | rootPath, 54 | })} 55 | onFlip={onFlip} 56 | onFocus={onFocus} 57 | /> 58 | ); 59 | }; 60 | 61 | export default TreeView; 62 | -------------------------------------------------------------------------------- /intuita-webview/src/codemodList/TreeView/style.module.css: -------------------------------------------------------------------------------- 1 | .action { 2 | margin-left: 3px; 3 | display: flex; 4 | align-items: center; 5 | color: inherit; 6 | } 7 | 8 | .active { 9 | background: var(--button-icon-hover-background); 10 | outline: 1px dotted var(--contrast-active-border); 11 | outline-offset: -1px; 12 | } 13 | -------------------------------------------------------------------------------- /intuita-webview/src/codemodList/components/DirectorySelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useLayoutEffect, useState } from 'react'; 2 | import { 3 | VSCodeOption, 4 | VSCodeTextField, 5 | } from '@vscode/webview-ui-toolkit/react'; 6 | import styles from './style.module.css'; 7 | import cn from 'classnames'; 8 | 9 | type Props = { 10 | initialValue: string; 11 | autocompleteItems: ReadonlyArray; 12 | onChange: (value: string) => void; 13 | }; 14 | 15 | const AUTOCOMPLETE_OPTIONS_LENGTH = 20; 16 | 17 | const getFilteredOptions = ( 18 | allOptions: ReadonlyArray, 19 | value: string, 20 | ) => { 21 | // ignores slashes at the beginning, ignores whitespace 22 | const trimmedLowerCaseValue = value 23 | .replace(/^[/\\]+/, '') 24 | .trim() 25 | .toLocaleLowerCase(); 26 | 27 | return allOptions 28 | .filter((i) => i.toLocaleLowerCase().startsWith(trimmedLowerCaseValue)) 29 | .slice(0, AUTOCOMPLETE_OPTIONS_LENGTH); 30 | }; 31 | export const DirectorySelector = ({ 32 | initialValue, 33 | onChange, 34 | autocompleteItems, 35 | }: Props) => { 36 | const [value, setValue] = useState(initialValue); 37 | const [focusedOptionIdx, setFocusedOptionIdx] = useState( 38 | null, 39 | ); 40 | const [showOptions, setShowOptions] = useState(false); 41 | 42 | useEffect(() => { 43 | setValue(initialValue); 44 | }, [initialValue]); 45 | 46 | const autocompleteOptions = getFilteredOptions(autocompleteItems, value); 47 | 48 | const handleChange = (e: Event | React.FormEvent) => { 49 | setValue((e.target as HTMLInputElement)?.value); 50 | }; 51 | 52 | const handleFocus = () => { 53 | setFocusedOptionIdx(-1); 54 | setShowOptions(true); 55 | }; 56 | 57 | const handleKeyDown = (e: React.KeyboardEvent) => { 58 | const maxLength = autocompleteOptions.length; 59 | 60 | if (e.key === 'Esc') { 61 | setFocusedOptionIdx(0); 62 | setShowOptions(false); 63 | } 64 | 65 | if (e.key === 'Enter') { 66 | setShowOptions(false); 67 | 68 | if (focusedOptionIdx === null) { 69 | return; 70 | } 71 | 72 | const nextValue = autocompleteOptions[focusedOptionIdx] ?? ''; 73 | 74 | onChange(nextValue); 75 | setValue(nextValue); 76 | } 77 | 78 | if (e.key === 'ArrowUp') { 79 | const nextValue = 80 | focusedOptionIdx === null 81 | ? maxLength 82 | : (focusedOptionIdx - 1 + maxLength) % maxLength; 83 | setFocusedOptionIdx(nextValue); 84 | e.stopPropagation(); 85 | e.preventDefault(); 86 | } 87 | 88 | if (e.key === 'ArrowDown') { 89 | const nextValue = 90 | focusedOptionIdx === null 91 | ? 0 92 | : (focusedOptionIdx + 1) % maxLength; 93 | 94 | setFocusedOptionIdx(nextValue); 95 | e.stopPropagation(); 96 | e.preventDefault(); 97 | } 98 | 99 | if (e.key === 'Tab') { 100 | const nextValue = 101 | focusedOptionIdx === null 102 | ? 0 103 | : (focusedOptionIdx + 1) % maxLength; 104 | setFocusedOptionIdx(nextValue); 105 | e.stopPropagation(); 106 | e.preventDefault(); 107 | } 108 | }; 109 | 110 | useLayoutEffect(() => { 111 | document.getElementById(`option_${focusedOptionIdx}`)?.focus(); 112 | }, [focusedOptionIdx]); 113 | 114 | return ( 115 |
123 |
127 | 134 | Target path 135 | 136 |
137 | {showOptions && 138 | autocompleteOptions.map((item, i) => ( 139 | { 144 | setShowOptions(false); 145 | setValue(item); 146 | onChange(item); 147 | }} 148 | > 149 | {item} 150 | 151 | ))} 152 |
153 |
154 |
155 | ); 156 | }; 157 | -------------------------------------------------------------------------------- /intuita-webview/src/codemodList/components/style.module.css: -------------------------------------------------------------------------------- 1 | .textField { 2 | flex-grow: 1; 3 | } 4 | 5 | .textField::part(control) { 6 | font-size: 12px; 7 | opacity: 0.75; 8 | } 9 | 10 | .option { 11 | min-height: 20px; 12 | width: 100%; 13 | display: inline-block; 14 | line-height: 20px; 15 | } 16 | 17 | .autocompleteItems { 18 | background-color: var(--vscode-editor-background); 19 | padding: 4px; 20 | max-height: 250px; 21 | overflow-y: auto; 22 | } 23 | -------------------------------------------------------------------------------- /intuita-webview/src/codemodList/style.module.css: -------------------------------------------------------------------------------- 1 | .privateCodemodWelcomeMessage { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | padding: 0 20px 1em; 6 | margin-top: 13px; 7 | } 8 | 9 | .privateCodemodWelcomeMessage p { 10 | line-height: 22px; 11 | text-align: center; 12 | } 13 | 14 | .privateCodemodWelcomeMessage p a { 15 | text-decoration: none; 16 | } 17 | -------------------------------------------------------------------------------- /intuita-webview/src/codemodList/useProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { CodemodHash, WebviewMessage } from '../shared/types'; 3 | 4 | export type Progress = Readonly<{ 5 | codemodHash: CodemodHash; 6 | progressKind: 'finite' | 'infinite'; 7 | totalFileNumber: number; 8 | processedFileNumber: number; 9 | }>; 10 | 11 | export const useProgressBar = (): Progress | null => { 12 | const [codemodExecutionProgress, setCodemodExecutionProgress] = 13 | useState(null); 14 | 15 | useEffect(() => { 16 | const handler = (e: MessageEvent) => { 17 | const message = e.data; 18 | 19 | if (message.kind === 'webview.global.setCodemodExecutionProgress') { 20 | setCodemodExecutionProgress({ 21 | codemodHash: message.codemodHash, 22 | progressKind: message.progressKind, 23 | totalFileNumber: message.totalFileNumber, 24 | processedFileNumber: message.processedFileNumber, 25 | }); 26 | } 27 | 28 | if (message.kind === 'webview.global.codemodExecutionHalted') { 29 | setCodemodExecutionProgress(null); 30 | } 31 | }; 32 | 33 | window.addEventListener('message', handler); 34 | 35 | return () => { 36 | window.removeEventListener('message', handler); 37 | }; 38 | }, [codemodExecutionProgress?.codemodHash]); 39 | 40 | return codemodExecutionProgress; 41 | }; 42 | -------------------------------------------------------------------------------- /intuita-webview/src/communityTab/CommunityTab.tsx: -------------------------------------------------------------------------------- 1 | import { VSCodeLink } from '@vscode/webview-ui-toolkit/react'; 2 | import { ReactComponent as SlackIcon } from '../assets/slack.svg'; 3 | import { ReactComponent as YoutubeIcon } from '../assets/youtube.svg'; 4 | import intuitaLogo from './../assets/intuita_square128.png'; 5 | 6 | import styles from './style.module.css'; 7 | 8 | const IntuitaIcon = ( 9 | intuita-logo 10 | ); 11 | 12 | const EXTERNAL_LINKS = [ 13 | { 14 | id: 'featureRequest', 15 | text: 'Feature requests', 16 | url: 'https://feedback.intuita.io/feature-requests-and-bugs', 17 | icon: IntuitaIcon, 18 | }, 19 | { 20 | id: 'codemodRequest', 21 | text: 'Codemod requests', 22 | url: 'https://feedback.intuita.io/codemod-requests', 23 | icon: IntuitaIcon, 24 | }, 25 | { 26 | id: 'docs', 27 | text: 'Docs', 28 | url: 'https://docs.intuita.io/docs/intro', 29 | icon: IntuitaIcon, 30 | }, 31 | { 32 | id: 'youtube', 33 | text: 'Youtube channel', 34 | url: 'https://www.youtube.com/channel/UCAORbHiie6y5yVaAUL-1nHA', 35 | icon: , 36 | }, 37 | { 38 | id: 'slack', 39 | text: 'Chat with us on Slack', 40 | url: 'https://join.slack.com/t/intuita-inc/shared_invite/zt-1untfdpwh-XWuFslRz0D8cGbmjymd3Bw', 41 | icon: ( 42 | 51 | ), 52 | }, 53 | ]; 54 | 55 | export const CommunityTab = () => ( 56 |
57 | {EXTERNAL_LINKS.map(({ text, url, id, icon }) => { 58 | return ( 59 | 60 | {icon} 61 | 68 | {text} 69 | 70 | 71 | ); 72 | })} 73 |
74 | ); 75 | -------------------------------------------------------------------------------- /intuita-webview/src/communityTab/style.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | padding: 4px; 6 | } 7 | 8 | .link { 9 | width: 100%; 10 | height: 24px; 11 | display: flex; 12 | align-items: center; 13 | color: var(--vscode-foreground); 14 | } 15 | 16 | .link::part(control) { 17 | display: flex; 18 | align-items: center; 19 | gap: 4px; 20 | } 21 | 22 | .link:focus { 23 | &::part(control) { 24 | border-color: transparent; 25 | } 26 | } 27 | 28 | .icon { 29 | width: 10px; 30 | height: 10px; 31 | } 32 | -------------------------------------------------------------------------------- /intuita-webview/src/errors/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import styles from './style.module.css'; 3 | import { 4 | VSCodeDataGrid, 5 | VSCodeDataGridRow, 6 | VSCodeDataGridCell, 7 | } from '@vscode/webview-ui-toolkit/react'; 8 | import type { WebviewMessage } from '../../../src/components/webview/webviewEvents'; 9 | import type { ExecutionError } from '../../../src/errors/types'; 10 | import type { ErrorWebviewViewProps } from '../../../src/selectors/selectErrorWebviewViewProps'; 11 | 12 | const header = ( 13 | 14 | 15 | Message 16 | 17 | 18 | File Path 19 | 20 | 21 | ); 22 | 23 | const buildExecutionErrorRow = ( 24 | executionError: ExecutionError, 25 | index: number, 26 | ) => { 27 | return ( 28 | 29 | 30 | {executionError.message} 31 | 32 | 33 | {executionError.path ?? ''} 34 | 35 | 36 | ); 37 | }; 38 | 39 | declare global { 40 | interface Window { 41 | errorWebviewViewProps: ErrorWebviewViewProps; 42 | } 43 | } 44 | 45 | export const App = () => { 46 | const [props, setProps] = useState(window.errorWebviewViewProps); 47 | 48 | useEffect(() => { 49 | const handler = (event: MessageEvent) => { 50 | if (event.data.kind !== 'webview.error.setProps') { 51 | return; 52 | } 53 | 54 | setProps(event.data.errorWebviewViewProps); 55 | }; 56 | 57 | window.addEventListener('message', handler); 58 | 59 | return () => { 60 | window.removeEventListener('message', handler); 61 | }; 62 | }, []); 63 | 64 | if (props.kind !== 'CASE_SELECTED') { 65 | return ( 66 |
67 |

68 | {props.kind === 'MAIN_WEBVIEW_VIEW_NOT_VISIBLE' 69 | ? 'Open the left-sided Intuita View Container to see the errors.' 70 | : props.kind === 'CODEMOD_RUNS_TAB_NOT_ACTIVE' 71 | ? 'Open the Codemod Runs tab to see the errors.' 72 | : 'Choose a codemod run from Codemod Runs to see its errors.'} 73 |

74 |
75 | ); 76 | } 77 | 78 | if (props.executionErrors.length === 0) { 79 | return ( 80 |
81 |

82 | No execution errors found for the selected codemod run. 83 |

84 |
85 | ); 86 | } 87 | 88 | const rows = props.executionErrors.map(buildExecutionErrorRow); 89 | 90 | return ( 91 |
92 | 93 | {header} 94 | {rows} 95 | 96 |
97 | ); 98 | }; 99 | -------------------------------------------------------------------------------- /intuita-webview/src/errors/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Errors 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /intuita-webview/src/errors/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import '../shared/index.css'; 4 | import { App } from './App'; 5 | 6 | const root = ReactDOM.createRoot( 7 | document.getElementById('root') as HTMLElement, 8 | ); 9 | 10 | root.render( 11 | 12 | 13 | , 14 | ); 15 | -------------------------------------------------------------------------------- /intuita-webview/src/errors/style.module.css: -------------------------------------------------------------------------------- 1 | .welcomeMessage { 2 | padding: 0 20px 1em; 3 | margin-top: 13px; 4 | } 5 | 6 | body { 7 | overflow-y: scroll; 8 | } 9 | -------------------------------------------------------------------------------- /intuita-webview/src/fileExplorer/ActionsFooter/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | VSCodeButton, 3 | VSCodeProgressRing, 4 | } from '@vscode/webview-ui-toolkit/react'; 5 | import IntuitaPopover from '../../shared/IntuitaPopover'; 6 | import { vscode } from '../../shared/utilities/vscode'; 7 | import styles from './style.module.css'; 8 | import { CaseHash } from '../../../../src/cases/types'; 9 | 10 | const POPOVER_TEXTS = { 11 | discard: 'Discard selected changes for the highlighted codemod.', 12 | apply: 'Save selected changes to file(s).', 13 | cannotApply: 'At least one job should be selected to apply the changes.', 14 | }; 15 | 16 | const discardSelected = (caseHashDigest: CaseHash) => { 17 | vscode.postMessage({ 18 | kind: 'webview.global.discardSelected', 19 | caseHashDigest, 20 | }); 21 | }; 22 | 23 | const applySelected = (caseHashDigest: CaseHash) => { 24 | vscode.postMessage({ 25 | kind: 'webview.global.applySelected', 26 | caseHashDigest, 27 | }); 28 | }; 29 | 30 | const getDiscardText = (selectedJobCount: number) => { 31 | return `Discard ${selectedJobCount} ${ 32 | selectedJobCount === 1 ? 'file' : 'files' 33 | }`; 34 | }; 35 | const getApplyText = (selectedJobCount: number) => { 36 | return `Apply ${selectedJobCount} ${ 37 | selectedJobCount === 1 ? 'file' : 'files' 38 | }`; 39 | }; 40 | 41 | type Props = Readonly<{ 42 | caseHash: CaseHash; 43 | selectedJobCount: number; 44 | applySelectedInProgress: boolean; 45 | screenWidth: number | null; 46 | }>; 47 | 48 | export const ActionsFooter = ({ 49 | caseHash, 50 | selectedJobCount, 51 | screenWidth, 52 | applySelectedInProgress, 53 | }: Props) => { 54 | return ( 55 |
= 300 && { justifyContent: 'flex-end' }), 60 | }} 61 | > 62 | 63 | { 66 | event.preventDefault(); 67 | 68 | discardSelected(caseHash); 69 | }} 70 | className={styles.vscodeButton} 71 | disabled={selectedJobCount === 0} 72 | > 73 | {getDiscardText(selectedJobCount)} 74 | 75 | 76 | 83 | { 86 | event.preventDefault(); 87 | 88 | applySelected(caseHash); 89 | }} 90 | className={styles.vscodeButton} 91 | disabled={applySelectedInProgress || selectedJobCount === 0} 92 | > 93 | {applySelectedInProgress && ( 94 | 95 | )} 96 | {getApplyText(selectedJobCount)} 97 | 98 | 99 |
100 | ); 101 | }; 102 | -------------------------------------------------------------------------------- /intuita-webview/src/fileExplorer/ActionsFooter/style.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | align-items: center; 4 | width: 100%; 5 | z-index: 1001; 6 | height: 35px; 7 | top: 0; 8 | padding: 4px 6px 4px 4px; 9 | background-color: var(--vscode-tab-inactiveBackground); 10 | gap: 8px; 11 | } 12 | 13 | .icon { 14 | width: 22px; 15 | height: 22px; 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | } 20 | 21 | .progressRing { 22 | margin-right: 8px; 23 | width: 18px; 24 | height: 18px; 25 | } 26 | 27 | .progressRing::part(indeterminate-indicator-1) { 28 | stroke: var(--vscode-icon-foreground); 29 | } 30 | -------------------------------------------------------------------------------- /intuita-webview/src/fileExplorer/App.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import styles from './style.module.css'; 3 | import SearchBar from '../shared/SearchBar'; 4 | import { ActionsFooter } from './ActionsFooter'; 5 | import Progress from '../shared/Progress'; 6 | 7 | import { vscode } from '../shared/utilities/vscode'; 8 | import { IntuitaTreeView } from '../intuitaTreeView'; 9 | import { explorerNodeRenderer } from './explorerNodeRenderer'; 10 | import { 11 | _ExplorerNode, 12 | _ExplorerNodeHashDigest, 13 | } from '../../../src/persistedState/explorerNodeCodec'; 14 | import { CaseHash } from '../../../src/cases/types'; 15 | import { MainWebviewViewProps } from '../../../src/selectors/selectMainWebviewViewProps'; 16 | import LoadingProgress from '../jobDiffView/Components/LoadingProgress'; 17 | import { useProgressBar } from '../codemodList/useProgressBar'; 18 | 19 | const setSearchPhrase = (caseHashDigest: CaseHash, searchPhrase: string) => { 20 | vscode.postMessage({ 21 | kind: 'webview.global.setChangeExplorerSearchPhrase', 22 | caseHashDigest, 23 | searchPhrase, 24 | }); 25 | }; 26 | 27 | const onFocus = ( 28 | caseHashDigest: CaseHash, 29 | explorerNodeHashDigest: _ExplorerNodeHashDigest, 30 | ) => { 31 | vscode.postMessage({ 32 | kind: 'webview.global.focusExplorerNode', 33 | caseHashDigest, 34 | explorerNodeHashDigest, 35 | }); 36 | }; 37 | 38 | const onCollapsibleExplorerNodeFlip = ( 39 | caseHashDigest: CaseHash, 40 | explorerNodeHashDigest: _ExplorerNodeHashDigest, 41 | ) => { 42 | vscode.postMessage({ 43 | kind: 'webview.global.flipCollapsibleExplorerNode', 44 | caseHashDigest, 45 | explorerNodeHashDigest, 46 | }); 47 | 48 | onFocus(caseHashDigest, explorerNodeHashDigest); 49 | }; 50 | 51 | export const App = ( 52 | props: { screenWidth: number | null } & MainWebviewViewProps & { 53 | activeTabId: 'codemodRuns'; 54 | }, 55 | ) => { 56 | const { changeExplorerTree, codemodExecutionInProgress } = props; 57 | const progress = useProgressBar(); 58 | const caseHash = changeExplorerTree?.caseHash ?? null; 59 | 60 | const handleFocus = useCallback( 61 | (hashDigest: _ExplorerNodeHashDigest) => { 62 | if (caseHash === null) { 63 | return; 64 | } 65 | 66 | onFocus(caseHash, hashDigest); 67 | }, 68 | 69 | [caseHash], 70 | ); 71 | 72 | const handleFlip = useCallback( 73 | (hashDigest: _ExplorerNodeHashDigest) => { 74 | if (caseHash === null) { 75 | return; 76 | } 77 | 78 | onCollapsibleExplorerNodeFlip(caseHash, hashDigest); 79 | }, 80 | [caseHash], 81 | ); 82 | 83 | if ((props.changeExplorerTree?.caseHash ?? null) === null) { 84 | return codemodExecutionInProgress ? ( 85 | 92 | ) : ( 93 |

94 | Choose a Codemod from Codemod Runs to explore its changes! 95 |

96 | ); 97 | } 98 | 99 | return ( 100 |
106 | {changeExplorerTree !== null && ( 107 | 110 | setSearchPhrase( 111 | changeExplorerTree.caseHash, 112 | searchPhrase, 113 | ) 114 | } 115 | placeholder="Search by file name" 116 | /> 117 | )} 118 |
119 | {changeExplorerTree !== null ? ( 120 | 121 | {...changeExplorerTree} 122 | nodeRenderer={explorerNodeRenderer(changeExplorerTree)} 123 | onFlip={handleFlip} 124 | onFocus={handleFocus} 125 | /> 126 | ) : ( 127 | 128 | )} 129 |
130 | {changeExplorerTree !== null && ( 131 | 137 | )} 138 |
139 | ); 140 | }; 141 | -------------------------------------------------------------------------------- /intuita-webview/src/fileExplorer/FileExplorerTreeNode.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames'; 2 | import TreeItem, { Props as TreeItemProps } from '../shared/TreeItem'; 3 | import { memo } from 'react'; 4 | import { ReactComponent as CheckboxMaterialIcon } from '../assets/material-icons/check_box.svg'; 5 | import { ReactComponent as CheckboxOutlineBlankMaterialIcon } from '../assets/material-icons/check_box_outline_blank.svg'; 6 | import { ReactComponent as IndeterminateCheckboxMaterialIcon } from '../assets/material-icons/indeterminate_check_box.svg'; 7 | 8 | import styles from './style.module.css'; 9 | import { _ExplorerNode } from '../../../src/persistedState/explorerNodeCodec'; 10 | 11 | type Props = Omit< 12 | TreeItemProps, 13 | 'icon' | 'startDecorator' | 'inlineStyles' | 'subLabel' 14 | > & { 15 | kind: _ExplorerNode['kind']; 16 | iconName: IconName | null; 17 | checkboxState: 'checked' | 'blank' | 'indeterminate'; 18 | reviewed: boolean; 19 | onCheckboxClick(e: React.MouseEvent): void; 20 | searchPhrase: string; 21 | }; 22 | 23 | export type IconName = 'file-add' | 'file'; 24 | 25 | const getIcon = (iconName: IconName | null) => { 26 | if (iconName !== 'file-add' && iconName !== 'file') { 27 | return null; 28 | } 29 | 30 | return ; 31 | }; 32 | 33 | const getIndent = (kind: _ExplorerNode['kind'], depth: number) => { 34 | let offset = 17 * depth; 35 | 36 | if (kind === 'FILE') { 37 | offset += 17; 38 | } 39 | 40 | return offset; 41 | }; 42 | 43 | const Checkbox = memo( 44 | ({ 45 | checkboxState, 46 | onClick, 47 | }: { 48 | checkboxState: 'checked' | 'blank' | 'indeterminate'; 49 | onClick(e: React.MouseEvent): void; 50 | }) => { 51 | return ( 52 | 53 | {checkboxState === 'checked' && ( 54 | 55 | )} 56 | {checkboxState === 'blank' && ( 57 | 58 | )} 59 | {checkboxState === 'indeterminate' && ( 60 | 61 | )} 62 | 63 | ); 64 | }, 65 | ); 66 | 67 | const FileExplorerTreeNode = ({ 68 | hasChildren, 69 | id, 70 | label, 71 | depth, 72 | open, 73 | focused, 74 | reviewed, 75 | iconName, 76 | checkboxState, 77 | kind, 78 | onClick, 79 | onCheckboxClick, 80 | onPressChevron, 81 | searchPhrase, 82 | }: Props) => { 83 | return ( 84 | 100 | } 101 | onPressChevron={onPressChevron} 102 | inlineStyles={{ 103 | root: { 104 | ...(!focused && { 105 | backgroundColor: 'var(--vscode-list-hoverBackground)', 106 | }), 107 | paddingRight: 4, 108 | }, 109 | }} 110 | endDecorator={ 111 | reviewed && 112 | } 113 | /> 114 | ); 115 | }; 116 | 117 | export default memo(FileExplorerTreeNode); 118 | -------------------------------------------------------------------------------- /intuita-webview/src/fileExplorer/explorerNodeRenderer.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import TreeItem, { IconName } from './FileExplorerTreeNode'; 3 | import { ExplorerTree } from '../../../src/selectors/selectExplorerTree'; 4 | import { NodeDatum } from '../intuitaTreeView'; 5 | import { 6 | _ExplorerNode, 7 | _ExplorerNodeHashDigest, 8 | } from '../../../src/persistedState/explorerNodeCodec'; 9 | import { vscode } from '../shared/utilities/vscode'; 10 | 11 | const getIconName = (explorerNode: _ExplorerNode): IconName | null => { 12 | if (explorerNode.kind === 'FILE') { 13 | return explorerNode.fileAdded ? 'file-add' : 'file'; 14 | } 15 | 16 | return null; 17 | }; 18 | 19 | const getLabel = (explorerNode: _ExplorerNode, opened: boolean): string => { 20 | return explorerNode.kind !== 'FILE' && !opened 21 | ? `${explorerNode.label} (${explorerNode.childCount})` 22 | : explorerNode.label; 23 | }; 24 | 25 | export const explorerNodeRenderer = 26 | (explorerTree: ExplorerTree) => 27 | (props: { 28 | nodeDatum: NodeDatum<_ExplorerNodeHashDigest, _ExplorerNode>; 29 | onFlip: (hashDigest: _ExplorerNodeHashDigest) => void; 30 | onFocus: (hashDigest: _ExplorerNodeHashDigest) => void; 31 | }) => { 32 | const iconName = getIconName(props.nodeDatum.node); 33 | const focused = props.nodeDatum.focused; 34 | const reviewed = props.nodeDatum.reviewed; 35 | 36 | const { onFocus, onFlip, nodeDatum } = props; 37 | 38 | const explorerNodeHashDigest = props.nodeDatum.node.hashDigest; 39 | 40 | const checkboxState = 41 | explorerTree.indeterminateExplorerNodeHashDigests.includes( 42 | explorerNodeHashDigest, 43 | ) 44 | ? 'indeterminate' 45 | : explorerTree.selectedExplorerNodeHashDigests.includes( 46 | explorerNodeHashDigest, 47 | ) 48 | ? 'checked' 49 | : 'blank'; 50 | 51 | const handleClick = useCallback( 52 | (event: React.MouseEvent) => { 53 | event.stopPropagation(); 54 | 55 | onFocus(nodeDatum.node.hashDigest); 56 | }, 57 | [onFocus, nodeDatum.node.hashDigest], 58 | ); 59 | 60 | const handleCheckboxClick = useCallback( 61 | (event: React.MouseEvent) => { 62 | event.stopPropagation(); 63 | 64 | vscode.postMessage({ 65 | kind: 'webview.global.flipSelectedExplorerNode', 66 | caseHashDigest: explorerTree.caseHash, 67 | explorerNodeHashDigest, 68 | }); 69 | }, 70 | [explorerNodeHashDigest], 71 | ); 72 | 73 | const handleChevronClick = useCallback( 74 | (event: React.MouseEvent) => { 75 | event.stopPropagation(); 76 | 77 | onFlip(nodeDatum.node.hashDigest); 78 | }, 79 | [onFlip, nodeDatum.node.hashDigest], 80 | ); 81 | 82 | return ( 83 | 101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /intuita-webview/src/fileExplorer/style.module.css: -------------------------------------------------------------------------------- 1 | .welcomeMessage { 2 | padding: 0 20px 1em; 3 | margin-top: 13px; 4 | } 5 | 6 | .container { 7 | height: 100%; 8 | overflow: hidden; 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | 13 | .treeContainer { 14 | flex-grow: 1; 15 | overflow-y: auto; 16 | } 17 | 18 | .checkbox { 19 | width: 16px; 20 | height: 16px; 21 | } 22 | 23 | .checkbox svg { 24 | width: 100%; 25 | height: 100%; 26 | } 27 | -------------------------------------------------------------------------------- /intuita-webview/src/jobDiffView/App.tsx: -------------------------------------------------------------------------------- 1 | import ReactMarkdown from 'react-markdown'; 2 | import { useEffect, useState } from 'react'; 3 | import { WebviewMessage } from '../shared/types'; 4 | import { JobDiffViewContainer } from './DiffViewer/index'; 5 | import './index.css'; 6 | import type { PanelViewProps } from '../../../src/components/webview/panelViewProps'; 7 | import { vscode } from '../shared/utilities/vscode'; 8 | import styles from './style.module.css'; 9 | 10 | declare global { 11 | interface Window { 12 | panelViewProps: PanelViewProps; 13 | } 14 | } 15 | 16 | export const App = () => { 17 | const [viewProps, setViewProps] = useState(window.panelViewProps); 18 | 19 | useEffect(() => { 20 | vscode.postMessage({ 21 | kind: 'webview.jobDiffView.webviewMounted', 22 | }); 23 | }, []); 24 | 25 | useEffect(() => { 26 | const eventHandler = (event: MessageEvent) => { 27 | if (event.data.kind === 'webview.setPanelViewProps') { 28 | setViewProps(event.data.panelViewProps); 29 | } 30 | }; 31 | 32 | window.addEventListener('message', eventHandler); 33 | 34 | return () => { 35 | window.removeEventListener('message', eventHandler); 36 | }; 37 | }, []); 38 | 39 | if (viewProps.kind === 'CODEMOD') { 40 | return ( 41 |
42 | 43 |
44 | ); 45 | } 46 | 47 | return ( 48 |
49 | 50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /intuita-webview/src/jobDiffView/Components/Collapsable.css: -------------------------------------------------------------------------------- 1 | .collapsable { 2 | display: flex; 3 | flex-direction: column; 4 | background-color: var(--vscode-editor-background); 5 | border-radius: 5px; 6 | } 7 | 8 | .collapsable__header { 9 | display: flex; 10 | flex-direction: row; 11 | align-items: center; 12 | padding: 0 0 0 0.5rem; 13 | cursor: pointer; 14 | } 15 | 16 | .collapsable__header--sticky { 17 | position: sticky; 18 | top: 0; 19 | z-index: 1000; 20 | background-color: var(--vscode-editor-background); 21 | } 22 | 23 | .collapsable__arrow { 24 | margin-right: 0.5rem; 25 | width: 18px; 26 | height: 18px; 27 | transition: transform 0.2s ease-in-out; 28 | } 29 | 30 | .collapsable__arrow--collapsed { 31 | transform: rotate(-90deg); 32 | } 33 | 34 | .collapsable_content { 35 | padding: 0 0 0 1.5rem; 36 | } 37 | -------------------------------------------------------------------------------- /intuita-webview/src/jobDiffView/Components/Collapsable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ReactComponent as ArrowDownIcon } from '../../assets/arrow-down.svg'; 3 | import './Collapsable.css'; 4 | import cn from 'classnames'; 5 | 6 | type CollapsableProps = Readonly<{ 7 | defaultExpanded: boolean; 8 | headerComponent: React.ReactNode; 9 | headerClassName?: string; 10 | headerChevronClassName?: string; 11 | headerSticky?: boolean; 12 | children: React.ReactNode; 13 | contentClassName?: string; 14 | className?: string; 15 | onToggle?: (expanded: boolean) => void; 16 | }>; 17 | 18 | export const Collapsable = ({ 19 | onToggle, 20 | defaultExpanded: defaultCollapsed, 21 | headerSticky, 22 | headerComponent, 23 | headerClassName, 24 | headerChevronClassName, 25 | contentClassName, 26 | className, 27 | children, 28 | }: CollapsableProps) => { 29 | return ( 30 |
31 |
onToggle?.(!defaultCollapsed)} 37 | > 38 | {onToggle && ( 39 | 49 | )} 50 | {headerComponent} 51 |
52 | {defaultCollapsed && ( 53 |
54 | {children} 55 |
56 | )} 57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /intuita-webview/src/jobDiffView/Components/LoadingProgress/index.tsx: -------------------------------------------------------------------------------- 1 | import { VSCodeProgressRing } from '@vscode/webview-ui-toolkit/react'; 2 | import styles from './style.module.css'; 3 | 4 | type Props = { 5 | description: string; 6 | }; 7 | 8 | const LoadingProgress = ({ description }: Props) => { 9 | return ( 10 |
11 | 12 | {description} 13 |
14 | ); 15 | }; 16 | 17 | export default LoadingProgress; 18 | -------------------------------------------------------------------------------- /intuita-webview/src/jobDiffView/Components/LoadingProgress/style.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | padding: 8px; 3 | padding-top: calc(8px * 4); 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | row-gap: 8px; 8 | } 9 | 10 | .progressRing { 11 | width: 45px; 12 | height: 45px; 13 | } 14 | -------------------------------------------------------------------------------- /intuita-webview/src/jobDiffView/DiffViewer/Container.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | display: flex; 4 | font-family: var(--vscode-editor-font-family); 5 | } 6 | 7 | .checkbox-container { 8 | padding: 2px 10px; 9 | background-color: var(--checkbox-background); 10 | } 11 | 12 | .diff-title { 13 | padding: 5px 8px; 14 | background: var(--vscode-editor-background); 15 | border-radius: 3px; 16 | overflow: hidden; 17 | } 18 | 19 | .highlighted-text { 20 | color: var(--vscode-editorSuggestWidget-focusHighlightForeground); 21 | padding: 5px 0px 5px 8px; 22 | background: transparent; 23 | border-radius: 3px; 24 | overflow: hidden; 25 | } 26 | 27 | .diff-changes { 28 | padding: 2px 4px; 29 | margin-left: 2px; 30 | border-radius: 2px; 31 | } 32 | 33 | .diff-removed { 34 | color: #cb3b3b; 35 | } 36 | .diff-added { 37 | color: #3ac06b; 38 | } 39 | 40 | /* VSCodeButton creates a shadow
74 |
75 | {props.viewType === 'side-by-side' ? ( 76 | props.onViewChange('inline')} 80 | > 81 | Inline 82 | 83 | ) : ( 84 | props.onViewChange('side-by-side')} 88 | > 89 | Side by Side 90 | 91 | )} 92 |
93 | 94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /intuita-webview/src/jobDiffView/DiffViewer/Header/style.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | align-items: center; 4 | z-index: 1001; 5 | position: sticky; 6 | height: 35px; 7 | top: 0; 8 | padding: 4px 8px; 9 | background-color: var(--vscode-tab-inactiveBackground); 10 | gap: 8px; 11 | } 12 | 13 | .icon { 14 | stroke: var(--vscode-icon-foreground); 15 | height: 16px; 16 | width: 16px; 17 | } 18 | 19 | .actionsContainer { 20 | display: flex; 21 | gap: 8px; 22 | } 23 | 24 | .actionsContainer span { 25 | padding: 8px; 26 | } 27 | 28 | .checkbox { 29 | color: var(--vscode-icon-foreground); 30 | margin-left: 32px; 31 | } 32 | 33 | .buttonGroup { 34 | display: flex; 35 | margin-left: auto; 36 | align-items: center; 37 | gap: 8px; 38 | } 39 | 40 | .buttonGroup h4 { 41 | user-select: none; 42 | font-size: var(--vscode-editor-font-size); 43 | overflow: hidden; 44 | text-overflow: ellipsis; 45 | } 46 | -------------------------------------------------------------------------------- /intuita-webview/src/jobDiffView/DiffViewer/configure.ts: -------------------------------------------------------------------------------- 1 | import { Monaco } from '@monaco-editor/react'; 2 | import type { editor } from 'monaco-editor'; 3 | 4 | const ignoreCodes = [ 5 | 2304, // unresolved vars 6 | 2451, // redeclared block scope vars 7 | 2552, // undef 8 | ]; 9 | 10 | const configure = (e: editor.IStandaloneDiffEditor, m: Monaco) => { 11 | m.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ 12 | diagnosticCodesToIgnore: ignoreCodes, 13 | }); 14 | 15 | const editor = e.getModifiedEditor(); 16 | const model = editor.getModel(); 17 | 18 | const path = model?.uri.path; 19 | const lang = model?.getLanguageId(); 20 | 21 | if (lang === 'typescript' && path?.endsWith('.tsx')) { 22 | m.languages.typescript.typescriptDefaults.setCompilerOptions({ 23 | jsx: m.languages.typescript.JsxEmit.React, 24 | }); 25 | } 26 | }; 27 | 28 | export default configure; 29 | -------------------------------------------------------------------------------- /intuita-webview/src/jobDiffView/DiffViewer/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import { JobDiffView } from './DiffItem'; 3 | import { DiffViewType } from '../../shared/types'; 4 | import { useCTLKey } from '../hooks/useKey'; 5 | 6 | import { Header } from './Header'; 7 | import { useTheme } from '../../shared/Snippet/useTheme'; 8 | import type { PanelViewProps } from '../../../../src/components/webview/panelViewProps'; 9 | import { vscode } from '../../shared/utilities/vscode'; 10 | import { CaseHash } from '../../../../src/cases/types'; 11 | 12 | const focusExplorerNodeSibling = ( 13 | caseHashDigest: CaseHash, 14 | direction: 'prev' | 'next', 15 | ) => { 16 | vscode.postMessage({ 17 | kind: 'webview.global.focusExplorerNodeSibling', 18 | caseHashDigest, 19 | direction, 20 | }); 21 | }; 22 | 23 | export const JobDiffViewContainer = ( 24 | props: PanelViewProps & { kind: 'JOB' }, 25 | ) => { 26 | const containerRef = useRef(null); 27 | const [viewType, setViewType] = useState('side-by-side'); 28 | 29 | useCTLKey('d', () => { 30 | setViewType((v) => (v === 'side-by-side' ? 'inline' : 'side-by-side')); 31 | }); 32 | 33 | const theme = useTheme(); 34 | 35 | return ( 36 |
37 |
41 | focusExplorerNodeSibling(props.caseHash, direction) 42 | } 43 | /> 44 |
45 | 46 |
47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /intuita-webview/src/jobDiffView/hooks/useElementSize.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, RefObject } from 'react'; 2 | 3 | type Size = Readonly<{ 4 | width: number; 5 | height: number; 6 | }>; 7 | export function useElementSize(ref: RefObject): Size { 8 | const [size, setSize] = useState({ width: 0, height: 0 }); 9 | 10 | useEffect(() => { 11 | const element = ref.current; 12 | 13 | function handleResize() { 14 | if (!element) { 15 | return; 16 | } 17 | setSize({ 18 | width: element.offsetWidth, 19 | height: element.offsetHeight, 20 | }); 21 | } 22 | 23 | if (element) { 24 | handleResize(); 25 | window.addEventListener('resize', handleResize); 26 | } 27 | return () => { 28 | window.removeEventListener('resize', handleResize); 29 | }; 30 | }, [ref]); 31 | 32 | return size; 33 | } 34 | -------------------------------------------------------------------------------- /intuita-webview/src/jobDiffView/hooks/useKey.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react'; 2 | 3 | /** 4 | * Hook that detects when ctl/meta + some key is pressed 5 | */ 6 | export const useCTLKey = (key: string, callback: () => void) => { 7 | const keyPressCallback = useCallback( 8 | (event: KeyboardEvent) => { 9 | if (event.key === key && (event.ctrlKey || event.metaKey)) { 10 | callback(); 11 | } 12 | }, 13 | [callback, key], 14 | ); 15 | 16 | useEffect(() => { 17 | document.addEventListener('keydown', keyPressCallback); 18 | 19 | return () => document.removeEventListener('keydown', keyPressCallback); 20 | }, [keyPressCallback]); 21 | }; 22 | 23 | /** 24 | * Hook that detects when some key is pressed 25 | */ 26 | export const useKey = ( 27 | container: HTMLElement | null, 28 | key: KeyboardEvent['key'], 29 | callback: () => void, 30 | ) => { 31 | const keyDownCallback = useCallback( 32 | (event: KeyboardEvent) => { 33 | if (event.key === key) { 34 | event.preventDefault(); 35 | callback(); 36 | } 37 | }, 38 | [callback, key], 39 | ); 40 | 41 | useEffect(() => { 42 | if (container === null) { 43 | return; 44 | } 45 | container.addEventListener('keydown', keyDownCallback); 46 | 47 | return () => { 48 | container.removeEventListener('keydown', keyDownCallback); 49 | }; 50 | }, [keyDownCallback, container]); 51 | }; 52 | -------------------------------------------------------------------------------- /intuita-webview/src/jobDiffView/index.css: -------------------------------------------------------------------------------- 1 | @import '../shared/util.css'; 2 | 3 | /** 4 | * App styles 5 | **/ 6 | 7 | #root { 8 | position: absolute; 9 | width: 100%; 10 | height: 100%; 11 | } 12 | 13 | html { 14 | overflow: hidden; 15 | } 16 | -------------------------------------------------------------------------------- /intuita-webview/src/jobDiffView/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Intuita 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /intuita-webview/src/jobDiffView/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import '../shared/index.css'; 4 | import { App } from './App'; 5 | 6 | import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; 7 | 8 | import 'monaco-editor/esm/vs/language/typescript/monaco.contribution'; 9 | import 'monaco-editor/esm/vs/language/css/monaco.contribution'; 10 | import 'monaco-editor/esm/vs/language/json/monaco.contribution'; 11 | import 'monaco-editor/esm/vs/language/html/monaco.contribution'; 12 | 13 | import 'monaco-editor/esm/vs/basic-languages/monaco.contribution'; 14 | 15 | import { loader } from '@monaco-editor/react'; 16 | 17 | loader.config({ monaco }); 18 | 19 | const root = ReactDOM.createRoot( 20 | document.getElementById('root') as HTMLElement, 21 | ); 22 | 23 | root.render( 24 | 25 | 26 | , 27 | ); 28 | -------------------------------------------------------------------------------- /intuita-webview/src/jobDiffView/style.module.css: -------------------------------------------------------------------------------- 1 | .app { 2 | height: 100%; 3 | width: 100%; 4 | position: relative; 5 | background-color: var(--vscode-textCodeBlock-background); 6 | } 7 | 8 | .markdownContainer { 9 | height: 100%; 10 | position: relative; 11 | background-color: var(--vscode-textCodeBlock-background); 12 | padding-left: 2em; 13 | padding-right: 2em; 14 | overflow-y: scroll; 15 | } 16 | -------------------------------------------------------------------------------- /intuita-webview/src/jobDiffView/util.ts: -------------------------------------------------------------------------------- 1 | import { JobHash } from '../shared/types'; 2 | import { vscode } from '../shared/utilities/vscode'; 3 | 4 | export const reportIssue = ( 5 | faultyJobHash: JobHash, 6 | oldFileContent: string, 7 | newFileContent: string, 8 | modifiedFileContent: string | null, 9 | ) => { 10 | vscode.postMessage({ 11 | kind: 'webview.global.openIssueCreation', 12 | faultyJobHash, 13 | oldFileContent, 14 | newFileContent, 15 | modifiedFileContent, 16 | }); 17 | }; 18 | 19 | export const exportToCodemodStudio = ( 20 | faultyJobHash: JobHash, 21 | oldFileContent: string, 22 | newFileContent: string, 23 | ) => { 24 | vscode.postMessage({ 25 | kind: 'webview.global.exportToCodemodStudio', 26 | faultyJobHash, 27 | oldFileContent, 28 | newFileContent, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /intuita-webview/src/main/App.css: -------------------------------------------------------------------------------- 1 | .block { 2 | width: 100%; 3 | height: 25%; 4 | } 5 | 6 | .App { 7 | height: 100%; 8 | width: 100%; 9 | } 10 | 11 | #root { 12 | height: 100vh; 13 | } 14 | 15 | .resize-handle { 16 | height: 3px; 17 | background-color: transparent; 18 | } 19 | 20 | .resize-handle:hover, 21 | .resize-handle:active { 22 | background-color: var(--vscode-focusBorder); 23 | } 24 | 25 | .vscode-panels { 26 | overflow: hidden; 27 | } 28 | 29 | .vscode-panels::part(tabpanel) { 30 | height: 100%; 31 | overflow: hidden; 32 | } 33 | 34 | .vscode-panels::part(tablist) { 35 | min-height: 35px; 36 | column-gap: 10px; 37 | padding: 0 2px; 38 | } 39 | 40 | .vscode-tab { 41 | overflow: hidden; 42 | text-overflow: ellipsis; 43 | white-space: nowrap; 44 | display: flex; 45 | padding: 0 2px; 46 | text-transform: uppercase; 47 | font-size: 11px; 48 | } 49 | 50 | .vscode-tab:focus-visible { 51 | border: none; 52 | } 53 | 54 | .vscode-panel-view { 55 | padding: 0px; 56 | } 57 | 58 | .toasterComponent { 59 | display: flex; 60 | flex-direction: column; 61 | align-items: center; 62 | } 63 | 64 | .clearing-progress-ring { 65 | height: 4em; 66 | width: 100%; 67 | } 68 | 69 | .warning { 70 | font-size: var(--type-ramp-base-font-size); 71 | line-height: var(--type-ramp-base-line-height); 72 | display: block; 73 | color: var(--foreground); 74 | } 75 | -------------------------------------------------------------------------------- /intuita-webview/src/main/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Intuita 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /intuita-webview/src/main/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | 4 | import App from './App'; 5 | 6 | import '../shared/index.css'; 7 | import '../shared/util.css'; 8 | import 'tippy.js/dist/tippy.css'; 9 | import './App.css'; 10 | 11 | const root = ReactDOM.createRoot( 12 | document.getElementById('root') as HTMLElement, 13 | ); 14 | 15 | root.render( 16 | 17 | 18 | , 19 | ); 20 | -------------------------------------------------------------------------------- /intuita-webview/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/IntuitaPopover/index.tsx: -------------------------------------------------------------------------------- 1 | import Tippy, { TippyProps } from '@tippyjs/react'; 2 | 3 | type Props = TippyProps; 4 | 5 | const IntuitaPopover = ({ delay = [800, 100], ...others }: Props) => { 6 | return ( 7 | 14 | ); 15 | }; 16 | 17 | export default IntuitaPopover; 18 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/Panel.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, forwardRef, useEffect, useRef } from 'react'; 2 | import { 3 | Panel as RResizablePanel, 4 | PanelGroup as RResizablePanelGroup, 5 | ImperativePanelHandle, 6 | PanelProps, 7 | PanelGroupProps, 8 | } from 'react-resizable-panels'; 9 | 10 | type ResizablePanelProps = { 11 | children?: ReactNode; 12 | defaultSize: number; 13 | minSize: number; 14 | collapsible?: boolean; 15 | className?: string; 16 | } & PanelProps; 17 | 18 | const PanelGroup = (props: PanelGroupProps) => { 19 | const isResizingRef = useRef(false); 20 | const containerRef = useRef(null); 21 | 22 | useEffect(() => { 23 | const onStartResizing = (e: MouseEvent) => { 24 | if ( 25 | (e.target as HTMLDivElement | null)?.getAttribute( 26 | 'data-panel-resize-handle-id', 27 | ) === undefined 28 | ) { 29 | return; 30 | } 31 | 32 | isResizingRef.current = true; 33 | }; 34 | 35 | const onEndResizing = () => { 36 | isResizingRef.current = false; 37 | }; 38 | 39 | const onResize = (e: MouseEvent) => { 40 | if (isResizingRef.current === false) { 41 | e.stopPropagation(); 42 | } 43 | }; 44 | 45 | if (containerRef.current === null) { 46 | return; 47 | } 48 | 49 | containerRef.current.addEventListener('mousedown', onStartResizing); 50 | 51 | containerRef.current.addEventListener('mouseup', onEndResizing); 52 | containerRef.current.addEventListener('contextmenu', onEndResizing); 53 | 54 | containerRef.current.addEventListener('mousemove', onResize); 55 | 56 | return () => { 57 | if (containerRef.current === null) { 58 | return; 59 | } 60 | 61 | containerRef.current.removeEventListener( 62 | 'mousedown', 63 | onStartResizing, 64 | ); 65 | 66 | containerRef.current.removeEventListener('mouseup', onEndResizing); 67 | containerRef.current.removeEventListener( 68 | 'contextmenu', 69 | onEndResizing, 70 | ); 71 | 72 | // eslint-disable-next-line react-hooks/exhaustive-deps 73 | containerRef.current.removeEventListener('mousemove', onResize); 74 | }; 75 | }, []); 76 | 77 | return ( 78 |
79 | 80 | {props.children} 81 | 82 |
83 | ); 84 | }; 85 | 86 | const ResizablePanel = forwardRef( 87 | (props, ref) => { 88 | const { 89 | children, 90 | defaultSize, 91 | minSize, 92 | collapsible, 93 | className, 94 | ...rest 95 | } = props; 96 | return ( 97 | 105 |
{children}
106 |
107 | ); 108 | }, 109 | ); 110 | 111 | export { ResizablePanel, PanelGroup }; 112 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/Progress/index.tsx: -------------------------------------------------------------------------------- 1 | import { VSCodeProgressRing } from '@vscode/webview-ui-toolkit/react'; 2 | 3 | import s from './style.module.css'; 4 | 5 | const Progress = () => { 6 | return ( 7 |
8 | 9 | Loading... 10 |
11 | ); 12 | }; 13 | 14 | export default Progress; 15 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/Progress/style.module.css: -------------------------------------------------------------------------------- 1 | .progressBar { 2 | margin-right: 10px; 3 | } 4 | 5 | .loadingContainer { 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | padding-top: 10%; 10 | } 11 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/SearchBar/index.tsx: -------------------------------------------------------------------------------- 1 | import { VSCodeTextField } from '@vscode/webview-ui-toolkit/react'; 2 | import cn from 'classnames'; 3 | import styles from './style.module.css'; 4 | 5 | type Props = Readonly<{ 6 | searchPhrase: string; 7 | setSearchPhrase: (searchPhrase: string) => void; 8 | placeholder: string; 9 | }>; 10 | 11 | export const SEARCH_QUERY_MIN_LENGTH = 1; 12 | 13 | const SearchBar = (props: Props) => { 14 | return ( 15 | { 20 | if ( 21 | event.target === null || 22 | !('value' in event.target) || 23 | typeof event.target.value !== 'string' 24 | ) { 25 | return; 26 | } 27 | 28 | props.setSearchPhrase(event.target.value); 29 | }} 30 | className={cn(styles.container, 'w-full')} 31 | > 32 | 37 | 38 | ); 39 | }; 40 | 41 | export default SearchBar; 42 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/SearchBar/style.module.css: -------------------------------------------------------------------------------- 1 | .container:focus { 2 | outline: none; 3 | } 4 | 5 | .container::part(start) { 6 | margin-inline-start: 2px; 7 | } 8 | 9 | .container::part(control) { 10 | padding: 0 4px; 11 | } 12 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/SectionHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames'; 2 | import s from './style.module.css'; 3 | import { Command } from 'vscode'; 4 | import { vscode } from '../utilities/vscode'; 5 | import { CSSProperties } from 'react'; 6 | 7 | const handleCommand = (value: Command) => { 8 | vscode.postMessage({ 9 | kind: 'webview.command', 10 | value, 11 | }); 12 | }; 13 | 14 | export const SectionHeader = ( 15 | props: Readonly<{ 16 | title: string; 17 | collapsed: boolean; 18 | commands: ReadonlyArray; 19 | onClick: React.MouseEventHandler; 20 | style?: CSSProperties; 21 | }>, 22 | ) => { 23 | return ( 24 |
29 | 38 | {props.title} 39 |
40 | {props.commands.map((c) => { 41 | return ( 42 | { 50 | e.stopPropagation(); 51 | handleCommand(c); 52 | }} 53 | /> 54 | ); 55 | })} 56 |
57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/SectionHeader/style.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | overflow: hidden; 6 | } 7 | 8 | .icon { 9 | width: 16px; 10 | height: 16px; 11 | margin: 0 2px; 12 | } 13 | 14 | .title { 15 | font-size: 11px; 16 | text-transform: uppercase; 17 | color: var(--vscode-sideBarSectionHeader-foreground); 18 | font-weight: 700; 19 | line-height: 22px; 20 | } 21 | 22 | .sectionHeader { 23 | display: flex; 24 | align-items: center; 25 | height: 22px; 26 | line-height: 22px; 27 | color: var(--vscode-sideBarSectionHeader-foreground); 28 | background-color: var(--vscode-sideBarSectionHeader-background); 29 | cursor: pointer; 30 | user-select: none; 31 | } 32 | 33 | .commands { 34 | margin-left: auto; 35 | margin-right: 4px; 36 | display: flex; 37 | align-items: center; 38 | } 39 | 40 | .content { 41 | flex-grow: 1; 42 | overflow-y: auto; 43 | } 44 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/Snippet/calculateDiff.tsx: -------------------------------------------------------------------------------- 1 | import type { editor } from 'monaco-editor'; 2 | 3 | export type Diff = { added: number; removed: number }; 4 | 5 | export const getDiff = (lineChanges: editor.ILineChange[]): Diff => { 6 | const diff: Diff = { 7 | added: 0, 8 | removed: 0, 9 | }; 10 | lineChanges.forEach((lineChange) => { 11 | if (lineChange.modifiedEndLineNumber !== 0) { 12 | diff.added += 13 | lineChange.modifiedEndLineNumber - 14 | lineChange.modifiedStartLineNumber + 15 | 1; 16 | } 17 | if (lineChange.originalEndLineNumber !== 0) { 18 | diff.removed += 19 | lineChange.originalEndLineNumber - 20 | lineChange.originalStartLineNumber + 21 | 1; 22 | } 23 | }); 24 | 25 | return diff; 26 | }; 27 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/Snippet/detectTheme.ts: -------------------------------------------------------------------------------- 1 | export function detectBaseTheme(): 'vs-light' | 'vs-dark' { 2 | const attribute = document.body.getAttribute('data-vscode-theme-kind'); 3 | if (attribute === 'vscode-dark' || attribute === 'vscode-high-contrast') { 4 | return 'vs-dark'; 5 | } 6 | 7 | return 'vs-light'; 8 | } 9 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/Snippet/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | /** 4 | * watch for a change in body [data-vscode-theme-kind] attribute and update the theme 5 | */ 6 | 7 | import { detectBaseTheme } from './detectTheme'; 8 | 9 | export const useTheme = () => { 10 | const [theme, setTheme] = useState(detectBaseTheme()); 11 | useEffect(() => { 12 | const observer = new MutationObserver((mutations) => { 13 | mutations.forEach((mutation) => { 14 | if ( 15 | mutation.type === 'attributes' && 16 | mutation.attributeName === 'data-vscode-theme-kind' 17 | ) { 18 | setTheme(detectBaseTheme()); 19 | } 20 | }); 21 | }); 22 | 23 | observer.observe(document.body, { 24 | attributes: true, 25 | }); 26 | 27 | return () => { 28 | observer.disconnect(); 29 | }; 30 | }, []); 31 | 32 | return theme; 33 | }; 34 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/TreeItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ReactNode, useLayoutEffect, useRef } from 'react'; 2 | import styles from './style.module.css'; 3 | import cn from 'classnames'; 4 | 5 | const getLabelComponent = ( 6 | label: string, 7 | searchPhrase: string, 8 | style?: CSSProperties, 9 | ) => { 10 | if ( 11 | searchPhrase.length >= 2 && 12 | label.toLowerCase().includes(searchPhrase.toLowerCase()) 13 | ) { 14 | const startIndex = label 15 | .toLowerCase() 16 | .indexOf(searchPhrase.toLowerCase()); 17 | const endIndex = startIndex + searchPhrase.length - 1; 18 | return ( 19 | 20 | {label.slice(0, startIndex)} 21 | 22 | {label.slice(startIndex, endIndex + 1)} 23 | 24 | {label.slice(endIndex + 1)} 25 | 26 | ); 27 | } 28 | 29 | return ( 30 | 31 | {label} 32 | 33 | ); 34 | }; 35 | 36 | export type Props = Readonly<{ 37 | id: string; 38 | label: string; 39 | open: boolean; 40 | focused: boolean; 41 | icon: ReactNode; 42 | hasChildren: boolean; 43 | onClick(event: React.MouseEvent): void; 44 | depth: number; 45 | indent: number; 46 | startDecorator?: ReactNode; 47 | endDecorator?: ReactNode; 48 | inlineStyles?: { 49 | root?: CSSProperties; 50 | icon?: CSSProperties; 51 | label?: CSSProperties; 52 | actions?: CSSProperties; 53 | }; 54 | onPressChevron?(event: React.MouseEvent): void; 55 | searchPhrase: string; 56 | }>; 57 | 58 | const TreeItem = ({ 59 | id, 60 | label, 61 | icon, 62 | open, 63 | focused, 64 | startDecorator, 65 | hasChildren, 66 | onClick, 67 | indent, 68 | inlineStyles, 69 | onPressChevron, 70 | endDecorator, 71 | searchPhrase, 72 | }: Props) => { 73 | const ref = useRef(null); 74 | useLayoutEffect(() => { 75 | if (focused) { 76 | const timeout = setTimeout(() => { 77 | ref.current?.scrollIntoView({ 78 | behavior: 'smooth', 79 | block: 'nearest', 80 | inline: 'center', 81 | }); 82 | }, 0); 83 | 84 | return () => { 85 | clearTimeout(timeout); 86 | }; 87 | } 88 | 89 | return () => {}; 90 | }, [focused]); 91 | 92 | return ( 93 |
101 |
106 | {hasChildren ? ( 107 | 114 | ) : null} 115 | {startDecorator} 116 | {icon !== null && ( 117 |
118 | {icon} 119 |
120 | )} 121 | {getLabelComponent(label, searchPhrase, inlineStyles?.label)} 122 | {endDecorator} 123 |
124 | ); 125 | }; 126 | 127 | export default TreeItem; 128 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/TreeItem/style.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | height: 22px; 4 | line-height: 22px; 5 | flex: 1; 6 | text-overflow: ellipsis; 7 | overflow: hidden; 8 | flex-wrap: nowrap; 9 | padding-left: 3px; 10 | padding-right: 12px; 11 | outline: none; 12 | align-items: center; 13 | } 14 | 15 | .root:hover { 16 | background-color: var(--vscode-list-hoverBackground); 17 | } 18 | 19 | .focused, 20 | .focused:hover { 21 | background-color: var(--vscode-list-activeSelectionBackground); 22 | } 23 | 24 | .label { 25 | line-height: 22px; 26 | font-size: 13px; 27 | cursor: pointer; 28 | flex-grow: 1; 29 | color: var(--vscode-foreground); 30 | text-overflow: ellipsis; 31 | overflow: hidden; 32 | white-space: nowrap; 33 | user-select: none; 34 | margin-left: 2.5px; 35 | } 36 | 37 | .subLabel { 38 | line-height: 20px; 39 | font-size: 11px; 40 | cursor: pointer; 41 | flex-grow: 1; 42 | color: var(--vscode-foreground); 43 | text-overflow: ellipsis; 44 | overflow: hidden; 45 | white-space: nowrap; 46 | user-select: none; 47 | text-align: right; 48 | } 49 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/WarningMessage/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import styles from './style.module.css'; 3 | 4 | type Props = { 5 | message: string; 6 | actionButtons: ReactNode; 7 | }; 8 | 9 | const WarningMessage = ({ message, actionButtons }: Props) => { 10 | return ( 11 |
12 |

{message}

13 | {actionButtons} 14 |
15 | ); 16 | }; 17 | 18 | export default WarningMessage; 19 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/WarningMessage/style.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 4px; 5 | align-items: center; 6 | row-gap: 4px; 7 | } 8 | 9 | .root p { 10 | text-align: center; 11 | } 12 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/constants.ts: -------------------------------------------------------------------------------- 1 | // Imported from /src/jobs/types.ts 2 | 3 | export const enum JobKind { 4 | rewriteFile = 1, 5 | createFile = 2, 6 | deleteFile = 3, 7 | moveFile = 4, 8 | moveAndRewriteFile = 5, 9 | copyFile = 6, 10 | } 11 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 4 | 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 5 | 'Helvetica Neue', sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | padding: 0; 9 | overflow: hidden; 10 | } 11 | 12 | code { 13 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 14 | monospace; 15 | } 16 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/types.ts: -------------------------------------------------------------------------------- 1 | export { 2 | WebviewMessage, 3 | Command, 4 | RunCodemodsCommand, 5 | CodemodHash, 6 | JobHash, 7 | } from '../../../src/components/webview/webviewEvents'; 8 | 9 | export type DiffViewType = 'inline' | 'side-by-side'; 10 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/util.css: -------------------------------------------------------------------------------- 1 | .flex { 2 | display: flex; 3 | } 4 | .gap-4 { 5 | column-gap: 4px; 6 | } 7 | 8 | .w-full { 9 | width: 100%; 10 | } 11 | 12 | .w-half { 13 | width: 50%; 14 | } 15 | 16 | .h-full { 17 | height: 100%; 18 | } 19 | 20 | .flex-col { 21 | flex-direction: column; 22 | } 23 | 24 | .flex-row { 25 | flex-direction: row; 26 | } 27 | .flex-wrap { 28 | flex-wrap: wrap; 29 | } 30 | .flex-nowrap { 31 | flex-wrap: nowrap; 32 | } 33 | 34 | .justify-center { 35 | justify-content: center; 36 | } 37 | 38 | .justify-start { 39 | justify-content: flex-start; 40 | } 41 | 42 | .justify-end { 43 | justify-content: flex-end; 44 | } 45 | 46 | .justify-between { 47 | justify-content: space-between; 48 | } 49 | 50 | .align-items-center { 51 | align-items: center; 52 | } 53 | .align-self-center { 54 | align-self: center; 55 | } 56 | .flex-1 { 57 | flex-grow: 1; 58 | } 59 | 60 | .text-center { 61 | text-align: center; 62 | } 63 | .p-3 { 64 | padding: 3px; 65 | } 66 | .p-10 { 67 | padding: 10px; 68 | } 69 | .px-5 { 70 | padding: 5px; 71 | } 72 | .py-2-5 { 73 | padding-top: 2.5px; 74 | padding-bottom: 2.5px; 75 | } 76 | .pb-2-5 { 77 | padding-bottom: 2.5px; 78 | } 79 | .px-10 { 80 | padding-left: 10px; 81 | padding-right: 10px; 82 | } 83 | .py-10 { 84 | padding-top: 10px; 85 | padding-bottom: 10px; 86 | } 87 | .pb-10 { 88 | padding-bottom: 10px; 89 | } 90 | 91 | .my-0 { 92 | margin-top: 0; 93 | margin-bottom: 0; 94 | } 95 | .ml-3 { 96 | margin-left: 3px; 97 | } 98 | .ml-10 { 99 | margin-left: 10px; 100 | } 101 | .my-10 { 102 | margin-top: 10px; 103 | margin-bottom: 10px; 104 | } 105 | .mb-10 { 106 | margin-bottom: 10px; 107 | } 108 | .m-10 { 109 | margin: 10px; 110 | } 111 | .mr-10 { 112 | margin-right: 10px; 113 | } 114 | .mr-2 { 115 | margin-right: 2px; 116 | } 117 | .mt-2 { 118 | margin-top: 2px; 119 | } 120 | .mb-2 { 121 | margin-bottom: 2px; 122 | } 123 | .mt-10 { 124 | margin-top: 10px; 125 | } 126 | .ml-50 { 127 | margin-left: 50px; 128 | } 129 | 130 | .rounded { 131 | border-radius: 5px; 132 | } 133 | 134 | .mt-0 { 135 | margin-top: 0; 136 | } 137 | 138 | .right-0 { 139 | right: 0; 140 | } 141 | 142 | .top-0 { 143 | top: 0; 144 | } 145 | .absolute { 146 | position: absolute; 147 | } 148 | .text-xl { 149 | font-size: 1rem; 150 | } 151 | 152 | .bold { 153 | font-weight: bold; 154 | } 155 | 156 | .cursor-pointer { 157 | cursor: pointer; 158 | } 159 | 160 | .pointer-events-none { 161 | pointer-events: none; 162 | } 163 | 164 | .user-select-none { 165 | user-select: none; 166 | } 167 | 168 | .relative { 169 | position: relative; 170 | } 171 | 172 | .overflow-hidden { 173 | overflow: hidden; 174 | } 175 | 176 | .overflow-y-auto { 177 | overflow-y: auto; 178 | } 179 | 180 | .codicon, 181 | .defaultIcon { 182 | width: 16px; 183 | height: 16px; 184 | display: flex; 185 | align-items: center; 186 | justify-content: center; 187 | cursor: inherit; 188 | padding: 0 0.5px; 189 | } 190 | 191 | .codicon svg, 192 | .defaultIcon svg { 193 | width: 100%; 194 | height: 100%; 195 | } 196 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/utilities/debounce.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | const debounce = (callback: (...args: any[]) => R, ms: number) => { 3 | let timeout: ReturnType | null = null; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | return (...args: any[]) => { 7 | if (timeout !== null) { 8 | clearTimeout(timeout); 9 | } 10 | 11 | timeout = setTimeout(() => callback(...args), ms); 12 | }; 13 | }; 14 | 15 | export default debounce; 16 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/utilities/throttle.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | const throttle = (callback: (...args: any[]) => R, ms: number) => { 3 | let timeout: ReturnType | null = null; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | return (...args: any[]) => { 7 | if (timeout) { 8 | return; 9 | } 10 | 11 | callback(...args); 12 | timeout = setTimeout(() => { 13 | callback(...args); 14 | timeout = null; 15 | }, ms); 16 | }; 17 | }; 18 | 19 | export default throttle; 20 | -------------------------------------------------------------------------------- /intuita-webview/src/shared/utilities/vscode.ts: -------------------------------------------------------------------------------- 1 | import type { WebviewApi } from 'vscode-webview'; 2 | import { WebviewResponse } from '../../../../src/components/webview/webviewEvents'; 3 | 4 | class VSCodeAPIWrapper { 5 | private readonly vsCodeApi: WebviewApi | undefined; 6 | 7 | constructor() { 8 | if (typeof acquireVsCodeApi === 'function') { 9 | this.vsCodeApi = acquireVsCodeApi(); 10 | } 11 | } 12 | 13 | public postMessage(message: WebviewResponse) { 14 | if (this.vsCodeApi) { 15 | this.vsCodeApi.postMessage(message); 16 | } else { 17 | console.log(message); 18 | } 19 | } 20 | 21 | public getState(): unknown | undefined { 22 | if (this.vsCodeApi) { 23 | return this.vsCodeApi.getState(); 24 | } else { 25 | const state = localStorage.getItem('vscodeState'); 26 | return state ? JSON.parse(state) : undefined; 27 | } 28 | } 29 | 30 | public setState(newState: T): T { 31 | if (this.vsCodeApi) { 32 | return this.vsCodeApi.setState(newState); 33 | } else { 34 | localStorage.setItem('vscodeState', JSON.stringify(newState)); 35 | return newState; 36 | } 37 | } 38 | } 39 | 40 | export const vscode = new VSCodeAPIWrapper(); 41 | -------------------------------------------------------------------------------- /intuita-webview/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "lib": ["dom", "dom.iterable", "ESNext"], 6 | "types": ["vite/client", "vite-plugin-svgr/client"] 7 | }, 8 | "include": ["./src/**/*"], 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /intuita-webview/vite.config.dev.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { defineConfig } from 'vite'; 3 | import react from '@vitejs/plugin-react'; 4 | import viteTsconfigPaths from 'vite-tsconfig-paths'; 5 | import svgrPlugin from 'vite-plugin-svgr'; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | resolve: { 10 | alias: { 11 | react: 'preact/compat', 12 | 'react-dom': 'preact/compat', 13 | }, 14 | }, 15 | build: { 16 | assetsInlineLimit: 10000, 17 | }, 18 | define: { 19 | 'process.env': {}, 20 | }, 21 | plugins: [react(), viteTsconfigPaths(), svgrPlugin()], 22 | }); 23 | -------------------------------------------------------------------------------- /intuita-webview/vite.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { fileURLToPath } from 'url'; 3 | import { defineConfig } from 'vite'; 4 | import react from '@vitejs/plugin-react'; 5 | import viteTsconfigPaths from 'vite-tsconfig-paths'; 6 | import svgrPlugin from 'vite-plugin-svgr'; 7 | import monacoEditorPlugin from 'vite-plugin-monaco-editor'; 8 | 9 | const target = process.env.TARGET_APP ?? ''; 10 | 11 | // https://vitejs.dev/config/ 12 | export default defineConfig({ 13 | resolve: { 14 | alias: { 15 | react: 'preact/compat', 16 | 'react-dom': 'preact/compat', 17 | }, 18 | }, 19 | build: { 20 | assetsInlineLimit: 10000, 21 | outDir: `build/${target}`, 22 | rollupOptions: { 23 | input: { 24 | [target]: fileURLToPath( 25 | new URL(`./src/${target}/index.html`, import.meta.url), 26 | ), 27 | }, 28 | output: { 29 | entryFileNames: `assets/[name].js`, 30 | chunkFileNames: `assets/[name].js`, 31 | assetFileNames: `assets/[name].[ext]`, 32 | }, 33 | }, 34 | }, 35 | define: { 36 | 'process.env': {}, 37 | }, 38 | plugins: [ 39 | react(), 40 | viteTsconfigPaths(), 41 | svgrPlugin(), 42 | monacoEditorPlugin({}), 43 | ], 44 | }); 45 | -------------------------------------------------------------------------------- /resources/codicon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codemod-com/intuita-vscode-extension/cc9b2e9fc70e894b3e92dcd48a980476dca1652d/resources/codicon.ttf -------------------------------------------------------------------------------- /src/axios/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import axiosRetry from 'axios-retry'; 3 | 4 | const retryingClient = axios.create(); 5 | axiosRetry(retryingClient); 6 | 7 | export const DEFAULT_RETRY_COUNT = 3; 8 | export { retryingClient }; 9 | -------------------------------------------------------------------------------- /src/cases/caseManager.ts: -------------------------------------------------------------------------------- 1 | import { Message, MessageBus, MessageKind } from '../components/messageBus'; 2 | import { Store } from '../data'; 3 | import { actions } from '../data/slice'; 4 | import { JobHash } from '../jobs/types'; 5 | import { LeftRightHashSetManager } from '../leftRightHashes/leftRightHashSetManager'; 6 | import { isNeitherNullNorUndefined } from '../utilities'; 7 | import { CaseHash } from './types'; 8 | 9 | export class CaseManager { 10 | public constructor( 11 | private readonly __messageBus: MessageBus, 12 | private readonly __store: Store, 13 | ) { 14 | this.__messageBus.subscribe(MessageKind.upsertCase, (message) => 15 | this.#onUpsertCasesMessage(message), 16 | ); 17 | this.__messageBus.subscribe(MessageKind.acceptCase, (message) => 18 | this.#onAcceptCaseMessage(message), 19 | ); 20 | this.__messageBus.subscribe(MessageKind.rejectCase, (message) => 21 | this.#onRejectCaseMessage(message), 22 | ); 23 | this.__messageBus.subscribe(MessageKind.jobsAccepted, (message) => 24 | this.#onJobsAcceptedOrJobsRejectedMessage(message), 25 | ); 26 | this.__messageBus.subscribe(MessageKind.jobsRejected, (message) => 27 | this.#onJobsAcceptedOrJobsRejectedMessage(message), 28 | ); 29 | } 30 | 31 | #onUpsertCasesMessage(message: Message & { kind: MessageKind.upsertCase }) { 32 | const caseHashJobHashes = message.jobs.map( 33 | ({ hash }) => `${message.kase.hash}${hash}`, 34 | ); 35 | 36 | this.__store.dispatch( 37 | actions.upsertCase([message.kase, caseHashJobHashes]), 38 | ); 39 | 40 | this.__messageBus.publish({ 41 | kind: MessageKind.upsertJobs, 42 | jobs: message.jobs, 43 | }); 44 | } 45 | 46 | #onAcceptCaseMessage(message: Message & { kind: MessageKind.acceptCase }) { 47 | const state = this.__store.getState(); 48 | 49 | if (!state.case.ids.includes(message.caseHash)) { 50 | throw new Error('You tried to accept a case that does not exist.'); 51 | } 52 | 53 | const caseHashJobHashSetManager = new LeftRightHashSetManager< 54 | CaseHash, 55 | JobHash 56 | >(new Set(state.caseHashJobHashes)); 57 | 58 | // we are not removing cases and jobs here 59 | // we wait for the jobs accepted message for data removal 60 | const jobHashes = caseHashJobHashSetManager.getRightHashesByLeftHash( 61 | message.caseHash, 62 | ); 63 | 64 | this.__messageBus.publish({ 65 | kind: MessageKind.acceptJobs, 66 | jobHashes, 67 | }); 68 | } 69 | 70 | #onJobsAcceptedOrJobsRejectedMessage( 71 | message: Message & { 72 | kind: MessageKind.jobsAccepted | MessageKind.jobsRejected; 73 | }, 74 | ) { 75 | const state = this.__store.getState(); 76 | 77 | const cases = Object.values(state.case.entities).filter( 78 | isNeitherNullNorUndefined, 79 | ); 80 | 81 | const caseHashJobHashSetManager = new LeftRightHashSetManager< 82 | CaseHash, 83 | JobHash 84 | >(new Set(state.caseHashJobHashes)); 85 | 86 | const removableCaseHashes: CaseHash[] = []; 87 | 88 | for (const kase of cases) { 89 | const caseJobHashes = 90 | caseHashJobHashSetManager.getRightHashesByLeftHash(kase.hash); 91 | 92 | let deletedCount = 0; 93 | 94 | for (const job of message.deletedJobs) { 95 | const deleted = caseHashJobHashSetManager.delete( 96 | kase.hash, 97 | job.hash, 98 | ); 99 | 100 | deletedCount += Number(deleted); 101 | } 102 | 103 | if (caseJobHashes.size <= deletedCount) { 104 | removableCaseHashes.push(kase.hash); 105 | } 106 | } 107 | 108 | this.__store.dispatch(actions.removeCases(removableCaseHashes)); 109 | } 110 | 111 | #onRejectCaseMessage(message: Message & { kind: MessageKind.rejectCase }) { 112 | const state = this.__store.getState(); 113 | 114 | const caseHashJobHashSetManager = new LeftRightHashSetManager< 115 | CaseHash, 116 | JobHash 117 | >(new Set(state.caseHashJobHashes)); 118 | 119 | const jobHashes = caseHashJobHashSetManager.getRightHashesByLeftHash( 120 | message.caseHash, 121 | ); 122 | 123 | this.__store.dispatch(actions.removeCases([message.caseHash])); 124 | 125 | this.__messageBus.publish({ 126 | kind: MessageKind.rejectJobs, 127 | jobHashes, 128 | }); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/cases/types.ts: -------------------------------------------------------------------------------- 1 | import { withFallback } from 'io-ts-types'; 2 | import { buildTypeCodec } from '../utilities'; 3 | import * as t from 'io-ts'; 4 | 5 | export interface CaseHashBrand { 6 | readonly __CaseHash: unique symbol; 7 | } 8 | 9 | export const caseHashCodec = t.brand( 10 | t.string, 11 | (hashDigest): hashDigest is t.Branded => 12 | hashDigest.length > 0, 13 | '__CaseHash', 14 | ); 15 | 16 | export type CaseHash = t.TypeOf; 17 | 18 | export const caseCodec = buildTypeCodec({ 19 | hash: caseHashCodec, 20 | codemodName: t.string, // deprecated 21 | codemodHashDigest: withFallback( 22 | t.union([t.string, t.undefined]), 23 | undefined, 24 | ), 25 | createdAt: t.number, 26 | path: t.string, 27 | }); 28 | 29 | export type Case = t.TypeOf; 30 | -------------------------------------------------------------------------------- /src/codemods/types.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { buildTypeCodec } from '../utilities'; 3 | import { withFallback } from 'io-ts-types'; 4 | 5 | export const argumentsCodec = t.union([ 6 | t.readonlyArray( 7 | t.union([ 8 | buildTypeCodec({ 9 | name: t.string, 10 | kind: t.literal('string'), 11 | default: t.union([t.string, t.undefined]), 12 | description: withFallback(t.string, ''), 13 | required: withFallback(t.boolean, false), 14 | }), 15 | buildTypeCodec({ 16 | name: t.string, 17 | kind: t.literal('number'), 18 | default: t.union([t.number, t.undefined]), 19 | description: withFallback(t.string, ''), 20 | required: withFallback(t.boolean, false), 21 | }), 22 | buildTypeCodec({ 23 | name: t.string, 24 | kind: t.literal('boolean'), 25 | default: t.union([t.boolean, t.undefined]), 26 | description: withFallback(t.string, ''), 27 | required: withFallback(t.boolean, false), 28 | }), 29 | ]), 30 | ), 31 | t.undefined, 32 | ]); 33 | 34 | export const codemodEntryCodec = t.union([ 35 | buildTypeCodec({ 36 | kind: t.literal('codemod'), 37 | hashDigest: t.string, 38 | name: t.string, 39 | engine: t.union([ 40 | t.literal('jscodeshift'), 41 | t.literal('ts-morph'), 42 | t.literal('repomod-engine'), 43 | t.literal('filemod'), 44 | t.literal('recipe'), 45 | ]), 46 | arguments: argumentsCodec, 47 | }), 48 | buildTypeCodec({ 49 | kind: t.literal('piranhaRule'), 50 | hashDigest: t.string, 51 | name: t.string, 52 | // TODO migrate to @effect/schema once all the codecs are migrated 53 | language: t.union([ 54 | t.literal('java'), 55 | t.literal('kt'), 56 | t.literal('go'), 57 | t.literal('py'), 58 | t.literal('swift'), 59 | t.literal('ts'), 60 | t.literal('tsx'), 61 | t.literal('scala'), 62 | ]), 63 | arguments: argumentsCodec, 64 | }), 65 | ]); 66 | 67 | export const privateCodemodEntryCodec = buildTypeCodec({ 68 | kind: t.literal('codemod'), 69 | hashDigest: t.string, 70 | name: t.string, 71 | engine: t.union([ 72 | t.literal('jscodeshift'), 73 | t.literal('ts-morph'), 74 | t.literal('repomod-engine'), 75 | t.literal('filemod'), 76 | t.literal('recipe'), 77 | ]), 78 | permalink: t.union([t.string, t.null]), 79 | }); 80 | 81 | export type CodemodEntry = t.TypeOf; 82 | 83 | export const codemodNamesCodec = buildTypeCodec({ 84 | kind: t.literal('names'), 85 | names: t.readonlyArray(t.string), 86 | }); 87 | 88 | export type Arguments = t.TypeOf; 89 | export type CodemodNames = t.TypeOf; 90 | export type PrivateCodemodEntry = t.TypeOf; 91 | -------------------------------------------------------------------------------- /src/commands/clearStateCommand.ts: -------------------------------------------------------------------------------- 1 | import { FileType, Uri, workspace } from 'vscode'; 2 | import { actions } from '../data/slice'; 3 | import { doesJobAddNewFile } from '../selectors/comparePersistedJobs'; 4 | import { Store } from '../data'; 5 | import { FileService } from '../components/fileService'; 6 | import { homedir } from 'node:os'; 7 | import { join } from 'node:path'; 8 | 9 | type Dependencies = Readonly<{ 10 | store: Store; 11 | fileService: FileService; 12 | }>; 13 | 14 | export const createClearStateCommand = 15 | ({ fileService, store }: Dependencies) => 16 | async () => { 17 | const state = store.getState(); 18 | 19 | store.dispatch(actions.clearState()); 20 | 21 | try { 22 | const uris: Uri[] = []; 23 | 24 | for (const job of Object.values(state.job.entities)) { 25 | if ( 26 | !job || 27 | !doesJobAddNewFile(job.kind) || 28 | job.newContentUri === null || 29 | job.newContentUri.includes('.intuita/cases') 30 | ) { 31 | continue; 32 | } 33 | 34 | uris.push(Uri.parse(job.newContentUri)); 35 | } 36 | 37 | await fileService.deleteFiles({ uris }); 38 | } catch (error) { 39 | console.error(error); 40 | } 41 | 42 | try { 43 | const casesDirectoryUri = Uri.parse( 44 | join(homedir(), '.intuita', 'cases'), 45 | ); 46 | 47 | const files = await workspace.fs.readDirectory(casesDirectoryUri); 48 | 49 | const caseDirectoryUris = files 50 | .filter(([, fileType]) => fileType === FileType.Directory) 51 | .map(([name]) => Uri.joinPath(casesDirectoryUri, name)); 52 | 53 | await fileService.deleteDirectories({ uris: caseDirectoryUris }); 54 | } catch (error) { 55 | console.error(error); 56 | } 57 | 58 | store.dispatch(actions.onStateCleared()); 59 | }; 60 | -------------------------------------------------------------------------------- /src/components/bootstrapExecutablesService.ts: -------------------------------------------------------------------------------- 1 | import { FileSystem, Uri, window } from 'vscode'; 2 | import { DownloadService, ForbiddenRequestError } from './downloadService'; 3 | import { MessageBus, MessageKind } from './messageBus'; 4 | import { Telemetry } from '../telemetry/telemetry'; 5 | 6 | // aka bootstrap engines 7 | export class BootstrapExecutablesService { 8 | constructor( 9 | private readonly __downloadService: DownloadService, 10 | private readonly __globalStorageUri: Uri, 11 | private readonly __fileSystem: FileSystem, 12 | private readonly __messageBus: MessageBus, 13 | private readonly __telemetryService: Telemetry, 14 | ) { 15 | __messageBus.subscribe(MessageKind.bootstrapEngine, () => 16 | this.__onBootstrapEngines(), 17 | ); 18 | } 19 | 20 | private async __onBootstrapEngines() { 21 | await this.__fileSystem.createDirectory(this.__globalStorageUri); 22 | 23 | try { 24 | // Uri.file('/intuita/nora-node-engine/package/intuita-linux') 25 | const codemodEngineNodeExecutableUri = 26 | await this.__bootstrapCodemodEngineNodeExecutableUri(); 27 | 28 | // Uri.file('/intuita/codemod-engine-rust/target/release/codemod-engine-rust'); 29 | const codemodEngineRustExecutableUri = 30 | await this.__bootstrapCodemodEngineRustExecutableUri(); 31 | 32 | this.__messageBus.publish({ 33 | kind: MessageKind.engineBootstrapped, 34 | codemodEngineNodeExecutableUri, 35 | codemodEngineRustExecutableUri, 36 | }); 37 | } catch (e) { 38 | const message = e instanceof Error ? e.message : String(e); 39 | 40 | window.showErrorMessage(message); 41 | 42 | this.__telemetryService.sendError({ 43 | kind: 'failedToBootstrapEngines', 44 | message, 45 | }); 46 | } 47 | } 48 | 49 | private async __bootstrapCodemodEngineNodeExecutableUri(): Promise { 50 | const platform = 51 | process.platform === 'darwin' 52 | ? 'macos' 53 | : process.platform === 'win32' 54 | ? 'win' 55 | : encodeURIComponent(process.platform); 56 | 57 | const executableBaseName = `intuita-${platform}`; 58 | const executableExt = process.platform === 'win32' ? '.exe' : ''; 59 | const executableName = `${executableBaseName}${executableExt}`; 60 | 61 | const executableUri = Uri.joinPath( 62 | this.__globalStorageUri, 63 | executableName, 64 | ); 65 | 66 | try { 67 | await this.__downloadService.downloadFileIfNeeded( 68 | `https://intuita-public.s3.us-west-1.amazonaws.com/intuita/${executableName}`, 69 | executableUri, 70 | '755', 71 | ); 72 | } catch (error) { 73 | if (!(error instanceof ForbiddenRequestError)) { 74 | throw error; 75 | } 76 | 77 | throw new Error( 78 | `Your platform (${process.platform}) is not supported.`, 79 | ); 80 | } 81 | 82 | return executableUri; 83 | } 84 | 85 | private async __bootstrapCodemodEngineRustExecutableUri(): Promise { 86 | const platform = 87 | process.platform === 'darwin' 88 | ? 'macos' 89 | : encodeURIComponent(process.platform); 90 | 91 | if (platform === 'win32') { 92 | return null; 93 | } 94 | 95 | const executableBaseName = `codemod-engine-rust-${platform}`; 96 | 97 | const executableUri = Uri.joinPath( 98 | this.__globalStorageUri, 99 | executableBaseName, 100 | ); 101 | 102 | try { 103 | await this.__downloadService.downloadFileIfNeeded( 104 | `https://intuita-public.s3.us-west-1.amazonaws.com/codemod-engine-rust/${executableBaseName}`, 105 | executableUri, 106 | '755', 107 | ); 108 | } catch (error) { 109 | if (!(error instanceof ForbiddenRequestError)) { 110 | throw error; 111 | } 112 | 113 | throw new Error( 114 | `Your platform (${process.platform}) is not supported.`, 115 | ); 116 | } 117 | 118 | return executableUri; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/components/buildArguments.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | import type { Configuration } from '../configuration'; 3 | import type { Message, MessageKind } from './messageBus'; 4 | import { buildCrossplatformArg } from '../utilities'; 5 | import { sep } from 'path'; 6 | 7 | const buildGlobPattern = (targetUri: Uri, pattern?: string) => { 8 | const { fsPath: targetUriFsPath } = targetUri; 9 | 10 | // Glob patterns should always use / as a path separator, even on Windows systems, as \ is used to escape glob characters. 11 | const pathParts = targetUriFsPath.split(sep); 12 | 13 | pathParts.push(pattern ?? ''); 14 | 15 | return pathParts.join('/'); 16 | }; 17 | 18 | export const buildArguments = ( 19 | configuration: Configuration, 20 | message: Omit< 21 | Message & { kind: MessageKind.executeCodemodSet }, 22 | 'storageUri' 23 | >, 24 | storageUri: Uri, 25 | ) => { 26 | const { command } = message; 27 | const args: string[] = []; 28 | 29 | const codemodArguments = 30 | command.kind !== 'executeLocalCodemod' 31 | ? (command.arguments ?? []).flatMap(({ name, value }) => [ 32 | `--arg:${name}`, 33 | String(value), 34 | ]) 35 | : []; 36 | 37 | if (command.kind === 'executePiranhaRule') { 38 | args.push('-i', buildCrossplatformArg(message.targetUri.fsPath)); 39 | args.push('-c', buildCrossplatformArg(command.configurationUri.fsPath)); 40 | args.push('-o', buildCrossplatformArg(storageUri.fsPath)); 41 | args.push('-l', command.language); 42 | args.push(...codemodArguments); 43 | return args; 44 | } 45 | 46 | if (command.kind === 'executeCodemod') { 47 | args.push(buildCrossplatformArg(command.name)); 48 | } else { 49 | args.push( 50 | '--sourcePath', 51 | buildCrossplatformArg(command.codemodUri.fsPath), 52 | ); 53 | args.push('--codemodEngine', 'jscodeshift'); 54 | } 55 | 56 | args.push('--targetPath', buildCrossplatformArg(message.targetUri.fsPath)); 57 | 58 | if (message.targetUriIsDirectory) { 59 | configuration.includePatterns.forEach((includePattern) => { 60 | args.push( 61 | '--include', 62 | buildCrossplatformArg( 63 | buildGlobPattern(message.targetUri, includePattern), 64 | ), 65 | ); 66 | }); 67 | 68 | configuration.excludePatterns.forEach((excludePattern) => { 69 | args.push( 70 | '--exclude', 71 | buildCrossplatformArg( 72 | buildGlobPattern(message.targetUri, excludePattern), 73 | ), 74 | ); 75 | }); 76 | } else { 77 | args.push( 78 | '--include', 79 | buildCrossplatformArg(buildGlobPattern(message.targetUri)), 80 | ); 81 | } 82 | 83 | args.push('--threadCount', String(configuration.workerThreadCount)); 84 | args.push('--fileLimit', String(configuration.fileLimit)); 85 | 86 | if (configuration.formatWithPrettier) { 87 | args.push('--usePrettier'); 88 | } 89 | 90 | args.push('--useJson'); 91 | args.push('--useCache'); 92 | 93 | args.push('--dryRun'); 94 | args.push( 95 | '--outputDirectoryPath', 96 | buildCrossplatformArg(storageUri.fsPath), 97 | ); 98 | args.push(...codemodArguments); 99 | return args; 100 | }; 101 | -------------------------------------------------------------------------------- /src/components/downloadService.ts: -------------------------------------------------------------------------------- 1 | import { isAxiosError } from 'axios'; 2 | import { retryingClient, DEFAULT_RETRY_COUNT } from '../axios'; 3 | import { Mode } from 'node:fs'; 4 | import { FileSystem, Uri } from 'vscode'; 5 | import { FileSystemUtilities } from './fileSystemUtilities'; 6 | 7 | export class RequestError extends Error {} 8 | export class ForbiddenRequestError extends Error {} 9 | 10 | export class DownloadService { 11 | #fileSystem: FileSystem; 12 | #fileSystemUtilities: FileSystemUtilities; 13 | 14 | constructor( 15 | fileSystem: FileSystem, 16 | fileSystemUtilities: FileSystemUtilities, 17 | ) { 18 | this.#fileSystem = fileSystem; 19 | this.#fileSystemUtilities = fileSystemUtilities; 20 | } 21 | 22 | async downloadFileIfNeeded( 23 | url: string, 24 | uri: Uri, 25 | chmod: Mode | null, 26 | ): Promise { 27 | const localModificationTime = 28 | await this.#fileSystemUtilities.getModificationTime(uri); 29 | 30 | let response; 31 | 32 | try { 33 | response = await retryingClient.head(url, { 34 | timeout: 15000, 35 | 'axios-retry': { 36 | retries: DEFAULT_RETRY_COUNT, 37 | }, 38 | }); 39 | } catch (error) { 40 | if (localModificationTime > 0) { 41 | return false; 42 | } 43 | 44 | if (!isAxiosError(error)) { 45 | throw error; 46 | } 47 | 48 | const status = error.response?.status; 49 | 50 | if (status === 403) { 51 | throw new ForbiddenRequestError( 52 | `Could not make a request to ${url}: request forbidden`, 53 | ); 54 | } 55 | 56 | throw new RequestError(`Could not make a request to ${url}`); 57 | } 58 | 59 | const lastModified = response?.headers['last-modified'] ?? null; 60 | const remoteModificationTime = lastModified 61 | ? Date.parse(lastModified) 62 | : localModificationTime; 63 | 64 | if (localModificationTime < remoteModificationTime) { 65 | await this.#downloadFile(url, uri, chmod); 66 | 67 | return true; 68 | } 69 | 70 | return false; 71 | } 72 | 73 | async #downloadFile( 74 | url: string, 75 | uri: Uri, 76 | chmod: Mode | null, 77 | ): Promise { 78 | const response = await retryingClient.get(url, { 79 | responseType: 'arraybuffer', 80 | 'axios-retry': { 81 | retries: DEFAULT_RETRY_COUNT, 82 | }, 83 | }); 84 | const content = new Uint8Array(response.data); 85 | 86 | await this.#fileSystem.writeFile(uri, content); 87 | 88 | if (chmod !== null) { 89 | await this.#fileSystemUtilities.setChmod(uri, chmod); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/components/fileService.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { Uri, workspace } from 'vscode'; 3 | import { Message, MessageBus, MessageKind } from './messageBus'; 4 | 5 | export class FileService { 6 | readonly #messageBus: MessageBus; 7 | 8 | public constructor(readonly messageBus: MessageBus) { 9 | this.#messageBus = messageBus; 10 | 11 | this.#messageBus.subscribe(MessageKind.createFile, (message) => 12 | this.#onCreateFile(message), 13 | ); 14 | 15 | this.#messageBus.subscribe(MessageKind.updateFile, (message) => 16 | this.#onUpdateFile(message), 17 | ); 18 | 19 | this.#messageBus.subscribe(MessageKind.moveFile, (message) => 20 | this.#onMoveFile(message), 21 | ); 22 | 23 | this.#messageBus.subscribe(MessageKind.deleteFiles, (message) => 24 | this.#onDeleteFile(message), 25 | ); 26 | 27 | this.#messageBus.subscribe(MessageKind.deleteDirectories, (message) => 28 | this.#onDeleteDirectory(message), 29 | ); 30 | } 31 | 32 | async #onCreateFile(message: Message & { kind: MessageKind.createFile }) { 33 | await this.createFile(message); 34 | } 35 | 36 | async #onUpdateFile(message: Message & { kind: MessageKind.updateFile }) { 37 | await this.updateFile(message); 38 | } 39 | 40 | async #onMoveFile(message: Message & { kind: MessageKind.moveFile }) { 41 | await this.moveFile(message); 42 | } 43 | 44 | async #onDeleteFile(message: Message & { kind: MessageKind.deleteFiles }) { 45 | await this.deleteFiles(message); 46 | } 47 | 48 | async #onDeleteDirectory( 49 | message: Message & { kind: MessageKind.deleteDirectories }, 50 | ) { 51 | await this.deleteDirectories(message); 52 | } 53 | 54 | public async createFile(params: { 55 | newUri: Uri; 56 | newContentUri: Uri; 57 | }): Promise { 58 | const content = await workspace.fs.readFile(params.newContentUri); 59 | 60 | const directory = dirname(params.newUri.fsPath); 61 | 62 | await workspace.fs.createDirectory(Uri.file(directory)); 63 | 64 | await workspace.fs.writeFile(params.newUri, content); 65 | } 66 | 67 | public async updateFileContent(params: { uri: Uri; content: string }) { 68 | await workspace.fs.writeFile(params.uri, Buffer.from(params.content)); 69 | } 70 | 71 | public async updateFile(params: { 72 | uri: Uri; 73 | contentUri: Uri; 74 | }): Promise { 75 | const content = await workspace.fs.readFile(params.contentUri); 76 | await workspace.fs.writeFile(params.uri, content); 77 | } 78 | 79 | public async moveFile(params: { 80 | newUri: Uri; 81 | oldUri: Uri; 82 | newContentUri: Uri; 83 | }): Promise { 84 | const content = await workspace.fs.readFile(params.newContentUri); 85 | 86 | const directory = dirname(params.newUri.fsPath); 87 | 88 | await workspace.fs.createDirectory(Uri.file(directory)); 89 | 90 | await workspace.fs.writeFile(params.newUri, content); 91 | 92 | await this.deleteFiles({ uris: [params.oldUri] }); 93 | } 94 | 95 | public async deleteDirectories(params: { 96 | uris: ReadonlyArray; 97 | }): Promise { 98 | for (const uri of params.uris) { 99 | try { 100 | await workspace.fs.delete(uri, { 101 | recursive: true, 102 | useTrash: false, 103 | }); 104 | } catch (error) { 105 | console.error(error); 106 | } 107 | } 108 | } 109 | 110 | public async deleteFiles(params: { 111 | uris: ReadonlyArray; 112 | }): Promise { 113 | for (const uri of params.uris) { 114 | try { 115 | await workspace.fs.delete(uri, { 116 | recursive: false, 117 | useTrash: false, 118 | }); 119 | } catch (error) { 120 | console.error(error); 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/components/fileSystemUtilities.ts: -------------------------------------------------------------------------------- 1 | import { Mode } from 'node:fs'; 2 | import { chmod } from 'node:fs/promises'; 3 | import { FileSystem, FileSystemError, Uri } from 'vscode'; 4 | 5 | export class FileSystemUtilities { 6 | readonly #fs: FileSystem; 7 | constructor(fs: FileSystem) { 8 | this.#fs = fs; 9 | } 10 | 11 | public async getModificationTime(uri: Uri): Promise { 12 | try { 13 | const fileStat = await this.#fs.stat(uri); 14 | 15 | return fileStat.mtime; 16 | } catch (error) { 17 | if (error instanceof FileSystemError) { 18 | return 0; 19 | } 20 | 21 | throw error; 22 | } 23 | } 24 | 25 | public async setChmod(uri: Uri, mode: Mode): Promise { 26 | if (uri.scheme !== 'file' && uri.scheme !== 'vscode-userdata') { 27 | console.warn('Cannot set chmod for a non-file URI', uri); 28 | 29 | return; 30 | } 31 | 32 | // the VSCode file system does not support chmod 33 | return chmod(uri.fsPath, mode); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/jobManager.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { isNeitherNullNorUndefined } from '../utilities'; 3 | import { Message, MessageBus, MessageKind } from './messageBus'; 4 | import { 5 | JobHash, 6 | JobKind, 7 | mapJobToPersistedJob, 8 | mapPersistedJobToJob, 9 | } from '../jobs/types'; 10 | import { FileService } from './fileService'; 11 | import { acceptJobs } from '../jobs/acceptJobs'; 12 | import { Store } from '../data'; 13 | import { actions } from '../data/slice'; 14 | 15 | export class JobManager { 16 | public constructor( 17 | private readonly __fileService: FileService, 18 | private readonly __messageBus: MessageBus, 19 | private readonly __store: Store, 20 | ) { 21 | this.__messageBus.subscribe(MessageKind.upsertJobs, (message) => 22 | this.__onUpsertJobsMessage(message), 23 | ); 24 | this.__messageBus.subscribe(MessageKind.acceptJobs, (message) => 25 | this.__onAcceptJobsMessage(message), 26 | ); 27 | this.__messageBus.subscribe(MessageKind.rejectJobs, (message) => 28 | this.__onRejectJobsMessage(message), 29 | ); 30 | } 31 | 32 | private __onUpsertJobsMessage( 33 | message: Message & { kind: MessageKind.upsertJobs }, 34 | ) { 35 | const persistedJobs = message.jobs.map(mapJobToPersistedJob); 36 | 37 | this.__store.dispatch(actions.upsertJobs(persistedJobs)); 38 | } 39 | 40 | private async __onAcceptJobsMessage( 41 | message: Message & { kind: MessageKind.acceptJobs }, 42 | ) { 43 | this.acceptJobs(message.jobHashes); 44 | } 45 | 46 | public async acceptJobs(jobHashes: ReadonlySet): Promise { 47 | const state = this.__store.getState(); 48 | 49 | const deletedJobs = Array.from(jobHashes) 50 | .map((jobHash) => state.job.entities[jobHash]) 51 | .filter(isNeitherNullNorUndefined) 52 | .map(mapPersistedJobToJob); 53 | 54 | await acceptJobs(this.__fileService, deletedJobs); 55 | 56 | this.deleteJobs(Array.from(jobHashes)); 57 | 58 | this.__messageBus.publish({ 59 | kind: MessageKind.jobsAccepted, 60 | deletedJobs: new Set(deletedJobs), 61 | }); 62 | } 63 | 64 | public deleteJobs(jobHashes: ReadonlyArray) { 65 | this.__store.dispatch(actions.deleteJobs(jobHashes)); 66 | const state = this.__store.getState(); 67 | 68 | const deletedJobs = Array.from(jobHashes) 69 | .map((jobHash) => state.job.entities[jobHash]) 70 | .filter(isNeitherNullNorUndefined) 71 | .map(mapPersistedJobToJob); 72 | 73 | this.__messageBus.publish({ 74 | kind: MessageKind.jobsRejected, 75 | deletedJobs: new Set(deletedJobs), 76 | }); 77 | } 78 | 79 | private __onRejectJobsMessage( 80 | message: Message & { kind: MessageKind.rejectJobs }, 81 | ) { 82 | const state = this.__store.getState(); 83 | 84 | const deletedJobs = Array.from(message.jobHashes) 85 | .map((jobHash) => state.job.entities[jobHash]) 86 | .filter(isNeitherNullNorUndefined) 87 | .map(mapPersistedJobToJob); 88 | 89 | const messages: Message[] = []; 90 | 91 | messages.push({ 92 | kind: MessageKind.jobsRejected, 93 | deletedJobs: new Set(deletedJobs), 94 | }); 95 | 96 | for (const job of deletedJobs) { 97 | if ( 98 | (job.kind === JobKind.rewriteFile || 99 | job.kind === JobKind.moveAndRewriteFile || 100 | job.kind === JobKind.createFile || 101 | job.kind === JobKind.moveFile) && 102 | job.newContentUri 103 | ) { 104 | messages.push({ 105 | kind: MessageKind.deleteFiles, 106 | uris: [job.newContentUri], 107 | }); 108 | } 109 | } 110 | 111 | this.deleteJobs(deletedJobs.map(({ hash }) => hash)); 112 | 113 | for (const message of messages) { 114 | this.__messageBus.publish(message); 115 | } 116 | } 117 | 118 | public async changeJobContent(jobHash: JobHash, newJobContent: string) { 119 | const job = this.__store.getState().job.entities[jobHash]; 120 | 121 | const newContentUri = job?.newContentUri ?? null; 122 | 123 | if (job === undefined || newContentUri === null) { 124 | return; 125 | } 126 | 127 | await this.__fileService.updateFileContent({ 128 | uri: vscode.Uri.parse(newContentUri), 129 | content: newJobContent, 130 | }); 131 | 132 | this.__store.dispatch(actions.upsertJobs([{ ...job }])); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/components/textDocumentContentProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Event, 3 | EventEmitter, 4 | ProviderResult, 5 | TextDocumentContentProvider, 6 | Uri, 7 | } from 'vscode'; 8 | 9 | export class IntuitaTextDocumentContentProvider 10 | implements TextDocumentContentProvider 11 | { 12 | readonly URI = Uri.parse('intuita:jscodeshiftCodemod.ts'); 13 | readonly #onDidChangeEmitter = new EventEmitter(); 14 | readonly onDidChange: Event | undefined = undefined; 15 | 16 | #content = ''; 17 | 18 | constructor() { 19 | this.onDidChange = this.#onDidChangeEmitter.event; 20 | } 21 | 22 | setContent(content: string) { 23 | this.#content = content; 24 | 25 | this.#onDidChangeEmitter.fire(this.URI); 26 | } 27 | 28 | provideTextDocumentContent(uri: Uri): ProviderResult { 29 | if (uri.toString() !== this.URI.toString()) { 30 | throw new Error( 31 | `You can only read the content of ${this.URI.toString()}`, 32 | ); 33 | } 34 | 35 | return this.#content; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/userService.ts: -------------------------------------------------------------------------------- 1 | import { Memento } from 'vscode'; 2 | 3 | export class GlobalStateTokenStorage { 4 | constructor(private readonly __globalState: Memento) {} 5 | 6 | getAccessToken(): string | null { 7 | const accessToken = this.__globalState.get('accessToken'); 8 | return typeof accessToken === 'string' ? accessToken : null; 9 | } 10 | 11 | setAccessToken(accessToken: string | undefined): void { 12 | this.__globalState.update('accessToken', accessToken); 13 | } 14 | } 15 | 16 | export class UserService { 17 | constructor(private readonly __storage: GlobalStateTokenStorage) {} 18 | 19 | getLinkedToken() { 20 | return this.__storage.getAccessToken(); 21 | } 22 | 23 | unlinkUserIntuitaAccount(): void { 24 | this.__storage.setAccessToken(undefined); 25 | } 26 | 27 | linkUserIntuitaAccount(accessToken: string): void { 28 | this.__storage.setAccessToken(accessToken); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/webview/CodemodDescriptionProvider.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, FileSystem, Uri } from 'vscode'; 2 | import { buildCodemodMetadataHash } from '../../utilities'; 3 | import { createHash } from 'node:crypto'; 4 | import { join } from 'node:path'; 5 | import { homedir } from 'node:os'; 6 | 7 | export class CodemodDescriptionProvider { 8 | private __descriptions = new Map(); 9 | public onDidChangeEmitter = new EventEmitter(); 10 | public onDidChange = this.onDidChangeEmitter.event; 11 | 12 | constructor(private readonly __fileSystem: FileSystem) {} 13 | 14 | public getCodemodDescription(name: string): string { 15 | const hash = buildCodemodMetadataHash(name); 16 | 17 | const hashDigest = createHash('ripemd160') 18 | .update(name) 19 | .digest('base64url'); 20 | 21 | const path = join(homedir(), '.intuita', hashDigest, 'description.md'); 22 | 23 | const data = this.__descriptions.get(hash) ?? null; 24 | 25 | if (data === null) { 26 | this.__fileSystem.readFile(Uri.file(path)).then((uint8array) => { 27 | const data = uint8array.toString(); 28 | 29 | this.__descriptions.set(hash, data); 30 | 31 | this.onDidChangeEmitter.fire(null); 32 | }); 33 | 34 | return 'No description or metadata found.'; 35 | } 36 | 37 | return data; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/webview/ErrorWebviewProvider.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, WebviewView, WebviewViewProvider } from 'vscode'; 2 | import { MessageBus, MessageKind } from '../messageBus'; 3 | import { WebviewMessage } from './webviewEvents'; 4 | import { WebviewResolver } from './WebviewResolver'; 5 | import { Store } from '../../data'; 6 | import { actions } from '../../data/slice'; 7 | import areEqual from 'fast-deep-equal'; 8 | import { MainViewProvider } from './MainProvider'; 9 | import { selectErrorWebviewViewProps } from '../../selectors/selectErrorWebviewViewProps'; 10 | 11 | export class ErrorWebviewProvider implements WebviewViewProvider { 12 | private readonly __webviewResolver: WebviewResolver; 13 | private __webviewView: WebviewView | null = null; 14 | 15 | public constructor( 16 | context: ExtensionContext, 17 | messageBus: MessageBus, 18 | private readonly __store: Store, 19 | private readonly __mainWebviewViewProvider: MainViewProvider, 20 | ) { 21 | this.__webviewResolver = new WebviewResolver(context.extensionUri); 22 | 23 | let prevProps = this.__buildViewProps(); 24 | 25 | const handler = async () => { 26 | const nextProps = this.__buildViewProps(); 27 | 28 | if (areEqual(prevProps, nextProps)) { 29 | return; 30 | } 31 | 32 | prevProps = nextProps; 33 | 34 | this.__postMessage({ 35 | kind: 'webview.error.setProps', 36 | errorWebviewViewProps: nextProps, 37 | }); 38 | 39 | if ( 40 | nextProps.kind === 'CASE_SELECTED' && 41 | nextProps.executionErrors.length !== 0 42 | ) { 43 | this.showView(); 44 | } 45 | }; 46 | 47 | messageBus.subscribe( 48 | MessageKind.mainWebviewViewVisibilityChange, 49 | handler, 50 | ); 51 | 52 | messageBus.subscribe( 53 | MessageKind.codemodSetExecuted, 54 | async ({ case: kase, executionErrors }) => { 55 | this.__store.dispatch( 56 | actions.setExecutionErrors({ 57 | caseHash: kase.hash, 58 | errors: executionErrors, 59 | }), 60 | ); 61 | }, 62 | ); 63 | 64 | this.__store.subscribe(handler); 65 | } 66 | 67 | public resolveWebviewView(webviewView: WebviewView): void | Thenable { 68 | this.__webviewView = webviewView; 69 | 70 | const resolve = () => { 71 | this.__webviewResolver.resolveWebview( 72 | webviewView.webview, 73 | 'errors', 74 | JSON.stringify(this.__buildViewProps()), 75 | 'errorWebviewViewProps', 76 | ); 77 | }; 78 | 79 | resolve(); 80 | 81 | this.__webviewView.onDidChangeVisibility(() => { 82 | if (this.__webviewView?.visible) { 83 | resolve(); 84 | } 85 | }); 86 | } 87 | 88 | public showView() { 89 | this.__webviewView?.show(true); 90 | } 91 | 92 | private __buildViewProps() { 93 | return selectErrorWebviewViewProps( 94 | this.__store.getState(), 95 | this.__mainWebviewViewProvider.isVisible(), 96 | ); 97 | } 98 | 99 | private __postMessage(message: WebviewMessage) { 100 | this.__webviewView?.webview.postMessage(message); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/components/webview/panelViewProps.ts: -------------------------------------------------------------------------------- 1 | import type { CaseHash } from '../../cases/types'; 2 | import type { JobKind } from '../../jobs/types'; 3 | import type { JobHash } from './webviewEvents'; 4 | 5 | export type PanelViewProps = 6 | | Readonly<{ 7 | kind: 'JOB'; 8 | title: string; 9 | caseHash: CaseHash; 10 | jobHash: JobHash; 11 | jobKind: JobKind; 12 | oldFileContent: string | null; 13 | newFileContent: string | null; 14 | originalNewFileContent: string | null; 15 | oldFileTitle: string | null; 16 | newFileTitle: string | null; 17 | reviewed: boolean; 18 | }> 19 | | Readonly<{ 20 | kind: 'CODEMOD'; 21 | title: string; 22 | description: string; 23 | }>; 24 | -------------------------------------------------------------------------------- /src/configuration.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export const getConfiguration = () => { 4 | const configuration = vscode.workspace.getConfiguration('intuita'); 5 | 6 | const fileLimit = configuration.get('fileLimit') ?? 100; 7 | 8 | const workerThreadCount = 9 | configuration.get('workerThreadCount') ?? 4; 10 | 11 | const includePatterns = configuration.get('include') ?? [ 12 | '**/*.*{ts,tsx,js,jsx,mjs,cjs,mdx,json}', 13 | ]; 14 | const excludePatterns = configuration.get('exclude') ?? [ 15 | '**/node_modules/**/*.*', 16 | ]; 17 | 18 | const formatWithPrettier = 19 | configuration.get('formatWithPrettier') ?? false; 20 | 21 | return { 22 | fileLimit, 23 | workerThreadCount, 24 | includePatterns, 25 | excludePatterns, 26 | formatWithPrettier, 27 | }; 28 | }; 29 | 30 | export const setConfigurationProperty = async ( 31 | propertyName: string, 32 | value: unknown, 33 | configurationTarget: vscode.ConfigurationTarget, 34 | ) => { 35 | const configuration = vscode.workspace.getConfiguration('intuita'); 36 | 37 | return configuration.update(propertyName, value, configurationTarget); 38 | }; 39 | export type Configuration = ReturnType; 40 | -------------------------------------------------------------------------------- /src/container.ts: -------------------------------------------------------------------------------- 1 | export const buildContainer = (initialValue: NonNullable) => { 2 | let currentValue: NonNullable = initialValue; 3 | 4 | const get = (): NonNullable => { 5 | return currentValue; 6 | }; 7 | 8 | const set = (value: NonNullable): void => { 9 | currentValue = value; 10 | }; 11 | 12 | return { 13 | get, 14 | set, 15 | }; 16 | }; 17 | 18 | export type Container = ReturnType>; 19 | -------------------------------------------------------------------------------- /src/data/codemodConfigSchema.ts: -------------------------------------------------------------------------------- 1 | import * as S from '@effect/schema/Schema'; 2 | 3 | export const argumentSchema = S.union( 4 | S.struct({ 5 | name: S.string, 6 | kind: S.literal('string'), 7 | description: S.optional(S.string).withDefault(() => ''), 8 | required: S.optional(S.boolean).withDefault(() => false), 9 | default: S.optional(S.string), 10 | }), 11 | S.struct({ 12 | name: S.string, 13 | kind: S.literal('number'), 14 | description: S.optional(S.string).withDefault(() => ''), 15 | required: S.optional(S.boolean).withDefault(() => false), 16 | default: S.optional(S.number), 17 | }), 18 | S.struct({ 19 | name: S.string, 20 | kind: S.literal('boolean'), 21 | description: S.optional(S.string).withDefault(() => ''), 22 | required: S.optional(S.boolean).withDefault(() => false), 23 | default: S.optional(S.boolean), 24 | }), 25 | // S.struct({ 26 | // name: S.string, 27 | // kind: S.literal('selection'), 28 | // description: S.string, 29 | // options: S.array(S.string), 30 | // default: S.union(S.string, S.undefined), 31 | // }), 32 | ); 33 | 34 | export const argumentsSchema = S.array(argumentSchema); 35 | 36 | export const PIRANHA_LANGUAGES = [ 37 | 'java', 38 | 'kt', 39 | 'go', 40 | 'py', 41 | 'swift', 42 | 'ts', 43 | 'tsx', 44 | 'scala', 45 | ] as const; 46 | 47 | const piranhaLanguageSchema = S.union( 48 | ...PIRANHA_LANGUAGES.map((language) => S.literal(language)), 49 | ); 50 | 51 | export type PiranhaLanguage = S.Schema.To; 52 | 53 | export const parsePiranhaLanguage = S.parseSync(piranhaLanguageSchema); 54 | 55 | export const codemodConfigSchema = S.union( 56 | S.struct({ 57 | schemaVersion: S.literal('1.0.0'), 58 | engine: S.literal('piranha'), 59 | language: piranhaLanguageSchema, 60 | arguments: S.optional(argumentsSchema), 61 | }), 62 | S.struct({ 63 | schemaVersion: S.literal('1.0.0'), 64 | engine: S.literal('jscodeshift'), 65 | arguments: S.optional(argumentsSchema), 66 | }), 67 | S.struct({ 68 | schemaVersion: S.literal('1.0.0'), 69 | engine: S.literal('ts-morph'), 70 | arguments: S.optional(argumentsSchema), 71 | }), 72 | S.struct({ 73 | schemaVersion: S.literal('1.0.0'), 74 | engine: S.literal('filemod'), 75 | arguments: S.optional(argumentsSchema), 76 | }), 77 | S.struct({ 78 | schemaVersion: S.literal('1.0.0'), 79 | engine: S.literal('repomod-engine'), 80 | arguments: S.optional(argumentsSchema), 81 | }), 82 | S.struct({ 83 | schemaVersion: S.literal('1.0.0'), 84 | engine: S.literal('recipe'), 85 | names: S.array(S.string), 86 | arguments: S.optional(argumentsSchema), 87 | }), 88 | ); 89 | 90 | export const parseCodemodConfigSchema = S.parseSync(codemodConfigSchema); 91 | 92 | export type CodemodArgument = S.Schema.To; 93 | export type CodemodConfig = S.Schema.To; 94 | -------------------------------------------------------------------------------- /src/data/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore, Dispatch, Reducer } from '@reduxjs/toolkit'; 2 | import { persistReducer, persistStore } from 'redux-persist'; 3 | import MementoStorage from './storage'; 4 | 5 | import rootReducer, { actions, getInitialState } from './slice'; 6 | import { Memento } from 'vscode'; 7 | import { PersistPartial } from 'redux-persist/es/persistReducer'; 8 | import { persistedStateCodecNew } from '../persistedState/codecs'; 9 | import { window } from 'vscode'; 10 | 11 | const PERSISTANCE_PREFIX = 'persist'; 12 | const PERSISTANCE_KEY = 'compressedRoot'; 13 | const HYDRATION_TIMEOUT = 3 * 1000; 14 | 15 | const deserializeState = (serializedState: string) => { 16 | const parsedState: Record = {}; 17 | 18 | try { 19 | const rawState = JSON.parse(serializedState); 20 | 21 | if (typeof rawState !== 'object' || rawState === null) { 22 | return null; 23 | } 24 | 25 | Object.entries(rawState).forEach(([key, value]) => { 26 | if (typeof value !== 'string') { 27 | return; 28 | } 29 | 30 | parsedState[key] = JSON.parse(value); 31 | }); 32 | } catch (e) { 33 | console.error(e); 34 | 35 | return null; 36 | } 37 | 38 | return parsedState; 39 | }; 40 | 41 | const getPreloadedState = async (storage: MementoStorage) => { 42 | const initialState = await storage.getItem( 43 | `${PERSISTANCE_PREFIX}:${PERSISTANCE_KEY}`, 44 | ); 45 | 46 | if (!initialState) { 47 | return null; 48 | } 49 | 50 | const deserializedState = deserializeState(initialState); 51 | 52 | if (!deserializedState) { 53 | return null; 54 | } 55 | 56 | const decodedState = persistedStateCodecNew.decode(deserializedState); 57 | 58 | // should never happen because of codec fallback 59 | if (decodedState._tag !== 'Right') { 60 | return null; 61 | } 62 | 63 | return decodedState.right; 64 | }; 65 | 66 | const buildStore = async (workspaceState: Memento) => { 67 | const storage = new MementoStorage(workspaceState); 68 | 69 | const persistedReducer = persistReducer( 70 | { 71 | key: PERSISTANCE_KEY, 72 | storage, 73 | timeout: HYDRATION_TIMEOUT, 74 | }, 75 | rootReducer, 76 | ); 77 | 78 | const validatedReducer: Reducer< 79 | (RootState & PersistPartial) | undefined 80 | > = (state, action) => { 81 | if (action.type === 'persist/REHYDRATE') { 82 | const decoded = persistedStateCodecNew.decode(action.payload); 83 | 84 | const validatedPayload = 85 | decoded._tag === 'Right' ? decoded.right : getInitialState(); 86 | 87 | return persistedReducer(state, { 88 | ...action, 89 | payload: validatedPayload, 90 | }); 91 | } 92 | 93 | return persistedReducer(state, action); 94 | }; 95 | 96 | const preloadedState = await getPreloadedState(storage); 97 | 98 | if (preloadedState === null) { 99 | window.showWarningMessage('Unable to get preloaded state.'); 100 | } 101 | 102 | const store = configureStore({ 103 | reducer: validatedReducer, 104 | ...(preloadedState !== null && { preloadedState }), 105 | }); 106 | 107 | const persistor = persistStore(store); 108 | return { store, persistor }; 109 | }; 110 | 111 | type RootState = ReturnType; 112 | type ActionCreators = typeof actions; 113 | type Actions = { [K in keyof ActionCreators]: ReturnType }; 114 | type Action = Actions[keyof Actions]; 115 | 116 | type AppDispatch = Dispatch; 117 | type Store = Awaited>['store']; 118 | 119 | export { buildStore }; 120 | 121 | export type { RootState, AppDispatch, Store }; 122 | -------------------------------------------------------------------------------- /src/data/privateCodemodsEnvelopeSchema.ts: -------------------------------------------------------------------------------- 1 | import * as S from '@effect/schema/Schema'; 2 | 3 | const privateCodemodsEnvelopeSchema = S.struct({ 4 | names: S.array(S.string), 5 | }); 6 | 7 | export const parsePrivateCodemodsEnvelope = S.parseSync( 8 | privateCodemodsEnvelopeSchema, 9 | ); 10 | -------------------------------------------------------------------------------- /src/data/schemata/argumentRecordSchema.ts: -------------------------------------------------------------------------------- 1 | import * as S from '@effect/schema/Schema'; 2 | 3 | export const argumentRecordSchema = S.record( 4 | S.string, 5 | S.union(S.string, S.number, S.boolean), 6 | ); 7 | 8 | export const parseArgumentRecordSchema = S.parseSync(argumentRecordSchema); 9 | 10 | export type ArgumentRecord = S.Schema.To; 11 | -------------------------------------------------------------------------------- /src/data/storage.ts: -------------------------------------------------------------------------------- 1 | import type { WebStorage } from 'redux-persist'; 2 | import { Memento } from 'vscode'; 3 | import { deflate, unzip } from 'node:zlib'; 4 | import { promisify } from 'node:util'; 5 | 6 | const asyncDeflate = promisify(deflate); 7 | const asyncUnzip = promisify(unzip); 8 | 9 | // redux-persists storage impl for vscode memento 10 | class MementoStorage implements WebStorage { 11 | constructor(private readonly __memento: Memento) {} 12 | 13 | public async getItem(key: string): Promise { 14 | const storedValue = this.__memento.get(key); 15 | 16 | if (typeof storedValue !== 'string') { 17 | return null; 18 | } 19 | 20 | try { 21 | const oldBuffer = Buffer.from(storedValue, 'base64url'); 22 | const newBuffer = await asyncUnzip(oldBuffer); 23 | return newBuffer.toString('utf8'); 24 | } catch (e) { 25 | return null; 26 | } 27 | } 28 | 29 | public async setItem(key: string, value: string): Promise { 30 | const oldBuffer = Buffer.from(value, 'utf8'); 31 | const newBuffer = await asyncDeflate(oldBuffer, {}); 32 | this.__memento.update(key, newBuffer.toString('base64url')); 33 | } 34 | 35 | public removeItem(key: string): Promise { 36 | return new Promise((resolve) => { 37 | this.__memento.update(key, void 0); 38 | 39 | resolve(); 40 | }); 41 | } 42 | 43 | public getAllKeys(): Promise> { 44 | return new Promise((resolve) => { 45 | const allKeys = this.__memento.keys(); 46 | 47 | resolve(allKeys); 48 | }); 49 | } 50 | } 51 | 52 | export default MementoStorage; 53 | -------------------------------------------------------------------------------- /src/data/urlParamsEnvelopeSchema.ts: -------------------------------------------------------------------------------- 1 | import * as S from '@effect/schema/Schema'; 2 | 3 | const urlParamsEnvelopeSchema = S.struct({ 4 | urlParams: S.string, 5 | }); 6 | 7 | export const parseUrlParamsEnvelope = S.parseSync(urlParamsEnvelopeSchema); 8 | -------------------------------------------------------------------------------- /src/errors/types.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { buildTypeCodec } from '../utilities'; 3 | 4 | export const executionErrorCodec = buildTypeCodec({ 5 | message: t.string, 6 | path: t.union([t.string, t.undefined]), 7 | }); 8 | 9 | export type ExecutionError = t.TypeOf; 10 | -------------------------------------------------------------------------------- /src/github/types.ts: -------------------------------------------------------------------------------- 1 | import { buildTypeCodec } from '../utilities'; 2 | import * as t from 'io-ts'; 3 | 4 | export const createIssueResponseCodec = buildTypeCodec({ 5 | html_url: t.string, 6 | }); 7 | 8 | export type CreateIssueResponse = t.TypeOf; 9 | -------------------------------------------------------------------------------- /src/jobs/acceptJobs.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | import { FileService } from '../components/fileService'; 3 | import { Job, JobKind } from './types'; 4 | 5 | export const acceptJobs = async ( 6 | fileService: FileService, 7 | jobs: ReadonlyArray, 8 | ) => { 9 | const createJobOutputs: [Uri, Uri][] = []; 10 | const updateJobOutputs: [Uri, Uri][] = []; 11 | const deleteJobOutputs: Uri[] = []; 12 | const moveJobOutputs: [Uri, Uri, Uri][] = []; 13 | 14 | for (const job of jobs) { 15 | if ( 16 | job.kind === JobKind.createFile && 17 | job.newUri && 18 | job.newContentUri 19 | ) { 20 | createJobOutputs.push([job.newUri, job.newContentUri]); 21 | } 22 | 23 | if (job.kind === JobKind.deleteFile && job.oldUri) { 24 | deleteJobOutputs.push(job.oldUri); 25 | } 26 | 27 | if ( 28 | (job.kind === JobKind.moveAndRewriteFile || 29 | job.kind === JobKind.moveFile) && 30 | job.oldUri && 31 | job.newUri && 32 | job.newContentUri 33 | ) { 34 | moveJobOutputs.push([job.oldUri, job.newUri, job.newContentUri]); 35 | } 36 | 37 | if ( 38 | job.kind === JobKind.rewriteFile && 39 | job.oldUri && 40 | job.newContentUri 41 | ) { 42 | updateJobOutputs.push([job.oldUri, job.newContentUri]); 43 | } 44 | 45 | if (job.kind === JobKind.copyFile && job.newUri && job.newContentUri) { 46 | createJobOutputs.push([job.newUri, job.newContentUri]); 47 | } 48 | } 49 | 50 | for (const createJobOutput of createJobOutputs) { 51 | const [newUri, newContentUri] = createJobOutput; 52 | await fileService.createFile({ 53 | newUri, 54 | newContentUri, 55 | }); 56 | } 57 | 58 | for (const updateJobOutput of updateJobOutputs) { 59 | const [uri, contentUri] = updateJobOutput; 60 | await fileService.updateFile({ 61 | uri, 62 | contentUri, 63 | }); 64 | } 65 | 66 | for (const moveJobOutput of moveJobOutputs) { 67 | const [oldUri, newUri, newContentUri] = moveJobOutput; 68 | await fileService.moveFile({ 69 | oldUri, 70 | newUri, 71 | newContentUri, 72 | }); 73 | } 74 | 75 | await fileService.deleteFiles({ 76 | uris: deleteJobOutputs.slice(), 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /src/jobs/buildJobHash.ts: -------------------------------------------------------------------------------- 1 | import type { CaseHash } from '../cases/types'; 2 | import type { Job, JobHash } from './types'; 3 | import { buildUriHash } from '../uris/buildUriHash'; 4 | import { buildHash } from '../utilities'; 5 | 6 | export const buildJobHash = ( 7 | hashlessJob: Omit, 8 | caseHashDigest: CaseHash, 9 | ): JobHash => { 10 | return buildHash( 11 | [ 12 | caseHashDigest, 13 | hashlessJob.kind, 14 | hashlessJob.oldUri ? buildUriHash(hashlessJob.oldUri) : '', 15 | hashlessJob.newUri ? buildUriHash(hashlessJob.newUri) : '', 16 | hashlessJob.newContentUri 17 | ? buildUriHash(hashlessJob.newContentUri) 18 | : '', 19 | hashlessJob.codemodName, 20 | ].join(','), 21 | ) as JobHash; 22 | }; 23 | -------------------------------------------------------------------------------- /src/jobs/types.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | import * as t from 'io-ts'; 3 | import { buildTypeCodec } from '../utilities'; 4 | import { CaseHash, caseHashCodec } from '../cases/types'; 5 | 6 | interface JobHashBrand { 7 | readonly __JobHash: unique symbol; 8 | } 9 | 10 | export const jobHashCodec = t.brand( 11 | t.string, 12 | (hashDigest): hashDigest is t.Branded => 13 | hashDigest.length > 0, 14 | '__JobHash', 15 | ); 16 | 17 | export type JobHash = t.TypeOf; 18 | 19 | export const enum JobKind { 20 | rewriteFile = 1, 21 | createFile = 2, 22 | deleteFile = 3, 23 | moveFile = 4, 24 | moveAndRewriteFile = 5, 25 | copyFile = 6, 26 | } 27 | 28 | export type Job = Readonly<{ 29 | hash: JobHash; 30 | kind: JobKind; 31 | oldUri: Uri | null; 32 | newUri: Uri | null; 33 | newContentUri: Uri | null; 34 | originalNewContent: string | null; 35 | codemodName: string; 36 | createdAt: number; 37 | caseHashDigest: CaseHash; 38 | }>; 39 | 40 | export const persistedJobCodec = buildTypeCodec({ 41 | hash: jobHashCodec, 42 | kind: t.union([ 43 | t.literal(JobKind.rewriteFile), 44 | t.literal(JobKind.createFile), 45 | t.literal(JobKind.deleteFile), 46 | t.literal(JobKind.moveAndRewriteFile), 47 | t.literal(JobKind.moveFile), 48 | t.literal(JobKind.copyFile), 49 | ]), 50 | oldUri: t.union([t.string, t.null]), 51 | newUri: t.union([t.string, t.null]), 52 | newContentUri: t.union([t.string, t.null]), 53 | originalNewContent: t.union([t.string, t.null]), 54 | codemodName: t.string, 55 | caseHashDigest: caseHashCodec, 56 | createdAt: t.number, 57 | }); 58 | 59 | export type PersistedJob = t.TypeOf; 60 | 61 | export const mapJobToPersistedJob = (job: Job): PersistedJob => { 62 | return { 63 | ...job, 64 | oldUri: job.oldUri?.toString() ?? null, 65 | newUri: job.newUri?.toString() ?? null, 66 | newContentUri: job.newContentUri?.toString() ?? null, 67 | originalNewContent: job.originalNewContent, 68 | }; 69 | }; 70 | 71 | export const mapPersistedJobToJob = (persistedJob: PersistedJob): Job => { 72 | return { 73 | ...persistedJob, 74 | oldUri: persistedJob.oldUri ? Uri.parse(persistedJob.oldUri) : null, 75 | newUri: persistedJob.newUri ? Uri.parse(persistedJob.newUri) : null, 76 | newContentUri: persistedJob.newContentUri 77 | ? Uri.parse(persistedJob.newContentUri) 78 | : null, 79 | originalNewContent: persistedJob.originalNewContent, 80 | }; 81 | }; 82 | -------------------------------------------------------------------------------- /src/leftRightHashes/leftRightHashSetManager.ts: -------------------------------------------------------------------------------- 1 | export class LeftRightHashSetManager { 2 | #set = new Set(); 3 | 4 | public constructor(set: ReadonlySet) { 5 | this.#set = new Set(set); 6 | } 7 | 8 | public getSetValues(): IterableIterator { 9 | return this.#set.values(); 10 | } 11 | 12 | public buildByRightHashes( 13 | rightHashes: Set, 14 | ): LeftRightHashSetManager { 15 | const set = new Set(); 16 | 17 | this.#set.forEach((leftRightHash) => { 18 | const rightHash = leftRightHash.slice( 19 | leftRightHash.length / 2, 20 | ) as R; 21 | 22 | if (!rightHashes.has(rightHash)) { 23 | return; 24 | } 25 | 26 | set.add(leftRightHash); 27 | }); 28 | 29 | return new LeftRightHashSetManager(set); 30 | } 31 | 32 | public getLeftHashes(): ReadonlySet { 33 | const set = new Set(); 34 | 35 | this.#set.forEach((leftRightHash) => { 36 | const leftHash = leftRightHash.slice( 37 | 0, 38 | leftRightHash.length / 2, 39 | ) as L; 40 | 41 | set.add(leftHash); 42 | }); 43 | 44 | return set; 45 | } 46 | 47 | public getRightHashes(): ReadonlySet { 48 | const rightHashes = new Set(); 49 | 50 | this.#set.forEach((leftRightHash) => { 51 | const rightHash = leftRightHash.slice( 52 | leftRightHash.length / 2, 53 | ) as R; 54 | 55 | rightHashes.add(rightHash); 56 | }); 57 | 58 | return rightHashes; 59 | } 60 | 61 | public getRightHashesByLeftHash(leftHash: L): ReadonlySet { 62 | const rightHashes = new Set(); 63 | 64 | this.#set.forEach((leftRightHash) => { 65 | if (!leftRightHash.startsWith(leftHash)) { 66 | return; 67 | } 68 | 69 | const rightHash = leftRightHash.slice(leftHash.length); 70 | 71 | rightHashes.add(rightHash as R); 72 | }); 73 | 74 | return rightHashes; 75 | } 76 | 77 | public upsert(leftHash: L, rightHash: R): void { 78 | const hash = this.#buildLeftRightHash(leftHash, rightHash); 79 | 80 | this.#set.add(hash); 81 | } 82 | 83 | public delete(leftHash: L, rightHash: R): boolean { 84 | const hash = this.#buildLeftRightHash(leftHash, rightHash); 85 | 86 | return this.#set.delete(hash); 87 | } 88 | 89 | public deleteRightHash(rightHash: R): void { 90 | const deletableHashes: string[] = []; 91 | 92 | for (const leftRightHash of this.#set.keys()) { 93 | if (leftRightHash.endsWith(rightHash)) { 94 | deletableHashes.push(leftRightHash); 95 | } 96 | } 97 | 98 | for (const hash of deletableHashes) { 99 | this.#set.delete(hash); 100 | } 101 | } 102 | 103 | public clear(): void { 104 | this.#set.clear(); 105 | } 106 | 107 | #buildLeftRightHash(leftHash: L, rightHash: R): string { 108 | return `${leftHash}${rightHash}`; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/packageJsonAnalyzer/types.ts: -------------------------------------------------------------------------------- 1 | export type CodemodHash = string & { __type: 'CodemodHash' }; 2 | -------------------------------------------------------------------------------- /src/persistedState/explorerNodeCodec.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { jobHashCodec } from '../jobs/types'; 3 | import { buildTypeCodec } from '../utilities'; 4 | 5 | interface ExplorerNodeHashDigestBrand { 6 | readonly __ExplorerNodeHashDigest: unique symbol; 7 | } 8 | 9 | export const _explorerNodeHashDigestCodec = t.brand( 10 | t.string, 11 | ( 12 | hashDigest, 13 | ): hashDigest is t.Branded => 14 | hashDigest.length > 0, 15 | '__ExplorerNodeHashDigest', 16 | ); 17 | 18 | export type _ExplorerNodeHashDigest = t.TypeOf< 19 | typeof _explorerNodeHashDigestCodec 20 | >; 21 | 22 | export const _explorerNodeCodec = t.union([ 23 | buildTypeCodec({ 24 | hashDigest: _explorerNodeHashDigestCodec, 25 | kind: t.literal('ROOT'), 26 | label: t.string, 27 | depth: t.number, 28 | childCount: t.number, 29 | }), 30 | buildTypeCodec({ 31 | hashDigest: _explorerNodeHashDigestCodec, 32 | kind: t.literal('DIRECTORY'), 33 | path: t.string, 34 | label: t.string, 35 | depth: t.number, 36 | childCount: t.number, 37 | }), 38 | buildTypeCodec({ 39 | hashDigest: _explorerNodeHashDigestCodec, 40 | kind: t.literal('FILE'), 41 | path: t.string, 42 | label: t.string, 43 | depth: t.number, 44 | jobHash: jobHashCodec, 45 | fileAdded: t.boolean, 46 | }), 47 | ]); 48 | 49 | export type _ExplorerNode = t.TypeOf; 50 | -------------------------------------------------------------------------------- /src/selectors/comparePersistedJobs.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | import { Job, JobKind, PersistedJob } from '../jobs/types'; 3 | 4 | export const doesJobAddNewFile = (kind: Job['kind']): boolean => { 5 | return [ 6 | JobKind.copyFile, 7 | JobKind.createFile, 8 | JobKind.moveAndRewriteFile, 9 | JobKind.moveFile, 10 | ].includes(kind); 11 | }; 12 | 13 | export const getPersistedJobUri = (job: PersistedJob): Uri | null => { 14 | if (doesJobAddNewFile(job.kind) && job.newUri !== null) { 15 | return Uri.parse(job.newUri); 16 | } 17 | 18 | if (!doesJobAddNewFile(job.kind) && job.oldUri !== null) { 19 | return Uri.parse(job.oldUri); 20 | } 21 | 22 | return null; 23 | }; 24 | 25 | export const comparePersistedJobs = (a: PersistedJob, b: PersistedJob) => { 26 | const aUri = getPersistedJobUri(a); 27 | const bUri = getPersistedJobUri(b); 28 | 29 | if (aUri === null || bUri === null) { 30 | return 0; 31 | } 32 | 33 | return aUri.fsPath.localeCompare(bUri.fsPath); 34 | }; 35 | -------------------------------------------------------------------------------- /src/selectors/selectCodemodRunsTree.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '../data'; 2 | import { isNeitherNullNorUndefined } from '../utilities'; 3 | import { sep } from 'path'; 4 | 5 | export const selectCodemodRunsTree = (state: RootState, rootPath: string) => { 6 | const { selectedCaseHash } = state.codemodRunsTab; 7 | const dirName = rootPath.split(sep).slice(-1).join(sep); 8 | 9 | const nodeData = Object.values(state.case.entities) 10 | .filter(isNeitherNullNorUndefined) 11 | .sort((a, b) => a.createdAt - b.createdAt) 12 | .map((kase) => { 13 | const label = 14 | kase.codemodHashDigest !== undefined 15 | ? state.privateCodemods.entities[kase.codemodHashDigest] 16 | ?.name ?? 17 | state.codemod.entities[kase.codemodHashDigest]?.name ?? 18 | kase.codemodName 19 | : kase.codemodName; 20 | 21 | return { 22 | node: { 23 | hashDigest: kase.hash, 24 | label, 25 | createdAt: kase.createdAt, 26 | path: kase.path.replace(rootPath, dirName), 27 | } as const, 28 | depth: 0, 29 | expanded: true, 30 | focused: kase.hash === selectedCaseHash, 31 | collapsable: false, 32 | reviewed: false, 33 | } as const; 34 | }); 35 | 36 | return { 37 | nodeData, 38 | selectedNodeHashDigest: selectedCaseHash, 39 | } as const; 40 | }; 41 | 42 | export type CodemodRunsTree = ReturnType; 43 | -------------------------------------------------------------------------------- /src/selectors/selectErrorWebviewViewProps.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '../data'; 2 | 3 | export const selectErrorWebviewViewProps = ( 4 | state: RootState, 5 | visible: boolean, 6 | ) => { 7 | if (!visible) { 8 | return { 9 | kind: 'MAIN_WEBVIEW_VIEW_NOT_VISIBLE' as const, 10 | }; 11 | } 12 | 13 | if (state.activeTabId !== 'codemodRuns') { 14 | return { 15 | kind: 'CODEMOD_RUNS_TAB_NOT_ACTIVE' as const, 16 | }; 17 | } 18 | 19 | const caseHash = state.codemodRunsTab.selectedCaseHash; 20 | 21 | if (caseHash === null) { 22 | return { 23 | kind: 'CASE_NOT_SELECTED' as const, 24 | }; 25 | } 26 | 27 | return { 28 | kind: 'CASE_SELECTED' as const, 29 | caseHash, 30 | executionErrors: state.executionErrors[caseHash] ?? [], 31 | }; 32 | }; 33 | 34 | export type ErrorWebviewViewProps = ReturnType< 35 | typeof selectErrorWebviewViewProps 36 | >; 37 | -------------------------------------------------------------------------------- /src/selectors/selectMainWebviewViewProps.ts: -------------------------------------------------------------------------------- 1 | import type { Uri } from 'vscode'; 2 | import type { RootState } from '../data'; 3 | import { selectCodemodRunsTree } from './selectCodemodRunsTree'; 4 | import { 5 | absoluteToRelativePath, 6 | selectCodemodTree, 7 | selectPrivateCodemods, 8 | } from './selectCodemodTree'; 9 | import { selectExplorerTree } from './selectExplorerTree'; 10 | import { CodemodHash } from '../packageJsonAnalyzer/types'; 11 | import { selectSourceControlTabProps } from './selectSourceControlTabProps'; 12 | 13 | export const selectMainWebviewViewProps = ( 14 | state: RootState, 15 | rootUri: Uri | null, 16 | autocompleteItems: ReadonlyArray | null, 17 | executionQueue: ReadonlyArray, 18 | ) => { 19 | if (rootUri === null) { 20 | return null; 21 | } 22 | 23 | if (state.activeTabId === 'codemods') { 24 | return { 25 | activeTabId: state.activeTabId, 26 | toaster: state.toaster, 27 | searchPhrase: state.codemodDiscoveryView.searchPhrase, 28 | autocompleteItems: (autocompleteItems ?? []).map((item) => 29 | absoluteToRelativePath(item, rootUri.fsPath ?? ''), 30 | ), 31 | codemodTree: selectCodemodTree( 32 | state, 33 | rootUri?.fsPath ?? null, 34 | executionQueue, 35 | ), 36 | privateCodemods: selectPrivateCodemods( 37 | state, 38 | rootUri?.fsPath ?? null, 39 | executionQueue, 40 | ), 41 | rootPath: rootUri?.fsPath ?? null, 42 | publicRegistryCollapsed: 43 | state.codemodDiscoveryView.publicRegistryCollapsed, 44 | privateRegistryCollapsed: 45 | state.codemodDiscoveryView.privateRegistryCollapsed, 46 | panelGroupSettings: state.codemodDiscoveryView.panelGroupSettings, 47 | }; 48 | } 49 | 50 | if (state.activeTabId === 'codemodRuns') { 51 | return { 52 | clearingInProgress: state.clearingInProgress, 53 | activeTabId: state.activeTabId, 54 | toaster: state.toaster, 55 | applySelectedInProgress: state.applySelectedInProgress, 56 | codemodRunsTree: 57 | rootUri !== null 58 | ? selectCodemodRunsTree(state, rootUri.fsPath) 59 | : null, 60 | changeExplorerTree: 61 | rootUri !== null 62 | ? selectExplorerTree(state, rootUri.fsPath) 63 | : null, 64 | codemodExecutionInProgress: state.caseHashInProgress !== null, 65 | panelGroupSettings: state.codemodRunsTab.panelGroupSettings, 66 | resultsCollapsed: state.codemodRunsTab.resultsCollapsed, 67 | changeExplorerCollapsed: 68 | state.codemodRunsTab.changeExplorerCollapsed, 69 | }; 70 | } 71 | 72 | if (state.activeTabId === 'sourceControl') { 73 | const sourceControlTabProps = selectSourceControlTabProps(state); 74 | 75 | return { 76 | activeTabId: state.activeTabId, 77 | toaster: state.toaster, 78 | title: sourceControlTabProps?.title ?? '', 79 | body: sourceControlTabProps?.body ?? '', 80 | loading: sourceControlTabProps?.loading ?? false, 81 | }; 82 | } 83 | 84 | return { 85 | activeTabId: state.activeTabId, 86 | toaster: state.toaster, 87 | }; 88 | }; 89 | 90 | export type MainWebviewViewProps = ReturnType< 91 | typeof selectMainWebviewViewProps 92 | >; 93 | -------------------------------------------------------------------------------- /src/selectors/selectSourceControlTabProps.ts: -------------------------------------------------------------------------------- 1 | import { createBeforeAfterSnippets } from '../components/webview/IntuitaPanelProvider'; 2 | import type { RootState } from '../data'; 3 | 4 | const sanitizeCodeBlock = (codeBlock: string) => 5 | codeBlock.replace(//g, '>'); 6 | 7 | const buildIssueTemplateInHTML = ( 8 | codemodName: string, 9 | before: string | null, 10 | after: string | null, 11 | expected: string | null, 12 | ): string => { 13 | return ` 14 |
15 |

16 | ⚠️⚠️ Please do not include any proprietary code in the issue. ⚠️⚠️ 17 |

18 |
19 |

Codemod: ${codemodName}

20 |

1. Code before transformation (Input for codemod)

21 |
${
 22 | 		before !== null ? sanitizeCodeBlock(before) : '// paste code here'
 23 | 	}
24 |

2. Expected code after transformation (Desired output of codemod)

25 |
${
 26 | 		expected !== null ? sanitizeCodeBlock(expected) : '// paste code here'
 27 | 	}
28 |

3. Faulty code obtained after running the current version of the codemod (Actual output of codemod)

29 |
${
 30 | 		after !== null ? sanitizeCodeBlock(after) : '// paste code here'
 31 | 	}
32 |

Additional context

33 | You can provide any relevant context here. 34 |
35 | `; 36 | }; 37 | 38 | type SourceControlTabProps = Readonly<{ 39 | title: string; 40 | body: string; 41 | loading: boolean; 42 | }>; 43 | 44 | export const selectSourceControlTabProps = ( 45 | state: RootState, 46 | ): SourceControlTabProps | null => { 47 | const sourceControlState = state.sourceControl; 48 | 49 | if (sourceControlState.kind === 'IDLENESS') { 50 | return null; 51 | } 52 | 53 | if (sourceControlState.kind === 'ISSUE_CREATION_WAITING_FOR_AUTH') { 54 | return { 55 | title: sourceControlState.title, 56 | body: sourceControlState.body, 57 | loading: false, 58 | }; 59 | } 60 | 61 | if (sourceControlState.kind === 'WAITING_FOR_ISSUE_CREATION_API_RESPONSE') { 62 | return { 63 | title: sourceControlState.title, 64 | body: sourceControlState.body, 65 | loading: true, 66 | }; 67 | } 68 | 69 | const job = state.job.entities[sourceControlState.jobHash] ?? null; 70 | 71 | if (job === null) { 72 | return null; 73 | } 74 | 75 | const title = `[Codemod:${job.codemodName}] Invalid codemod output`; 76 | const { beforeSnippet, afterSnippet: newFileSnippet } = 77 | createBeforeAfterSnippets( 78 | sourceControlState.oldFileContent, 79 | sourceControlState.newFileContent, 80 | ); 81 | 82 | if (sourceControlState.modifiedFileContent === null) { 83 | const body = buildIssueTemplateInHTML( 84 | job.codemodName, 85 | beforeSnippet, 86 | newFileSnippet, 87 | null, 88 | ); 89 | 90 | return { 91 | title, 92 | body, 93 | loading: false, 94 | }; 95 | } 96 | 97 | const { afterSnippet: modifiedFileSnippet } = createBeforeAfterSnippets( 98 | sourceControlState.oldFileContent, 99 | sourceControlState.modifiedFileContent, 100 | ); 101 | 102 | const body = buildIssueTemplateInHTML( 103 | job.codemodName, 104 | beforeSnippet, 105 | newFileSnippet, 106 | modifiedFileSnippet, 107 | ); 108 | 109 | return { 110 | title, 111 | body, 112 | loading: false, 113 | }; 114 | }; 115 | -------------------------------------------------------------------------------- /src/telemetry/hashes.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'node:crypto'; 2 | import type { CaseHash } from '../cases/types'; 3 | 4 | export const buildCaseHash = (): CaseHash => 5 | randomBytes(20).toString('base64url') as CaseHash; 6 | -------------------------------------------------------------------------------- /src/telemetry/telemetry.ts: -------------------------------------------------------------------------------- 1 | import type { CaseHash } from '../cases/types'; 2 | 3 | export type ErrorEvent = 4 | | Readonly<{ 5 | kind: 'failedToExecuteCommand'; 6 | commandName: string; 7 | }> 8 | | Readonly<{ 9 | kind: 'failedToBootstrapEngines'; 10 | message: string; 11 | }>; 12 | 13 | export type Event = 14 | | Readonly<{ 15 | kind: 'codemodExecuted'; 16 | fileCount: number; 17 | executionId: CaseHash; 18 | codemodName: string; 19 | }> 20 | | Readonly<{ 21 | kind: 'codemodHalted'; 22 | fileCount: number; 23 | executionId: CaseHash; 24 | codemodName: string; 25 | }> 26 | | Readonly<{ 27 | kind: 'jobsAccepted'; 28 | jobCount: number; 29 | executionId: CaseHash; 30 | }> 31 | | Readonly<{ 32 | kind: 'jobsRejected'; 33 | jobCount: number; 34 | executionId: CaseHash; 35 | }>; 36 | 37 | export interface Telemetry { 38 | sendEvent(event: Event): void; 39 | 40 | sendError(error: ErrorEvent): void; 41 | } 42 | -------------------------------------------------------------------------------- /src/telemetry/vscodeTelemetry.ts: -------------------------------------------------------------------------------- 1 | import TelemetryReporter from '@vscode/extension-telemetry'; 2 | import { Event, ErrorEvent, Telemetry } from './telemetry'; 3 | import { Message, MessageBus, MessageKind } from '../components/messageBus'; 4 | import { Job } from '../jobs/types'; 5 | import { CaseHash } from '../cases/types'; 6 | 7 | export class VscodeTelemetry implements Telemetry { 8 | constructor( 9 | private readonly __telemetryReporter: TelemetryReporter, 10 | private readonly __messageBus: MessageBus, 11 | ) { 12 | this.__messageBus.subscribe( 13 | MessageKind.codemodSetExecuted, 14 | (message) => { 15 | this.__onCodemodSetExecuted(message); 16 | }, 17 | ); 18 | 19 | this.__messageBus.subscribe(MessageKind.jobsAccepted, (message) => 20 | this.__onJobsAcceptedMessage(message), 21 | ); 22 | 23 | this.__messageBus.subscribe(MessageKind.jobsRejected, (message) => 24 | this.__onJobsRejectedMessage(message), 25 | ); 26 | } 27 | 28 | __onJobsAcceptedMessage( 29 | message: Message & { kind: MessageKind.jobsAccepted }, 30 | ): void { 31 | const { deletedJobs } = message; 32 | 33 | const jobsByExecution: Record = {}; 34 | 35 | for (const job of deletedJobs) { 36 | const { caseHashDigest } = job; 37 | 38 | if (!jobsByExecution[caseHashDigest]) { 39 | jobsByExecution[caseHashDigest] = []; 40 | } 41 | 42 | jobsByExecution[caseHashDigest]?.push(job); 43 | } 44 | 45 | for (const [caseHashDigest, jobs] of Object.entries(jobsByExecution)) { 46 | this.sendEvent({ 47 | kind: 'jobsAccepted', 48 | jobCount: jobs.length, 49 | executionId: caseHashDigest as CaseHash, 50 | }); 51 | } 52 | } 53 | 54 | __onJobsRejectedMessage( 55 | message: Message & { kind: MessageKind.jobsRejected }, 56 | ): void { 57 | const { deletedJobs } = message; 58 | 59 | const jobsByExecution: Record = {}; 60 | 61 | for (const job of deletedJobs) { 62 | const { caseHashDigest } = job; 63 | 64 | if (!jobsByExecution[caseHashDigest]) { 65 | jobsByExecution[caseHashDigest] = []; 66 | } 67 | 68 | jobsByExecution[caseHashDigest]?.push(job); 69 | } 70 | 71 | for (const [caseHashDigest, jobs] of Object.entries(jobsByExecution)) { 72 | this.sendEvent({ 73 | kind: 'jobsRejected', 74 | jobCount: jobs.length, 75 | executionId: caseHashDigest as CaseHash, 76 | }); 77 | } 78 | } 79 | 80 | __onCodemodSetExecuted( 81 | message: Message & { kind: MessageKind.codemodSetExecuted }, 82 | ): void { 83 | this.sendEvent({ 84 | kind: message.halted ? 'codemodHalted' : 'codemodExecuted', 85 | executionId: message.case.hash, 86 | fileCount: message.jobs.length, 87 | codemodName: message.case.codemodName, 88 | }); 89 | } 90 | 91 | __rawEventToTelemetryEvent(event: ErrorEvent | Event): { 92 | properties: Record; 93 | measurements: Record; 94 | name: string; 95 | } { 96 | const properties: Record = {}; 97 | const measurements: Record = {}; 98 | 99 | for (const [key, value] of Object.entries(event)) { 100 | if (typeof value === 'string') { 101 | properties[key] = value; 102 | continue; 103 | } 104 | 105 | if (typeof value === 'number') { 106 | measurements[key] = value; 107 | continue; 108 | } 109 | } 110 | 111 | return { 112 | name: event.kind, 113 | properties, 114 | measurements, 115 | }; 116 | } 117 | 118 | sendEvent(event: Event): void { 119 | const { name, properties, measurements } = 120 | this.__rawEventToTelemetryEvent(event); 121 | 122 | this.__telemetryReporter.sendTelemetryEvent( 123 | name, 124 | properties, 125 | measurements, 126 | ); 127 | } 128 | 129 | sendError(event: ErrorEvent): void { 130 | const { name, properties, measurements } = 131 | this.__rawEventToTelemetryEvent(event); 132 | this.__telemetryReporter.sendTelemetryErrorEvent( 133 | name, 134 | properties, 135 | measurements, 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/types/reset.d.ts: -------------------------------------------------------------------------------- 1 | import '@total-typescript/ts-reset'; 2 | -------------------------------------------------------------------------------- /src/types/vscode.d.ts: -------------------------------------------------------------------------------- 1 | // applied from https://github.com/microsoft/vscode/blob/main/src/vscode-dts/vscode.proposed.treeItemCheckbox.d.ts 2 | // remove when @types/vscode catches up 3 | 4 | /*--------------------------------------------------------------------------------------------- 5 | * Copyright (c) Microsoft Corporation. All rights reserved. 6 | * Licensed under the MIT License. See License.txt in the project root for license information. 7 | *--------------------------------------------------------------------------------------------*/ 8 | 9 | declare module 'vscode' { 10 | export class TreeItem2 extends TreeItem { 11 | /** 12 | * [TreeItemCheckboxState](#TreeItemCheckboxState) of the tree item. 13 | */ 14 | checkboxState?: 15 | | TreeItemCheckboxState 16 | | { 17 | readonly state: TreeItemCheckboxState; 18 | readonly tooltip?: string; 19 | }; 20 | } 21 | 22 | /** 23 | * Checkbox state of the tree item 24 | */ 25 | export enum TreeItemCheckboxState { 26 | /** 27 | * Determines an item is unchecked 28 | */ 29 | Unchecked = 0, 30 | /** 31 | * Determines an item is checked 32 | */ 33 | Checked = 1, 34 | } 35 | 36 | /** 37 | * A data provider that provides tree data 38 | */ 39 | export interface TreeView { 40 | /** 41 | * An event to signal that an element or root has either been checked or unchecked. 42 | */ 43 | onDidChangeCheckboxState: Event>; 44 | } 45 | 46 | export interface TreeCheckboxChangeEvent { 47 | /** 48 | * The item that was checked or unchecked. 49 | */ 50 | readonly items: ReadonlyArray<[T, TreeItemCheckboxState]>; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/uris/buildUriHash.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | import { buildHash } from '../utilities'; 3 | import { UriHash } from './types'; 4 | 5 | export const buildUriHash = (uri: Pick): UriHash => { 6 | return buildHash(uri.toString()) as UriHash; 7 | }; 8 | -------------------------------------------------------------------------------- /src/uris/types.ts: -------------------------------------------------------------------------------- 1 | export type UriHash = string & { __UriHash: '__UriHash' }; 2 | -------------------------------------------------------------------------------- /test/dowloadService.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, afterEach, vi, expect } from 'vitest'; 2 | import { DownloadService } from '../src/components/downloadService'; 3 | import type { FileSystem } from 'vscode'; 4 | import { AxiosError, AxiosInstance } from 'axios'; 5 | import nock from 'nock'; 6 | import { retryingClient as axiosInstance } from '../src/axios'; 7 | 8 | const mockedFileSystemUtilities = { 9 | getModificationTime: vi.fn(() => 1), 10 | setChmod: vi.fn(), 11 | }; 12 | 13 | const fs = { 14 | writeFile: vi.fn(), 15 | stat: vi.fn(() => ({ mtime: Date.now() })), 16 | } as unknown as FileSystem; 17 | 18 | const downloadService = new DownloadService( 19 | fs, 20 | // @ts-ignore 21 | mockedFileSystemUtilities, 22 | ); 23 | 24 | const NETWORK_ERROR = new AxiosError('Some connection error'); 25 | NETWORK_ERROR.code = 'ECONNRESET'; 26 | 27 | // 3 failed responses, then good response 28 | const responses = [ 29 | () => nock('https://test.com').head('/test').replyWithError(NETWORK_ERROR), 30 | () => nock('https://test.com').head('/test').replyWithError(NETWORK_ERROR), 31 | () => nock('https://test.com').head('/test').replyWithError(NETWORK_ERROR), 32 | () => 33 | nock('https://test.com') 34 | .head('/test') 35 | .reply(200, '', { 'last-modified': new Date(2).toISOString() }), 36 | () => nock('https://test.com').get('/test').replyWithError(NETWORK_ERROR), 37 | () => nock('https://test.com').get('/test').replyWithError(NETWORK_ERROR), 38 | () => nock('https://test.com').get('/test').replyWithError(NETWORK_ERROR), 39 | () => nock('https://test.com').get('/test').reply(200, 'Test'), 40 | ]; 41 | 42 | const setupResponses = (client: AxiosInstance, responses: Array) => { 43 | const configureResponse = () => { 44 | const response = responses.shift(); 45 | if (response) { 46 | response(); 47 | } 48 | }; 49 | 50 | client.interceptors.request.use( 51 | (config) => { 52 | configureResponse(); 53 | return config; 54 | }, 55 | (error) => { 56 | configureResponse(); 57 | return Promise.reject(error); 58 | }, 59 | ); 60 | }; 61 | 62 | describe('DownloadService', () => { 63 | afterEach(() => { 64 | nock.cleanAll(); 65 | nock.enableNetConnect(); 66 | }); 67 | 68 | test('Should retry 3 times if request fails', async () => { 69 | setupResponses(axiosInstance, responses); 70 | 71 | await downloadService.downloadFileIfNeeded( 72 | `https://test.com/test`, 73 | // @ts-expect-error passing a string instead of URI, because URI cannot be imported from vscode 74 | '/', 75 | '755', 76 | ); 77 | 78 | expect(fs.writeFile).toBeCalledWith( 79 | '/', 80 | new Uint8Array([84, 101, 115, 116]), 81 | ); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "module": "commonjs", 5 | "target": "ES2020", 6 | "lib": ["ES2020"], 7 | "sourceMap": false, 8 | "strict": true, 9 | "noUncheckedIndexedAccess": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noUnusedParameters": true, 13 | "noUnusedLocals": true, 14 | "esModuleInterop": true, 15 | "skipLibCheck": true 16 | }, 17 | "include": ["./src/**/*.ts", "./src/types/**/*.d.ts", "./test/**/*.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | //@ts-check 8 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 9 | 10 | /** @type WebpackConfig */ 11 | const extensionConfig = { 12 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 13 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 14 | 15 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 16 | output: { 17 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 18 | path: path.resolve(__dirname, 'dist'), 19 | filename: 'extension.js', 20 | libraryTarget: 'commonjs2', 21 | }, 22 | externals: { 23 | vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 24 | // modules added here also need to be added in the .vscodeignore file 25 | fsevents: "require('fsevents')", 26 | }, 27 | resolve: { 28 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 29 | extensions: ['.ts', '.js'], 30 | }, 31 | module: { 32 | // noParse: [ 33 | // require.resolve("@ts-morph/common/dist/typescript.js") 34 | // ], 35 | rules: [ 36 | { 37 | test: /node_modules[\\|/]code-block-writer[\\|/]umd[\\|/]/, 38 | use: { loader: 'umd-compat-loader' }, 39 | }, 40 | { 41 | test: /\.ts$/, 42 | exclude: /node_modules/, 43 | use: [ 44 | { 45 | loader: 'ts-loader', 46 | }, 47 | ], 48 | }, 49 | ], 50 | }, 51 | devtool: 'nosources-source-map', 52 | infrastructureLogging: { 53 | level: 'log', // enables logging required for problem matchers 54 | }, 55 | }; 56 | module.exports = [extensionConfig]; 57 | --------------------------------------------------------------------------------