Repository: sleistner/vscode-fileutils Branch: master Commit: 54fc9407e18f Files: 73 Total size: 122.3 KB Directory structure: gitextract_xq5fqy4z/ ├── .biomeignore ├── .claude/ │ └── settings.local.json ├── .devcontainer/ │ ├── Dockerfile │ └── devcontainer.json ├── .editorconfig ├── .eslintignore ├── .github/ │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ └── main.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .node-version ├── .releaserc ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── biome.json ├── main.ts ├── package.json ├── renovate.json ├── scripts/ │ └── dev-env ├── src/ │ ├── FileItem.ts │ ├── command/ │ │ ├── BaseCommand.ts │ │ ├── Command.ts │ │ ├── CopyFileNameCommand.ts │ │ ├── DuplicateFileCommand.ts │ │ ├── MoveFileCommand.ts │ │ ├── NewFileCommand.ts │ │ ├── NewFolderCommand.ts │ │ ├── RemoveFileCommand.ts │ │ ├── RenameFileCommand.ts │ │ └── index.ts │ ├── controller/ │ │ ├── BaseFileController.ts │ │ ├── CopyFileNameController.ts │ │ ├── DuplicateFileController.ts │ │ ├── FileController.ts │ │ ├── MoveFileController.ts │ │ ├── NewFileController.ts │ │ ├── RemoveFileController.ts │ │ ├── RenameFileController.ts │ │ ├── TypeAheadController.ts │ │ └── index.ts │ ├── extension.ts │ └── lib/ │ ├── Cache.ts │ ├── TreeWalker.ts │ └── config.ts ├── test/ │ ├── command/ │ │ ├── CopyFileNameCommand.test.ts │ │ ├── DuplicateFileCommand.test.ts │ │ ├── MoveFileCommand.test.ts │ │ ├── NewFileCommand.test.ts │ │ ├── RemoveFileCommand.test.ts │ │ └── RenameFileCommand.test.ts │ ├── fixtures/ │ │ ├── file-1.rb │ │ └── file-2.rb │ ├── helper/ │ │ ├── callbacks.ts │ │ ├── environment.ts │ │ ├── functions.ts │ │ ├── index.ts │ │ ├── steps/ │ │ │ ├── describe.ts │ │ │ ├── index.ts │ │ │ ├── it.ts │ │ │ └── types.ts │ │ └── stubs.ts │ ├── index.ts │ └── runTest.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .biomeignore ================================================ out/ node_modules/ **/*.d.ts .vscode-test/ *.vsix ================================================ FILE: .claude/settings.local.json ================================================ { "permissions": { "allow": [ "Bash(npm run pretest:*)", "Bash(grep:*)", "Bash(npm test)", "Bash(VSCODE_TEST_ELECTRON_PATH= npm test)", "Bash(mkdir:*)" ], "deny": [] } } ================================================ FILE: .devcontainer/Dockerfile ================================================ #------------------------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. #------------------------------------------------------------------------------------------------------------- FROM node:19 # Avoid warnings by switching to noninteractive ENV DEBIAN_FRONTEND=noninteractive # Configure apt and install packages RUN apt-get update \ && apt-get -y install --no-install-recommends apt-utils 2>&1 \ # # Verify git and needed tools are installed && apt-get install -y git procps \ # # Remove outdated npm from /opt and install via package # so it can be easily updated via apt-get upgrade npm && rm -rf /opt/npm-* \ && rm -f /usr/local/bin/npm \ && rm -f /usr/local/bin/npmpkg \ && apt-get install -y curl apt-transport-https lsb-release \ && curl -sS https://dl.npmpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \ && echo "deb https://dl.npmpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/npm.list \ && apt-get update \ && apt-get -y install --no-install-recommends npm \ # # Install tslint and typescript globally && npm install -g tslint typescript \ # # Clean up && apt-get autoremove -y \ && apt-get clean -y \ && rm -rf /var/lib/apt/lists/* # Switch back to dialog for any ad-hoc use of apt-get ENV DEBIAN_FRONTEND=dialog ================================================ FILE: .devcontainer/devcontainer.json ================================================ // See https://aka.ms/vscode-remote/devcontainer.json for format details. { "name": "Node.js 8 & TypeScript", "dockerFile": "Dockerfile", "extensions": [ "ms-vscode.vscode-typescript-tslint-plugin", "sleistner.vscode-fileutils" ] } ================================================ FILE: .editorconfig ================================================ root = true [*] max_line_length = 120 insert_final_newline = true trim_trailing_whitespace = true indent_style = space indent_size = 4 [{*.yml, *.yaml, *.sh, package.json}] indent_size = 2 [*.md] trim_trailing_whitespace = false ================================================ FILE: .eslintignore ================================================ "**/*.js" ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing Contributing is easy: * You can report bugs and request features using the [issues page][issues]. [issues]: https://github.com/sleistner/vscode-fileutils/issues We love pull requests from everyone: * Fork the project * Download source code and install dependencies ```bash git clone git@github.com:your-username/vscode-fileutils.git cd vscode-fileutils npm install code . ``` * Make the respective code changes. * Go to the debugger in VS Code, choose `Launch Extension` and click run. You can test your changes. * Choose `Launch Tests` to run the tests. * Push to your fork and [submit a pull request][pr]. [pr]: https://github.com/sleistner/vscode-fileutils/compare/ At this point you're waiting on us. We like to at least comment on pull requests as soon as possible. We may suggest some changes or improvements or alternatives. **Important:** Release and changleog update are executed as TravisCI job. Please write commit messages considering Angular Commit Message Conventions. * https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits * https://blog.greenkeeper.io/introduction-to-semantic-release-33f73b117c8 ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - VSCode Version: - OS Version: - FileUtils Extension Version: **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ # Description Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. Fixes # (issue) # Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules ================================================ FILE: .github/workflows/main.yml ================================================ name: CI/CD on: push: branches: - master pull_request: branches: - master release: types: - published concurrency: group: ci-fileutils-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: lint-test: name: Lint, Test strategy: matrix: os: - ubuntu-latest - macos-latest runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v4 with: persist-credentials: false - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22.x cache: "npm" - name: Install dependencies run: npm ci - name: Run code analysis run: npm run lint if: runner.os == 'Linux' - name: Run tests on Linux run: | sudo apt-get --assume-yes install libsecret-1-0 xclip; /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & xvfb-run -a npm run test env: DISPLAY: ":99.0" if: runner.os == 'Linux' - name: Run tests on macOS run: npm run test if: runner.os != 'Linux' release: name: Release runs-on: ubuntu-latest needs: [lint-test] if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} steps: - name: Checkout uses: actions/checkout@v4 with: persist-credentials: false - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22.x cache: "npm" - name: Install dependencies run: npm ci - name: Run semantic-release run: npm run semantic-release env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} VSCE_PAT: ${{ secrets.VSCE_PAT }} OVSX_PAT: ${{ secrets.OVSX_PAT }} ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory node_modules # Optional npm cache directory .npm # Optional REPL history .node_repl_history out .vscode-test .idea *.vsix tmp ================================================ FILE: .husky/pre-commit ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npm run lint && npm run test ================================================ FILE: .node-version ================================================ 22 ================================================ FILE: .releaserc ================================================ { "branch": "master", "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/npm", "@semantic-release/changelog", "@semantic-release/git", "@semantic-release/github" ], "prepare": [ "@semantic-release/npm", "@semantic-release/changelog", "@semantic-release/git", { "path": "semantic-release-vsce", "packageVsix": "sleistner.vscode-fileutils.vsix" } ], "publish": [ "semantic-release-vsce", { "path": "@semantic-release/github", "assets": "sleistner.vscode-fileutils.vsix" } ] } ================================================ FILE: .vscode/extensions.json ================================================ { // See http://go.microsoft.com/fwlink/?LinkId=827846 // for the documentation about the extensions.json format "recommendations": [ "dbaeumer.vscode-eslint", "editorconfig.editorconfig", "esbenp.prettier-vscode", "connor4312.esbuild-problem-matchers" ] } ================================================ FILE: .vscode/launch.json ================================================ // A launch configuration that compiles the extension and then opens it inside a new window // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { "version": "0.2.0", "configurations": [ { "name": "Launch Extension", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--folder-uri=${workspaceFolder}/tmp"], "outFiles": ["${workspaceFolder}/out/src/**/*.js"], "preLaunchTask": "npm: watch" }, { "name": "Launch Tests", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": [ "${workspaceFolder}/test", "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test" ], "outFiles": ["${workspaceFolder}/out/test/**/*.js"], "preLaunchTask": "npm: tsc:watch" } ] } ================================================ FILE: .vscode/settings.json ================================================ // Place your settings in this file to overwrite default and user settings. { "files.exclude": { "out": false // set this to true to hide the "out" folder with the compiled JS files }, "search.exclude": { "out": true // set this to false to include "out" folder in search results }, "editor.codeActionsOnSave": { "source.organizeImports": true } } ================================================ FILE: .vscode/tasks.json ================================================ { "version": "2.0.0", "tasks": [ { "type": "npm", "script": "watch", "group": { "kind": "build", "isDefault": true }, "problemMatcher": "$esbuild-watch", "isBackground": true, "label": "npm: watch" }, { "type": "npm", "script": "tsc:watch", "isBackground": true, "presentation": { "reveal": "never" }, "group": { "kind": "build", "isDefault": false }, "problemMatcher": ["$tsc-watch"], "label": "npm: tsc:watch" } ] } ================================================ FILE: .vscodeignore ================================================ * */** !images/icon.* !README.md !CHANGELOG.md !LICENSE !out/extension.js ================================================ FILE: CHANGELOG.md ================================================ ## [3.10.3](https://github.com/sleistner/vscode-fileutils/compare/v3.10.2...v3.10.3) (2023-07-22) ### Bug Fixes * **deps:** update dependency fast-glob to v3.3.1 ([5581fa3](https://github.com/sleistner/vscode-fileutils/commit/5581fa33c582fc43bcc2f951570db60b367b9928)) ## [3.10.2](https://github.com/sleistner/vscode-fileutils/compare/v3.10.1...v3.10.2) (2023-06-30) ### Bug Fixes * **deps:** update dependency fast-glob to v3.3.0 ([ad9f56d](https://github.com/sleistner/vscode-fileutils/commit/ad9f56d5a07abc9d9a0a48cc4a03ea3fcd8b4e35)) ## [3.10.1](https://github.com/sleistner/vscode-fileutils/compare/v3.10.0...v3.10.1) (2023-03-21) ### Bug Fixes * rename commands to comply with VSCode style guides ([72f6843](https://github.com/sleistner/vscode-fileutils/commit/72f6843c02d6cbec5d5588a27ef8097e1130e68e)) # [3.10.0](https://github.com/sleistner/vscode-fileutils/compare/v3.9.3...v3.10.0) (2023-01-30) ### Features * **Settings:** add option to disable context menus ([f3b1431](https://github.com/sleistner/vscode-fileutils/commit/f3b143134f62337a1082ecf30b4588e7dcfab7ae)) ## [3.9.3](https://github.com/sleistner/vscode-fileutils/compare/v3.9.2...v3.9.3) (2023-01-30) ### Bug Fixes * **NewFileController:** show workspace selector when relative to root ([e3fcf96](https://github.com/sleistner/vscode-fileutils/commit/e3fcf962ec74f5b803da963ba9bdfe4676f83aeb)) ## [3.9.2](https://github.com/sleistner/vscode-fileutils/compare/v3.9.1...v3.9.2) (2023-01-28) ### Bug Fixes * **MoveFileController:** disable brace expansion ([8874f26](https://github.com/sleistner/vscode-fileutils/commit/8874f2664c62035e31ad61656fa18d018f5fc36a)) ## [3.9.1](https://github.com/sleistner/vscode-fileutils/compare/v3.9.0...v3.9.1) (2023-01-25) ### Bug Fixes * **build:** include changelog ([550e190](https://github.com/sleistner/vscode-fileutils/commit/550e19025a52bd842e76021045a2786c4f4a8758)) # [3.9.0](https://github.com/sleistner/vscode-fileutils/compare/v3.8.0...v3.9.0) (2023-01-25) ### Bug Fixes * **RenameFileController:** always append base path ([cd6f352](https://github.com/sleistner/vscode-fileutils/commit/cd6f35276331f5ff8b73dbd88443f7f0952b6cc8)) ### Features * add inputBox pathTypeIndicator setting ([3390544](https://github.com/sleistner/vscode-fileutils/commit/3390544f5f75ac6dc481827b485a3113447d4b3b)) * **inputBox:** add path representation configuration ([82e7364](https://github.com/sleistner/vscode-fileutils/commit/82e7364b67551388a1cc58ac1ee8122975f835aa)) # [3.8.0](https://github.com/sleistner/vscode-fileutils/compare/v3.7.0...v3.8.0) (2023-01-23) ### Features * **DuplicateFile:** add typeahead support ([44ac603](https://github.com/sleistner/vscode-fileutils/commit/44ac603dd241eb61e3732172ffc6cfe80555e0c0)) * **MoveFile:** add typeahead support ([0e3e0ca](https://github.com/sleistner/vscode-fileutils/commit/0e3e0ca1f5886926758987b594461d57980f750c)) * **NewFile:** add typeahead setting ([764f614](https://github.com/sleistner/vscode-fileutils/commit/764f614e8e7a8b8d350bd3ad68f785c695bf5e01)) * **NewFolder:** add dedicated typeahead setting ([6d4359a](https://github.com/sleistner/vscode-fileutils/commit/6d4359a5f7bb4689b22d2ba7b3762c9b602839fb)) # [3.7.0](https://github.com/sleistner/vscode-fileutils/compare/v3.6.0...v3.7.0) (2023-01-23) ### Features * publish to open vsx repository ([f0d643f](https://github.com/sleistner/vscode-fileutils/commit/f0d643f92aae168505ca750cf9a020cb5610840e)) # [3.6.0](https://github.com/sleistner/vscode-fileutils/compare/v3.5.0...v3.6.0) (2023-01-23) ### Bug Fixes * **TreeWalker:** replace workspace.findFiles in favor of fast-glob ([37c5078](https://github.com/sleistner/vscode-fileutils/commit/37c50781b4025e31c2023ea568aa78b4ad66714d)) ## [3.5.1](https://github.com/sleistner/vscode-fileutils/compare/v3.5.0...v3.5.1) (2023-01-02) ### Bug Fixes * **ci:** update semantic release ([6c12e92](https://github.com/sleistner/vscode-fileutils/commit/6c12e92409a9a04d92193781ad1fdb8e17c99ea1)) # [3.5.0](https://github.com/sleistner/vscode-fileutils/compare/v3.4.6...v3.5.0) (2022-01-18) ### Features * **ci:** enable github actions ([3eead61](https://github.com/sleistner/vscode-fileutils/commit/3eead61d04d6adf1632503d29939e2c150147d87)) ## [3.4.6](https://github.com/sleistner/vscode-fileutils/compare/v3.4.5...v3.4.6) (2022-01-14) ### Bug Fixes * trigger gh actions release pipeline ([d9a59c9](https://github.com/sleistner/vscode-fileutils/commit/d9a59c9f974ceb2be6407aed64131e5761acb48a)) ## [3.4.5](https://github.com/sleistner/vscode-fileutils/compare/v3.4.4...v3.4.5) (2021-02-22) ### Bug Fixes * **deps:** update dependency brace-expansion to v2.0.1 ([dd094d0](https://github.com/sleistner/vscode-fileutils/commit/dd094d0)) ## [3.4.4](https://github.com/sleistner/vscode-fileutils/compare/v3.4.3...v3.4.4) (2021-02-01) ### Bug Fixes * prefer uri over current editor ([e63b27f](https://github.com/sleistner/vscode-fileutils/commit/e63b27f)) ## [3.4.3](https://github.com/sleistner/vscode-fileutils/compare/v3.4.2...v3.4.3) (2021-01-06) ### Bug Fixes * **NewFileController:** properly brace expand backslash paths ([ff95aae](https://github.com/sleistner/vscode-fileutils/commit/ff95aae)) ## [3.4.2](https://github.com/sleistner/vscode-fileutils/compare/v3.4.1...v3.4.2) (2020-11-17) ### Bug Fixes * **build:** include README ([b724700](https://github.com/sleistner/vscode-fileutils/commit/b724700)) ## [3.4.1](https://github.com/sleistner/vscode-fileutils/compare/v3.4.0...v3.4.1) (2020-11-08) ### Bug Fixes * **build:** include node_modules ([a28b0da](https://github.com/sleistner/vscode-fileutils/commit/a28b0da)) # [3.4.0](https://github.com/sleistner/vscode-fileutils/compare/v3.3.3...v3.4.0) (2020-11-06) ### Bug Fixes * **readme:** trigger release ([9314428](https://github.com/sleistner/vscode-fileutils/commit/9314428)) ### Features * **NewFileCommand:** add support for brace expansion ([5e06afc](https://github.com/sleistner/vscode-fileutils/commit/5e06afc)) ## [3.3.3](https://github.com/sleistner/vscode-fileutils/compare/v3.3.2...v3.3.3) (2020-10-26) ### Bug Fixes * **New Folder or File Relative to Current View:** cancel execution if no editor is open ([858fea6](https://github.com/sleistner/vscode-fileutils/commit/858fea6)) ## [3.3.2](https://github.com/sleistner/vscode-fileutils/compare/v3.3.1...v3.3.2) (2020-10-26) ### Bug Fixes * **package:** update extension main file entry ([4892f84](https://github.com/sleistner/vscode-fileutils/commit/4892f84)) ## [3.3.1](https://github.com/sleistner/vscode-fileutils/compare/v3.3.0...v3.3.1) (2020-10-25) ### Bug Fixes * **duplicate:** prevent directories to be opened as document ([dc1c9f0](https://github.com/sleistner/vscode-fileutils/commit/dc1c9f0)) # [3.3.0](https://github.com/sleistner/vscode-fileutils/compare/v3.2.0...v3.3.0) (2020-10-25) ### Features * **menus:** add file releated commands to tab and editor context ([a8b748e](https://github.com/sleistner/vscode-fileutils/commit/a8b748e)) # [3.2.0](https://github.com/sleistner/vscode-fileutils/compare/v3.1.1...v3.2.0) (2020-10-25) ### Features * update icon ([5c2156b](https://github.com/sleistner/vscode-fileutils/commit/5c2156b)) ## [3.1.1](https://github.com/sleistner/vscode-fileutils/compare/v3.1.0...v3.1.1) (2020-10-23) ### Bug Fixes * **Rename, Move:** keep file in editor group ([5478345](https://github.com/sleistner/vscode-fileutils/commit/5478345)) # [3.1.0](https://github.com/sleistner/vscode-fileutils/compare/v3.0.1...v3.1.0) (2020-10-18) ### Features * **move/rename:** trigger update imports when moving file ([7a40237](https://github.com/sleistner/vscode-fileutils/commit/7a40237)) ## [3.0.1](https://github.com/sleistner/vscode-fileutils/compare/v3.0.0...v3.0.1) (2020-01-15) ### Bug Fixes * **FileItem:** ensure file exists before deleting it ([7a44326](https://github.com/sleistner/vscode-fileutils/commit/7a44326)) # [3.0.0](https://github.com/sleistner/vscode-fileutils/compare/v2.14.9...v3.0.0) (2019-09-03) ### Bug Fixes * **TreeWalker:** handle large directory structures safely ([c419c78](https://github.com/sleistner/vscode-fileutils/commit/c419c78)) ### BREAKING CHANGES * **TreeWalker:** The configuration option "typeahead.exclude" has been removed in favour of VS Code native "files.exclude" option. ## [2.14.9](https://github.com/sleistner/vscode-fileutils/compare/v2.14.8...v2.14.9) (2019-08-26) ### Bug Fixes * **RemoveFileCommand:** ensure only delete file tab was closed ([557e794](https://github.com/sleistner/vscode-fileutils/commit/557e794)) ## [2.14.8](https://github.com/sleistner/vscode-fileutils/compare/v2.14.7...v2.14.8) (2019-08-26) ### Bug Fixes * **NewFileCommand:** show quickpick on large directory structures ([8c8c537](https://github.com/sleistner/vscode-fileutils/commit/8c8c537)) ## [2.14.7](https://github.com/sleistner/vscode-fileutils/compare/v2.14.6...v2.14.7) (2019-08-23) ### Bug Fixes * **NewFileCommand:** show folder selector ([38fb33f](https://github.com/sleistner/vscode-fileutils/commit/38fb33f)) ## [2.14.6](https://github.com/sleistner/vscode-fileutils/compare/v2.14.5...v2.14.6) (2019-08-20) ### Bug Fixes * missing callback in remote environments ([63ef29a](https://github.com/sleistner/vscode-fileutils/commit/63ef29a)) ## [2.14.5](https://github.com/sleistner/vscode-fileutils/compare/v2.14.4...v2.14.5) (2019-06-03) ### Bug Fixes * **CopyFileName:** forward and process tab uri ([68ae985](https://github.com/sleistner/vscode-fileutils/commit/68ae985)) ## [2.14.4](https://github.com/sleistner/vscode-fileutils/compare/v2.14.3...v2.14.4) (2019-05-29) ### Bug Fixes * **FileItem:** update trash import ([850dfff](https://github.com/sleistner/vscode-fileutils/commit/850dfff)) * **package:** update trash to version 5.0.0 ([51f7017](https://github.com/sleistner/vscode-fileutils/commit/51f7017)) ## [2.14.3](https://github.com/sleistner/vscode-fileutils/compare/v2.14.2...v2.14.3) (2019-05-29) ### Bug Fixes * **contribution:** reorder conext menu items ([2883402](https://github.com/sleistner/vscode-fileutils/commit/2883402)) ## [2.14.2](https://github.com/sleistner/vscode-fileutils/compare/v2.14.1...v2.14.2) (2019-05-29) ### Bug Fixes * **package:** update fs-extra to version 8.0.0 ([86ff0b9](https://github.com/sleistner/vscode-fileutils/commit/86ff0b9)) ## [2.14.1](https://github.com/sleistner/vscode-fileutils/compare/v2.14.0...v2.14.1) (2019-05-29) ### Bug Fixes * icon position ([a273e32](https://github.com/sleistner/vscode-fileutils/commit/a273e32)) # [2.14.0](https://github.com/sleistner/vscode-fileutils/compare/v2.13.7...v2.14.0) (2019-05-29) ### Features * **editor/title/context:** add rename, remove and copy command ([bb0482e](https://github.com/sleistner/vscode-fileutils/commit/bb0482e)) ## [2.13.7](https://github.com/sleistner/vscode-fileutils/compare/v2.13.6...v2.13.7) (2019-04-20) ### Bug Fixes * icon color ([21f4eb4](https://github.com/sleistner/vscode-fileutils/commit/21f4eb4)) ## [2.13.6](https://github.com/sleistner/vscode-fileutils/compare/v2.13.5...v2.13.6) (2019-04-20) ### Bug Fixes * **NewFileCommand:** prompt to select workspace ([8335975](https://github.com/sleistner/vscode-fileutils/commit/8335975)) ## [2.13.4](https://github.com/sleistner/vscode-fileutils/compare/v2.13.3...v2.13.4) (2019-01-03) ### Bug Fixes * **README:** remove unsupported category ([4a13e08](https://github.com/sleistner/vscode-fileutils/commit/4a13e08)) ## [2.13.3](https://github.com/sleistner/vscode-fileutils/compare/v2.13.2...v2.13.3) (2018-11-11) ### Bug Fixes * **releaserc:** enable release notes plugin ([b88a7c6](https://github.com/sleistner/vscode-fileutils/commit/b88a7c6)) * **releaserc:** enable release notes plugin ([eabac50](https://github.com/sleistner/vscode-fileutils/commit/eabac50)) ## 2.13.0 (2018-11-10) ### Features - `File: Rename` - `File: Move` [iliashkolyar](https://github.com/iliashkolyar) Add configuration to support whether to close old tabs [PR#67](https://github.com/sleistner/vscode-fileutils/pull/67) ## 2.12.0 (2018-11-02) ### Fixes - [iliashkolyar](https://github.com/iliashkolyar) Support file operations on non-textual files [PR#63](https://github.com/sleistner/vscode-fileutils/pull/63) ### Features - `File: Copy Name Of Active File` [iliashkolyar](https://github.com/iliashkolyar) Support copy name of active file [PR#61](https://github.com/sleistner/vscode-fileutils/pull/61) ## 2.10.3 (2018-06-15) ### Fixes - `File: New File`, Show quick pick view only if more than 1 choice available. ## 2.10.0 (2018-06-14) ### Features - `File: New File`, Autocomplete paths when creating a new file. [PR#48](https://github.com/sleistner/vscode-fileutils/pull/48) Inspired and heavily borrowed from [https://github.com/patbenatar/vscode-advanced-new-file](https://github.com/patbenatar/vscode-advanced-new-file) ## 2.9.0 (2018-05-24) ### Features - `File: New File`, Adding a trailing / to the supplied target name causes the creation of a new directory. [PR#25](https://github.com/sleistner/vscode-fileutils/pull/25) ## 2.8.1 (2018-02-25) ### Fixes - Extension can not be loaded due to missing dependency. ## 2.8.0 (2018-02-25) ### Features - `File: Delete`, Add configuration `fileutils.delete.useTrash` in order to move files to trash. - `File: Delete`, Add configuration `fileutils.delete.confirm` to toggle confirmation dialog. ## 2.7.1 (2017-10-25) ### Fixes: - Renaming and other actions move editor to first group ## 2.7.0 (2017-10-05) ### Features: - [lazyc97](https://github.com/lazyc97) Select filename when inputbox shows up [PR#23](https://github.com/sleistner/vscode-fileutils/pull/23) ## 2.6.1 (2017-06-12) ### Fixes: - Keyboard shortcuts failed to execute ## 2.4.1 (2017-03-06) ### Features: - Enable modal confirmation dialogs ## 2.3.4 (2017-03-06) ### Fixes: - File-New File or Folder failed to execute ## 2.3.3 (2017-01-12) ### Fixes: - File-Duplicate from the context menu doesn't work on Windows ## 2.3.1 (2016-10-14) ### Features: - file browser context menu Duplicate - file editor context menu Duplicate - file editor context menu Move ## 2.0.0 (2016-07-18) ### Features: - file browser context menu Move Moves the selected file or directory. _(Also creates nested directories)_ ### Breaking Changes: - command prefix `extensions` has been renamed to `fileutils` ## 1.1.0 (2016-05-04) ### Features: - command File: New File Relative to Current View Adds a new file relative to file open in active editor. _(Also creates nested directories)_ - command File: New File Relative to Project Root Adds a new file relative to project root. _(Also creates nested directories)_ - command File: New Folder Relative to Current View Adds a new directory relative to file open in active editor. _(Also creates nested directories)_ - command File: New Folder Relative to Project Root Adds a new directory relative to project root. _(Also creates nested directories)_ ## 1.0.0 (2016-05-03) Features: - command File: Rename Renames the file open in active editor. _(Also creates nested directories)_ - command File: Move Moves the file open in active editor. _(Also creates nested directories)_ - command File: Duplicate Duplicates the file open in active editor. _(Also creates nested directories)_ - command File: Remove Deletes the file open in active editor. ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Steffen Leistner Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # File Utils - Visual Studio Code Extension [![Known Vulnerabilities](https://snyk.io/test/github/sleistner/vscode-fileutils/badge.svg)](https://snyk.io/test/github/sleistner/vscode-fileutils) [![Renovate](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) --- ![icon](images/icon-96x96.png) A convenient way of creating, duplicating, moving, renaming, deleting files and directories. _Inspired by [Sidebar Enhancements](https://github.com/titoBouzout/SideBarEnhancements) for Sublime._ ## How to use ![demo](images/demo.gif) ### Using the context menu - Right click on a file or folder in the Explorer pane or in a file tab. - Select one of those command: `Copy Name`, `Duplicate...`, `Move...`, `Rename...`, `Delete...`. - For comand `Copy name`, the file name is copied to the clipboard. - For other command, answer the additional info requested: - Duplicate: update path to duplicate near where the command palette is displayed. - Move: update path to move to near where the command palette is displayed. - Rename: update name in Explorer pane or near where the command palette is displayed - Delete: answer Visual Studio Code delete dialog. ## Using the command palette - Bring up the command palette, and select "File Utils: ". - Select one of the commands mentioned below. - Press [Enter] to confirm, or [Escape] to cancel. ![howto](images/howto.png) ### Brace Expansion > Brace expansion is a mechanism by which arbitrary strings may be generated. Example file name input ```bash /tmp/{a,b,c}/index.{cpp,ts,scss} ``` will generate the following files ```bash ➜ tree /tmp /tmp ├── a │ ├── index.cpp │ ├── index.scss │ └── index.ts ├── b │ ├── index.cpp │ ├── index.scss │ └── index.ts └── c ├── index.cpp ├── index.scss └── index.ts ``` ### Note Non-existent folders are created automatically. ## Changelog - [https://github.com/sleistner/vscode-fileutils/blob/master/CHANGELOG.md](https://github.com/sleistner/vscode-fileutils/blob/master/CHANGELOG.md) ## How to contribute - [https://github.com/sleistner/vscode-fileutils/blob/master/CONTRIBUTING.md](https://github.com/sleistner/vscode-fileutils/blob/master/CONTRIBUTING.md) ## Disclaimer **Important:** This extension due to the nature of it's purpose will create files on your hard drive and if necessary create the respective folder structure. While it should not override any files during this process, I'm not giving any guarantees or take any responsibility in case of lost data. ## Contributors - [Steffen Leistner](https://github.com/sleistner) - [Ilia Shkolyar](https://github.com/iliashkolyar) ## License MIT ## Credits ### Icon - [Janosch, Green Tropical Waters - Utilities Icon](https://iconarchive.com/show/tropical-waters-folders-icons-by-janosch500/Utilities-icon.html) ================================================ FILE: biome.json ================================================ { "$schema": "https://biomejs.dev/schemas/2.1.4/schema.json", "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, "files": { "includes": ["src/**/*", "test/**/*"], "ignoreUnknown": false }, "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 4, "lineWidth": 120 }, "linter": { "enabled": true, "rules": { "recommended": true, "style": { "useBlockStatements": "off", "useNodejsImportProtocol": "off" }, "suspicious": { "noExplicitAny": "warn" } } }, "javascript": { "formatter": { "quoteStyle": "double", "trailingCommas": "es5", "semicolons": "always" } }, "assist": { "enabled": true, "actions": { "source": { "organizeImports": "on" } } } } ================================================ FILE: main.ts ================================================ export { activate } from "./src/extension"; ================================================ FILE: package.json ================================================ { "name": "vscode-fileutils", "displayName": "File Utils", "description": "A convenient way of creating, duplicating, moving, renaming and deleting files and directories.", "version": "3.10.3", "private": true, "license": "MIT", "publisher": "sleistner", "engines": { "node": ">=22.0.0", "vscode": "^1.74.0" }, "categories": [ "Other" ], "keywords": [ "utils", "files", "move", "duplicate", "rename" ], "icon": "images/icon.png", "galleryBanner": { "color": "#1c2237", "theme": "dark" }, "bugs": { "url": "https://github.com/sleistner/vscode-fileutils/issues" }, "repository": { "type": "git", "url": "https://github.com/sleistner/vscode-fileutils.git" }, "homepage": "https://github.com/sleistner/vscode-fileutils/blob/master/README.md", "main": "./out/extension.js", "contributes": { "commands": [ { "command": "fileutils.renameFile", "category": "File Utils", "title": "Rename..." }, { "command": "fileutils.moveFile", "category": "File Utils", "title": "Move..." }, { "command": "fileutils.duplicateFile", "category": "File Utils", "title": "Duplicate..." }, { "command": "fileutils.removeFile", "category": "File Utils", "title": "Delete" }, { "command": "fileutils.newFile", "category": "File Utils", "title": "New File Relative to Current View..." }, { "command": "fileutils.newFileAtRoot", "category": "File Utils", "title": "New File Relative to Project Root..." }, { "command": "fileutils.newFolder", "category": "File Utils", "title": "New Folder Relative to Current View..." }, { "command": "fileutils.newFolderAtRoot", "category": "File Utils", "title": "New Folder Relative to Project Root..." }, { "command": "fileutils.copyFileName", "category": "File Utils", "title": "Copy Name" } ], "menus": { "explorer/context": [ { "command": "fileutils.moveFile", "group": "7_modification", "when": "config.fileutils.menus.context.explorer =~ /moveFile/" }, { "command": "fileutils.duplicateFile", "group": "7_modification", "when": "config.fileutils.menus.context.explorer =~ /duplicateFile/" }, { "command": "fileutils.newFileAtRoot", "group": "2_workspace", "when": "config.fileutils.menus.context.explorer =~ /newFileAtRoot/" }, { "command": "fileutils.newFolderAtRoot", "group": "2_workspace", "when": "config.fileutils.menus.context.explorer =~ /newFolderAtRoot/" }, { "command": "fileutils.copyFileName", "group": "6_copypath", "when": "config.fileutils.menus.context.explorer =~ /copyFileName/" } ], "editor/context": [ { "command": "fileutils.copyFileName", "group": "1_copypath", "when": "config.fileutils.menus.context.editor =~ /copyFileName/ && resourceScheme != output" }, { "command": "fileutils.renameFile", "group": "1_modification@1", "when": "config.fileutils.menus.context.editor =~ /renameFile/ && resourceScheme != output" }, { "command": "fileutils.moveFile", "group": "1_modification@2", "when": "config.fileutils.menus.context.editor =~ /moveFile/ && resourceScheme != output" }, { "command": "fileutils.duplicateFile", "group": "1_modification@3", "when": "config.fileutils.menus.context.editor =~ /duplicateFile/ && resourceScheme != output" }, { "command": "fileutils.removeFile", "group": "1_modification@4", "when": "config.fileutils.menus.context.editor =~ /removeFile/ && resourceScheme != output" } ], "editor/title/context": [ { "command": "fileutils.copyFileName", "group": "1_copypath", "when": "config.fileutils.menus.context.editorTitle =~ /copyFileName/" }, { "command": "fileutils.renameFile", "group": "1_modification@1", "when": "config.fileutils.menus.context.editorTitle =~ /renameFile/" }, { "command": "fileutils.moveFile", "group": "1_modification@2", "when": "config.fileutils.menus.context.editorTitle =~ /moveFile/" }, { "command": "fileutils.duplicateFile", "group": "1_modification@3", "when": "config.fileutils.menus.context.editorTitle =~ /duplicateFile/" }, { "command": "fileutils.removeFile", "group": "1_modification@4", "when": "config.fileutils.menus.context.editorTitle =~ /removeFile/" } ] }, "configuration": { "type": "object", "title": "File Utils", "properties": { "fileutils.typeahead.enabled": { "type": "boolean", "default": true, "description": "Controls whether to show a directory selector for new file and new folder command.", "markdownDeprecationMessage": "**Deprecated**: Please use `#fileutils.newFile.typeahead.enabled#` or `#fileutils.newFolder.typeahead.enabled#` instead.", "deprecationMessage": "Deprecated: Please use fileutils.newFile.typeahead.enabled or fileutils.newFolder.typeahead.enabled instead." }, "fileutils.duplicateFile.typeahead.enabled": { "type": "boolean", "default": false, "description": "Controls whether to show a directory selector for the duplicate file command." }, "fileutils.moveFile.typeahead.enabled": { "type": "boolean", "default": false, "description": "Controls whether to show a directory selector for the move file command." }, "fileutils.newFile.typeahead.enabled": { "type": "boolean", "default": true, "description": "Controls whether to show a directory selector for the new file command." }, "fileutils.newFolder.typeahead.enabled": { "type": "boolean", "default": true, "description": "Controls whether to show a directory selector for new folder command." }, "fileutils.inputBox.pathType": { "type": "string", "default": "root", "enum": [ "root", "workspace" ], "enumDescriptions": [ "Absolute file path of the opened workspace or folder (e.g. /Users/Development/myWorkspace)", "Relative file path of the opened workspace or folder (e.g. /myWorkspace)" ], "description": "Controls the path that is shown in the input box." }, "fileutils.inputBox.pathTypeIndicator": { "type": "string", "default": "@", "maxLength": 50, "description": "Controls the indicator that is shown in the input box when the path type is workspace. This setting only has an effect when 'fileutils.inputBox.pathType' is set to 'workspace'.", "markdownDescription": "Controls the indicator that is shown in the input box when the path type is workspace. \n\nThis setting only has an effect when `#fileutils.inputBox.pathType#` is set to `workspace`.\n\nFor example, if the path type is `workspace` and the indicator is `@`, the path will be shown as `@/myWorkspace`." }, "fileutils.menus.context.explorer": { "type": "array", "default": [ "moveFile", "duplicateFile", "newFileAtRoot", "newFolderAtRoot", "copyFileName" ], "items": { "type": "string", "enum": [ "moveFile", "duplicateFile", "newFileAtRoot", "newFolderAtRoot", "copyFileName" ], "enumDescriptions": [ "Move", "Duplicate", "New File Relative to Project Root", "New Folder Relative to Project Root", "Copy Name" ] }, "uniqueItems": true, "description": "Controls whether to show the command in the explorer context menu.", "order": 90 }, "fileutils.menus.context.editor": { "type": "array", "default": [ "renameFile", "moveFile", "duplicateFile", "removeFile", "copyFileName" ], "items": { "type": "string", "enum": [ "renameFile", "moveFile", "duplicateFile", "removeFile", "copyFileName" ], "enumDescriptions": [ "Rename", "Move", "Duplicate", "Remove", "Copy Name" ] }, "uniqueItems": true, "description": "Controls whether to show the command in the editor context menu.", "order": 100 }, "fileutils.menus.context.editorTitle": { "type": "array", "default": [ "renameFile", "moveFile", "duplicateFile", "removeFile", "copyFileName" ], "items": { "type": "string", "enum": [ "renameFile", "moveFile", "duplicateFile", "removeFile", "copyFileName" ], "enumDescriptions": [ "Rename", "Move", "Duplicate", "Remove", "Copy Name" ] }, "uniqueItems": true, "description": "Controls whether to show the command in the editor title context menu.", "order": 110 } } } }, "scripts": { "vscode:prepublish": "npm run -S esbuild-base -- --minify", "esbuild-base": "npm run clean && esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node", "watch": "scripts/dev-env && npm run -S esbuild-base -- --sourcemap --watch", "tsc:watch": "tsc -watch -p ./", "pretest": "tsc -p ./", "test": "node ./out/test/runTest.js", "lint": "biome check src test", "lint:fix": "biome check --write src test", "format": "biome format src test", "format:write": "biome format --write src test", "semantic-release": "semantic-release", "prepare": "[ ! -x ./node_modules/.bin/husky ] && exit 0; husky install", "clean": "rimraf out" }, "devDependencies": { "@biomejs/biome": "^2.1.4", "@semantic-release/changelog": "6.0.3", "@semantic-release/git": "10.0.1", "@tsconfig/node22": "^22.0.2", "@types/bluebird": "^3.5.42", "@types/bluebird-retry": "^0.11.8", "@types/brace-expansion": "^1.1.2", "@types/chai": "^4.3.20", "@types/mocha": "^10.0.10", "@types/node": "^24.2.1", "@types/sinon": "^17.0.4", "@types/sinon-chai": "^3.2.12", "@types/vscode": "^1.102.0", "@vscode/test-electron": "^2.5.2", "bluebird": "3.7.2", "bluebird-retry": "0.11.0", "chai": "^4.5.0", "esbuild": "^0.27.0", "husky": "^9.1.7", "mocha": "^11.7.1", "rimraf": "^6.0.1", "semantic-release": "^24.2.7", "semantic-release-vsce": "^6.0.11", "sinon": "^21.0.0", "sinon-chai": "^3.7.0", "typescript": "^5.9.2" }, "dependencies": { "brace-expansion": "^4.0.1", "fast-glob": "^3.3.3" } } ================================================ FILE: renovate.json ================================================ { "extends": [ "config:base" ], "ignoreDeps": ["@types/node", "@types/vscode"], "packageRules": [ { "updateTypes": ["minor", "patch", "pin", "digest"], "automerge": true } ] } ================================================ FILE: scripts/dev-env ================================================ #!/usr/bin/env bash rm -rf ./tmp mkdir -p ./tmp/{app,workspace,scripts} touch ./tmp/{app,workspace,scripts}/{foo,bar,baz}.ts ================================================ FILE: src/FileItem.ts ================================================ import * as fs from "fs"; import * as path from "path"; import { Uri, WorkspaceEdit, workspace } from "vscode"; function assertTargetPath(targetPath: Uri | undefined): asserts targetPath is Uri { if (targetPath === undefined) { throw new Error("Missing target path"); } } export class FileItem { private SourcePath: Uri; private TargetPath: Uri | undefined; constructor( sourcePath: Uri | string, targetPath?: Uri | string, private IsDir: boolean = false ) { this.SourcePath = this.toUri(sourcePath); if (targetPath !== undefined) { this.TargetPath = this.toUri(targetPath); } } get name(): string { return path.basename(this.SourcePath.path); } get path(): Uri { return this.SourcePath; } get targetPath(): Uri | undefined { return this.TargetPath; } get exists(): boolean { if (this.targetPath === undefined) { return false; } return fs.existsSync(this.targetPath.fsPath); } get isDir(): boolean { return this.IsDir; } public async move(): Promise { assertTargetPath(this.targetPath); const edit = new WorkspaceEdit(); edit.renameFile(this.path, this.targetPath, { overwrite: true }); const result = await workspace.applyEdit(edit); if (!result) { throw new Error(`Failed to move file "${this.targetPath.fsPath}."`); } this.SourcePath = this.targetPath; return this; } public async duplicate(): Promise { assertTargetPath(this.targetPath); try { await workspace.fs.copy(this.path, this.targetPath, { overwrite: true }); return new FileItem(this.targetPath, undefined, this.isDir); } catch (error) { throw new Error(`Failed to duplicate file "${this.targetPath.fsPath}. (${error})"`); } } public async remove(): Promise { const edit = new WorkspaceEdit(); edit.deleteFile(this.path, { recursive: true, ignoreIfNotExists: true }); const result = await workspace.applyEdit(edit); if (!result) { throw new Error(`Failed to delete file "${this.path.fsPath}."`); } return this; } public async create(mkDir?: boolean): Promise { assertTargetPath(this.targetPath); if (this.exists) { await workspace.fs.delete(this.targetPath, { recursive: true }); } if (mkDir === true || this.isDir) { await workspace.fs.createDirectory(this.targetPath); } else { await workspace.fs.writeFile(this.targetPath, new Uint8Array()); } return new FileItem(this.targetPath, undefined, this.isDir); } private toUri(uriOrString: Uri | string): Uri { return uriOrString instanceof Uri ? uriOrString : Uri.file(uriOrString); } } ================================================ FILE: src/command/BaseCommand.ts ================================================ import type { Uri } from "vscode"; import type { FileController } from "../controller"; import type { FileItem } from "../FileItem"; import type { Command, CommandConstructorOptions } from "./Command"; interface ExecuteControllerOptions { openFileInEditor?: boolean; } export abstract class BaseCommand implements Command { constructor( protected controller: T, readonly options?: CommandConstructorOptions ) {} public abstract execute(uri?: Uri): Promise; protected async executeController( fileItem: FileItem | undefined, options?: ExecuteControllerOptions ): Promise { if (fileItem) { const result = await this.controller.execute({ fileItem }); if (options?.openFileInEditor) { await this.controller.openFileInEditor(result); } } } } ================================================ FILE: src/command/Command.ts ================================================ import type { Uri } from "vscode"; export interface CommandConstructorOptions { relativeToRoot?: boolean; } export interface Command { execute(uri?: Uri): Promise; } ================================================ FILE: src/command/CopyFileNameCommand.ts ================================================ import type { Uri } from "vscode"; import type { CopyFileNameController } from "../controller/CopyFileNameController"; import { BaseCommand } from "./BaseCommand"; export class CopyFileNameCommand extends BaseCommand { public async execute(uri?: Uri): Promise { const dialogOptions = { uri }; const fileItem = await this.controller.showDialog(dialogOptions); await this.executeController(fileItem); } } ================================================ FILE: src/command/DuplicateFileCommand.ts ================================================ import type { Uri } from "vscode"; import type { MoveFileController } from "../controller/MoveFileController"; import { getConfiguration } from "../lib/config"; import { BaseCommand } from "./BaseCommand"; export class DuplicateFileCommand extends BaseCommand { public async execute(uri?: Uri): Promise { const typeahead = getConfiguration("duplicateFile.typeahead.enabled") === true; const dialogOptions = { prompt: "Duplicate As", uri, typeahead }; const fileItem = await this.controller.showDialog(dialogOptions); await this.executeController(fileItem, { openFileInEditor: !fileItem?.isDir }); } } ================================================ FILE: src/command/MoveFileCommand.ts ================================================ import type { Uri } from "vscode"; import type { MoveFileController } from "../controller/MoveFileController"; import { getConfiguration } from "../lib/config"; import { BaseCommand } from "./BaseCommand"; export class MoveFileCommand extends BaseCommand { public async execute(uri?: Uri): Promise { const typeahead = getConfiguration("moveFile.typeahead.enabled") === true; const dialogOptions = { prompt: "New Location", uri, typeahead }; const fileItem = await this.controller.showDialog(dialogOptions); await this.executeController(fileItem); } } ================================================ FILE: src/command/NewFileCommand.ts ================================================ import type { NewFileController } from "../controller/NewFileController"; import { getConfiguration } from "../lib/config"; import { BaseCommand } from "./BaseCommand"; export class NewFileCommand extends BaseCommand { public async execute(): Promise { const typeahead = this.typeahead; const relativeToRoot = this.options?.relativeToRoot ?? false; const dialogOptions = { prompt: "File Name", relativeToRoot, typeahead }; const fileItems = await this.controller.showDialog(dialogOptions); if (fileItems) { const executions = [...fileItems].map(async (fileItem) => { const result = await this.controller.execute({ fileItem }); await this.controller.openFileInEditor(result); }); await Promise.all(executions); } } protected get typeahead(): boolean { return (getConfiguration("newFile.typeahead.enabled") ?? getConfiguration("typeahead.enabled")) === true; } } ================================================ FILE: src/command/NewFolderCommand.ts ================================================ import { getConfiguration } from "../lib/config"; import { NewFileCommand } from "./NewFileCommand"; export class NewFolderCommand extends NewFileCommand { public async execute(): Promise { const typeahead = this.typeahead; const relativeToRoot = this.options?.relativeToRoot ?? false; const dialogOptions = { prompt: "Folder Name", relativeToRoot, typeahead }; const fileItems = await this.controller.showDialog(dialogOptions); if (fileItems) { const executions = [...fileItems].map(async (fileItem) => { await this.controller.execute({ fileItem, isDir: true }); }); await Promise.all(executions); } } protected get typeahead(): boolean { return (getConfiguration("newFolder.typeahead.enabled") ?? getConfiguration("typeahead.enabled")) === true; } } ================================================ FILE: src/command/RemoveFileCommand.ts ================================================ import type { Uri } from "vscode"; import type { RemoveFileController } from "../controller"; import { BaseCommand } from "./BaseCommand"; export class RemoveFileCommand extends BaseCommand { public async execute(uri?: Uri): Promise { const fileItem = await this.controller.showDialog({ uri }); await this.executeController(fileItem); } } ================================================ FILE: src/command/RenameFileCommand.ts ================================================ import type { Uri } from "vscode"; import type { RenameFileController } from "../controller/RenameFileController"; import { BaseCommand } from "./BaseCommand"; export class RenameFileCommand extends BaseCommand { public async execute(uri?: Uri): Promise { const dialogOptions = { prompt: "New Name", uri }; const fileItem = await this.controller.showDialog(dialogOptions); await this.executeController(fileItem); } } ================================================ FILE: src/command/index.ts ================================================ export { Command } from "./Command"; export { CopyFileNameCommand } from "./CopyFileNameCommand"; export { DuplicateFileCommand } from "./DuplicateFileCommand"; export { MoveFileCommand } from "./MoveFileCommand"; export { NewFileCommand } from "./NewFileCommand"; export { NewFolderCommand } from "./NewFolderCommand"; export { RemoveFileCommand } from "./RemoveFileCommand"; export { RenameFileCommand } from "./RenameFileCommand"; ================================================ FILE: src/controller/BaseFileController.ts ================================================ import path from "path"; import { commands, type ExtensionContext, env, type InputBoxOptions, type TextEditor, Uri, type WorkspaceFolder, window, workspace, } from "vscode"; import type { FileItem } from "../FileItem"; import { Cache } from "../lib/Cache"; import { getConfiguration } from "../lib/config"; import type { DialogOptions, ExecuteOptions, FileController, SourcePathOptions } from "./FileController"; import { TypeAheadController } from "./TypeAheadController"; enum InputBoxPathType { Root = "root", Workspace = "workspace", } type TargetPathInputBoxOptions = InputBoxOptions & Required>; export interface TargetPathInputBoxValueOptions extends DialogOptions { workspaceFolderPath?: string; pathType: InputBoxPathType; } export abstract class BaseFileController implements FileController { constructor(protected context: ExtensionContext) {} public abstract showDialog(options?: DialogOptions): Promise; public abstract execute(options: ExecuteOptions): Promise; private get pathTypeIndicator(): string { return getConfiguration("inputBox.pathTypeIndicator") ?? ""; } public async openFileInEditor(fileItem: FileItem): Promise { if (fileItem.isDir) { return; } const textDocument = await workspace.openTextDocument(fileItem.path); if (!textDocument) { throw new Error("Could not open file!"); } const editor = await window.showTextDocument(textDocument); if (!editor) { throw new Error("Could not show document!"); } return editor; } public async closeCurrentFileEditor(): Promise { return commands.executeCommand("workbench.action.closeActiveEditor"); } protected async getTargetPath(sourcePath: string, options: DialogOptions): Promise { const { prompt } = options; const pathType = this.getInputBoxPathType(); const workspaceFolderPath = await this.getWorkspaceFolderPath(); const value = await this.getTargetPathInputBoxValue(sourcePath, { ...options, workspaceFolderPath, pathType, }); const targetPath = await this.showTargetPathInputBox({ prompt, value, }); const shouldRestoreAbsolutePath = targetPath && workspaceFolderPath && pathType === InputBoxPathType.Workspace; if (shouldRestoreAbsolutePath) { return path.join( workspaceFolderPath, targetPath.replace(new RegExp(`^(${this.pathTypeIndicator}|${workspaceFolderPath})`, "g"), "") ); } return targetPath; } protected async showTargetPathInputBox(options: TargetPathInputBoxOptions): Promise { const { prompt, value } = options; const valueSelection = this.getFilenameSelection(value); return await window.showInputBox({ prompt, value, valueSelection, ignoreFocusOut: true, }); } private getInputBoxPathType(): InputBoxPathType { const pathType: InputBoxPathType | undefined = getConfiguration("inputBox.pathType"); if (pathType && Object.values(InputBoxPathType).includes(pathType)) { return pathType; } return InputBoxPathType.Root; } protected async getTargetPathInputBoxValue( sourcePath: string, options: TargetPathInputBoxValueOptions ): Promise { const { workspaceFolderPath, pathType } = options; if (pathType === InputBoxPathType.Workspace && workspaceFolderPath) { return sourcePath.replace(workspaceFolderPath, this.pathTypeIndicator); } return sourcePath; } protected getFilenameSelection(value: string): [number, number] { return [value.length, value.length]; } public async getSourcePath({ ignoreIfNotExists, uri }: SourcePathOptions = {}): Promise { if (uri?.fsPath) { return uri.fsPath; } // Attempting to get the fileName from the activeTextEditor. // Works for text files only. const activeEditor = window.activeTextEditor; if (activeEditor?.document?.fileName) { return activeEditor.document.fileName; } // No activeTextEditor means that we don't have an active file or // the active file is a non-text file (e.g. binary files such as images). // Since there is no actual API to differentiate between the scenarios, we try to retrieve // the path for a non-textual file before throwing an error. const sourcePath = await this.getSourcePathForNonTextFile(); if (!sourcePath && ignoreIfNotExists !== true) { throw new Error(); } return sourcePath; } protected getCache(namespace: string): Cache { return new Cache(this.context.globalState, namespace); } protected async ensureWritableFile(fileItem: FileItem): Promise { if (!fileItem.exists) { return fileItem; } if (fileItem.targetPath === undefined) { throw new Error("Missing target path"); } const message = `File '${fileItem.targetPath.path}' already exists.`; const action = "Overwrite"; const overwrite = await window.showInformationMessage(message, { modal: true }, action); if (overwrite) { return fileItem; } throw new Error(); } private async getSourcePathForNonTextFile(): Promise { // Since there is no API to get details of non-textual files, the following workaround is performed: // 1. Saving the original clipboard data to a local variable. const originalClipboardData = await env.clipboard.readText(); // 2. Populating the clipboard with an empty string await env.clipboard.writeText(""); // 3. Calling the copyPathOfActiveFile that populates the clipboard with the source path of the active file. // If there is no active file - the clipboard will not be populated and it will stay with the empty string. await commands.executeCommand("workbench.action.files.copyPathOfActiveFile"); // 4. Get the clipboard data after the API call const postAPICallClipboardData = await env.clipboard.readText(); // 5. Return the saved original clipboard data to the clipboard so this method // will not interfere with the clipboard's content. await env.clipboard.writeText(originalClipboardData); // 6. Return the clipboard data from the API call (which could be an empty string if it failed). return postAPICallClipboardData; } protected async getWorkspaceFolderPath(relativeToRoot?: boolean): Promise; protected async getWorkspaceFolderPath(): Promise { const workspaceFolder = await this.selectWorkspaceFolder(); return workspaceFolder?.uri.fsPath; } protected async selectWorkspaceFolder(): Promise { if (workspace.workspaceFolders && workspace.workspaceFolders.length === 1) { return workspace.workspaceFolders[0]; } const sourcePath = await this.getSourcePath({ ignoreIfNotExists: true }); const uri = Uri.file(sourcePath); return workspace.getWorkspaceFolder(uri) || window.showWorkspaceFolderPick(); } protected get isMultiRootWorkspace(): boolean { return workspace.workspaceFolders !== undefined && workspace.workspaceFolders.length > 1; } protected async getFileSourcePathAtRoot(rootPath: string, options: SourcePathOptions): Promise { const { relativeToRoot = false, typeahead } = options; let sourcePath = rootPath; if (typeahead) { const cache = this.getCache(`workspace:${sourcePath}`); const typeAheadController = new TypeAheadController(cache, relativeToRoot); sourcePath = await typeAheadController.showDialog(sourcePath); } if (!sourcePath) { throw new Error(); } return sourcePath; } } ================================================ FILE: src/controller/CopyFileNameController.ts ================================================ import { env } from "vscode"; import { FileItem } from "../FileItem"; import { BaseFileController } from "./BaseFileController"; import type { DialogOptions, ExecuteOptions } from "./FileController"; export class CopyFileNameController extends BaseFileController { public async showDialog(options: DialogOptions): Promise { const { uri } = options; const sourcePath = await this.getSourcePath({ uri }); if (!sourcePath) { throw new Error(); } return new FileItem(sourcePath); } public async execute(options: ExecuteOptions): Promise { await env.clipboard.writeText(options.fileItem.name); return options.fileItem; } } ================================================ FILE: src/controller/DuplicateFileController.ts ================================================ import type { FileItem } from "../FileItem"; import type { ExecuteOptions } from "./FileController"; import { MoveFileController } from "./MoveFileController"; export class DuplicateFileController extends MoveFileController { public async execute(options: ExecuteOptions): Promise { const { fileItem } = options; await this.ensureWritableFile(fileItem); return fileItem.duplicate(); } } ================================================ FILE: src/controller/FileController.ts ================================================ import type { TextEditor, Uri } from "vscode"; import type { FileItem } from "../FileItem"; export interface DialogOptions { prompt?: string; uri?: Uri; typeahead?: boolean; } export interface ExecuteOptions { fileItem: FileItem; } export interface SourcePathOptions { relativeToRoot?: boolean; ignoreIfNotExists?: boolean; uri?: Uri; typeahead?: boolean; } export interface FileController { showDialog(options?: DialogOptions): Promise; execute(options: ExecuteOptions): Promise; openFileInEditor(fileItem: FileItem): Promise; closeCurrentFileEditor(): Promise; getSourcePath(options?: SourcePathOptions): Promise; } ================================================ FILE: src/controller/MoveFileController.ts ================================================ import * as path from "path"; import { FileType, Uri, workspace } from "vscode"; import { FileItem } from "../FileItem"; import { BaseFileController, type TargetPathInputBoxValueOptions } from "./BaseFileController"; import type { DialogOptions, ExecuteOptions } from "./FileController"; export class MoveFileController extends BaseFileController { public async showDialog(options: DialogOptions): Promise { const { uri } = options; const sourcePath = await this.getSourcePath({ uri }); if (!sourcePath) { throw new Error(); } const targetPath = await this.getTargetPath(sourcePath, options); if (targetPath) { const isDir = (await workspace.fs.stat(Uri.file(sourcePath))).type === FileType.Directory; return new FileItem(sourcePath, targetPath, isDir); } } public async execute(options: ExecuteOptions): Promise { const { fileItem } = options; await this.ensureWritableFile(fileItem); return fileItem.move(); } protected async getTargetPathInputBoxValue( sourcePath: string, options: TargetPathInputBoxValueOptions ): Promise { const value = await this.getFullTargetPathInputBoxValue(sourcePath, options); return super.getTargetPathInputBoxValue(value, options); } private async getFullTargetPathInputBoxValue( sourcePath: string, options: TargetPathInputBoxValueOptions ): Promise { const { typeahead, workspaceFolderPath } = options; if (!typeahead) { return sourcePath; } if (!workspaceFolderPath) { throw new Error(); } const rootPath = await this.getFileSourcePathAtRoot(workspaceFolderPath, { relativeToRoot: true, typeahead }); const fileName = path.basename(sourcePath); return path.join(rootPath, fileName); } protected getFilenameSelection(value: string): [number, number] { const basename = path.basename(value); const start = value.length - basename.length; const dot = basename.lastIndexOf("."); const exclusiveEndIndex = dot <= 0 ? value.length : start + dot; return [start, exclusiveEndIndex]; } } ================================================ FILE: src/controller/NewFileController.ts ================================================ import expand from "brace-expansion"; import * as path from "path"; import { window } from "vscode"; import { FileItem } from "../FileItem"; import { BaseFileController, type TargetPathInputBoxValueOptions } from "./BaseFileController"; import type { DialogOptions, ExecuteOptions, SourcePathOptions } from "./FileController"; export interface NewFileDialogOptions extends Omit { relativeToRoot?: boolean; } export interface NewFileExecuteOptions extends ExecuteOptions { isDir?: boolean; } export class NewFileController extends BaseFileController { public async showDialog(options: NewFileDialogOptions): Promise { const { relativeToRoot = false, typeahead } = options; const sourcePath = await this.getNewFileSourcePath({ relativeToRoot, typeahead }); const targetPath = await this.getTargetPath(sourcePath, options); if (!targetPath) { return; } return expand(targetPath.replace(/\\/g, "/")).map((filePath) => { const realPath = path.resolve(sourcePath, filePath); const isDir = filePath.endsWith("/"); return new FileItem(sourcePath, realPath, isDir); }); } public async execute(options: NewFileExecuteOptions): Promise { const { fileItem, isDir = false } = options; await this.ensureWritableFile(fileItem); try { return fileItem.create(isDir); } catch { throw new Error(`Error creating file '${fileItem.path}'.`); } } protected async getTargetPathInputBoxValue( sourcePath: string, options: TargetPathInputBoxValueOptions ): Promise { const value = path.join(sourcePath, path.sep); return super.getTargetPathInputBoxValue(value, options); } public async getNewFileSourcePath({ relativeToRoot, typeahead }: SourcePathOptions): Promise { const rootPath = await this.getRootPath(relativeToRoot === true); if (!rootPath) { throw new Error(); } return this.getFileSourcePathAtRoot(rootPath, { relativeToRoot, typeahead }); } private async getRootPath(relativeToRoot: boolean): Promise { if (relativeToRoot) { return this.getWorkspaceFolderPath(relativeToRoot); } return path.dirname(await this.getSourcePath()); } protected async getWorkspaceFolderPath(relativeToRoot: boolean): Promise { const requiresWorkspaceFolderPick = relativeToRoot && this.isMultiRootWorkspace; if (requiresWorkspaceFolderPick) { const workspaceFolder = await window.showWorkspaceFolderPick(); return workspaceFolder?.uri.fsPath; } return super.getWorkspaceFolderPath(); } } ================================================ FILE: src/controller/RemoveFileController.ts ================================================ import * as path from "path"; import { window, workspace } from "vscode"; import { FileItem } from "../FileItem"; import { BaseFileController } from "./BaseFileController"; import type { DialogOptions, ExecuteOptions } from "./FileController"; export class RemoveFileController extends BaseFileController { public async showDialog(options: DialogOptions): Promise { const { uri } = options; const sourcePath = await this.getSourcePath({ uri }); if (!sourcePath) { throw new Error(); } if (this.confirmDelete === false) { return new FileItem(sourcePath); } const message = `Are you sure you want to delete '${path.basename(sourcePath)}'?`; const action = "Move to Trash"; const remove = await window.showInformationMessage(message, { modal: true }, action); if (remove) { return new FileItem(sourcePath); } } public async execute(options: ExecuteOptions): Promise { const { fileItem } = options; try { await fileItem.remove(); } catch (_e) { throw new Error(`Error deleting file '${fileItem.path}'.`); } return fileItem; } private get confirmDelete(): boolean { return workspace.getConfiguration("explorer", null).get("confirmDelete") === true; } } ================================================ FILE: src/controller/RenameFileController.ts ================================================ import * as path from "path"; import type { DialogOptions } from "./FileController"; import { MoveFileController } from "./MoveFileController"; export class RenameFileController extends MoveFileController { protected async getTargetPath(sourcePath: string, options: DialogOptions): Promise { const { prompt } = options; const value = path.basename(sourcePath); const targetPath = await this.showTargetPathInputBox({ prompt, value }); if (targetPath) { const basePath = path.dirname(sourcePath); return path.join(basePath, targetPath.replace(basePath, "")); } } } ================================================ FILE: src/controller/TypeAheadController.ts ================================================ import * as path from "path"; import { type QuickPickItem, window } from "vscode"; import type { Cache } from "../lib/Cache"; import { TreeWalker } from "../lib/TreeWalker"; async function waitForIOEvents(): Promise { return new Promise((resolve) => setImmediate(resolve)); } const ROOT_PATH = "/"; export class TypeAheadController { constructor( private cache: Cache, private relativeToRoot: boolean = false ) {} public async showDialog(sourcePath: string): Promise { const items = await this.buildQuickPickItems(sourcePath); const item = items.length === 1 ? items[0] : await this.showQuickPick(items); if (!item) { throw new Error(); } const selection = item.label; this.cache.put("last", selection); return path.join(sourcePath, selection); } private async buildQuickPickItems(sourcePath: string): Promise { const lastEntry: string = this.cache.get("last"); const header = this.buildQuickPickItemsHeader(lastEntry); const directories = (await this.getDirectoriesAtSourcePath(sourcePath)) .filter((directory) => directory !== lastEntry && directory !== ROOT_PATH) .map((directory) => this.buildQuickPickItem(directory)); if (directories.length === 0 && header.length === 1) { return header; } return [...header, ...directories]; } private async getDirectoriesAtSourcePath(sourcePath: string): Promise { await waitForIOEvents(); const treeWalker = new TreeWalker(); return treeWalker.directories(sourcePath); } private buildQuickPickItemsHeader(lastEntry: string | undefined): QuickPickItem[] { const items = [ this.buildQuickPickItem(ROOT_PATH, `- ${this.relativeToRoot ? "workspace root" : "current file"}`), ]; if (lastEntry && lastEntry !== ROOT_PATH) { items.push(this.buildQuickPickItem(lastEntry, "- last selection")); } return items; } private buildQuickPickItem(label: string, description?: string | undefined): QuickPickItem { return { description, label }; } private async showQuickPick(items: readonly QuickPickItem[]) { const hint = "larger projects may take a moment to load"; const placeHolder = `First, select an existing path to create relative to (${hint})`; return window.showQuickPick(items, { placeHolder, ignoreFocusOut: true }); } } ================================================ FILE: src/controller/index.ts ================================================ export { CopyFileNameController } from "./CopyFileNameController"; export { DuplicateFileController } from "./DuplicateFileController"; export { FileController } from "./FileController"; export { MoveFileController } from "./MoveFileController"; export { NewFileController } from "./NewFileController"; export { RemoveFileController } from "./RemoveFileController"; ================================================ FILE: src/extension.ts ================================================ import * as vscode from "vscode"; import { type Command, CopyFileNameCommand, DuplicateFileCommand, MoveFileCommand, NewFileCommand, NewFolderCommand, RemoveFileCommand, RenameFileCommand, } from "./command"; import { CopyFileNameController, DuplicateFileController, MoveFileController, NewFileController, RemoveFileController, } from "./controller"; import { RenameFileController } from "./controller/RenameFileController"; function handleError(err: Error) { if (err?.message) { vscode.window.showErrorMessage(err.message); } return err; } function register(context: vscode.ExtensionContext, command: Command, commandName: string) { const proxy = (...args: never[]) => command.execute(...args).catch(handleError); const disposable = vscode.commands.registerCommand(`fileutils.${commandName}`, proxy); context.subscriptions.push(disposable); } export function activate(context: vscode.ExtensionContext): void { const copyFileNameController = new CopyFileNameController(context); const duplicateFileController = new DuplicateFileController(context); const moveFileController = new MoveFileController(context); const newFileController = new NewFileController(context); const removeFileController = new RemoveFileController(context); const renameFileController = new RenameFileController(context); register(context, new CopyFileNameCommand(copyFileNameController), "copyFileName"); register(context, new DuplicateFileCommand(duplicateFileController), "duplicateFile"); register(context, new MoveFileCommand(moveFileController), "moveFile"); register(context, new NewFileCommand(newFileController, { relativeToRoot: true }), "newFileAtRoot"); register(context, new NewFileCommand(newFileController), "newFile"); register(context, new NewFolderCommand(newFileController, { relativeToRoot: true }), "newFolderAtRoot"); register(context, new NewFolderCommand(newFileController), "newFolder"); register(context, new RemoveFileCommand(removeFileController), "removeFile"); register(context, new RenameFileCommand(renameFileController), "renameFile"); } ================================================ FILE: src/lib/Cache.ts ================================================ import type * as vscode from "vscode"; export class Cache { private cache: { [key: string]: unknown }; constructor( private storage: vscode.Memento, private namespace: string ) { this.cache = storage.get(this.namespace, {}); } public put(key: string, value: unknown): void { this.cache[key] = value; this.storage.update(this.namespace, this.cache); } public get(key: string, defaultValue?: unknown): T { return (key in this.cache ? this.cache[key] : defaultValue) as T; } } ================================================ FILE: src/lib/TreeWalker.ts ================================================ import glob from "fast-glob"; import * as path from "path"; import { workspace } from "vscode"; interface ExtendedProcess { noAsar: boolean; } export class TreeWalker { public async directories(sourcePath: string): Promise { try { this.ensureFailSafeFileLookup(); const files = await glob("**", { cwd: sourcePath, onlyDirectories: true, ignore: this.getExcludePatterns(), }); return files.map((file) => path.join(path.sep, file)).sort(); } catch (err) { const details = (err as Error).message; throw new Error(`Unable to list subdirectories for directory "${sourcePath}". Details: (${details})`); } } private getExcludePatterns(): string[] { const exclude = new Set([ ...Object.keys(workspace.getConfiguration("search.exclude")), ...Object.keys(workspace.getConfiguration("files.exclude")), ]); return Array.from(exclude); } private ensureFailSafeFileLookup() { (process as unknown as ExtendedProcess).noAsar = true; } } ================================================ FILE: src/lib/config.ts ================================================ import { workspace } from "vscode"; export function getConfiguration(key: string): T | undefined { return workspace.getConfiguration("fileutils", null).get(key); } ================================================ FILE: test/command/CopyFileNameCommand.test.ts ================================================ import { expect } from "chai"; import { env } from "vscode"; import { CopyFileNameCommand } from "../../src/command"; import { CopyFileNameController } from "../../src/controller"; import { FileItem } from "../../src/FileItem"; import * as helper from "../helper"; describe(CopyFileNameCommand.name, () => { const clipboardInitialTestData = "SOME_TEXT"; const subject = new CopyFileNameCommand(new CopyFileNameController(helper.createExtensionContext())); beforeEach(helper.beforeEach); afterEach(helper.afterEach); describe("as command", () => { afterEach(async () => { await env.clipboard.writeText(clipboardInitialTestData); }); describe("with open text document", () => { beforeEach(async () => helper.openDocument(helper.editorFile1)); afterEach(async () => helper.closeAllEditors()); it("should put the file name to the clipboard", async () => { await subject.execute(); const clipboardData = await env.clipboard.readText(); expect(clipboardData).to.equal(new FileItem(helper.editorFile1).name); }); }); describe("without an open text document", () => { beforeEach(async () => { await helper.closeAllEditors(); await env.clipboard.writeText(clipboardInitialTestData); }); it("should ignore the command call and not change the clipboard data", async () => { try { await subject.execute(); expect.fail("must fail"); } catch (_e) { const clipboardData = await env.clipboard.readText(); expect(clipboardData).to.equal(clipboardInitialTestData); } }); }); }); }); ================================================ FILE: test/command/DuplicateFileCommand.test.ts ================================================ import { expect } from "chai"; import * as fs from "fs"; import * as path from "path"; import { Uri, window, workspace } from "vscode"; import { DuplicateFileCommand } from "../../src/command/DuplicateFileCommand"; import { DuplicateFileController } from "../../src/controller"; import * as helper from "../helper"; describe(DuplicateFileCommand.name, () => { const subject = new DuplicateFileCommand(new DuplicateFileController(helper.createExtensionContext())); beforeEach(async () => { await helper.beforeEach(); helper.createGetConfigurationStub({ "duplicateFile.typeahead.enabled": false, "inputBox.path": "root" }); }); afterEach(helper.afterEach); describe("as command", () => { describe("with open text document", () => { beforeEach(async () => { await helper.openDocument(helper.editorFile1); helper.createShowInputBoxStub().resolves(helper.targetFile.path); helper.createShowQuickPickStub().resolves({ label: "/", description: "" }); }); afterEach(async () => { await helper.closeAllEditors(); }); helper.protocol.it("should prompt for file destination", subject, "Duplicate As"); helper.protocol.it("should duplicate current file to destination", subject); helper.protocol.describe("with target file in non-existent nested directory", subject); helper.protocol.describe("when target destination exists", subject); helper.protocol.it("should open target file as active editor", subject); helper.protocol.describe("typeahead configuration", subject, { command: "duplicateFile", items: helper.quickPick.typeahead.items.workspace, }); helper.protocol.describe("inputBox configuration", subject, { editorFile: helper.editorFile1, }); }); helper.protocol.describe("without an open text document", subject); }); describe("as context menu", () => { describe("with selected file", () => { beforeEach(async () => helper.createShowInputBoxStub().resolves(helper.targetFile.path)); helper.protocol.it("should prompt for file destination", subject, "Duplicate As"); helper.protocol.it("should duplicate current file to destination", subject, helper.editorFile1); helper.protocol.it("should open target file as active editor", subject, helper.editorFile1); }); describe("with selected directory", () => { const sourceDirectory = Uri.file(path.resolve(helper.tmpDir.path, "duplicate-source-dir")); const targetDirectory = Uri.file(path.resolve(helper.tmpDir.path, "duplicate-target-dir")); beforeEach(async () => { await workspace.fs.createDirectory(sourceDirectory); helper.createShowInputBoxStub().resolves(targetDirectory.path); }); afterEach(async () => { await workspace.fs.delete(sourceDirectory, { recursive: true, useTrash: false }); await workspace.fs.delete(targetDirectory, { recursive: true, useTrash: false }); }); it("should prompt for file destination", async () => { await subject.execute(sourceDirectory); const value = sourceDirectory.path; const valueSelection = [value.length - (value.split(path.sep).pop() as string).length, value.length]; const prompt = "Duplicate As"; expect(window.showInputBox).to.have.been.calledWithExactly({ prompt, value, valueSelection, ignoreFocusOut: true, }); }); it("should duplicate current file to destination", async () => { await subject.execute(sourceDirectory); const message = `${targetDirectory} does not exist`; expect(fs.existsSync(targetDirectory.fsPath), message).to.be.true; }); it("should not open target file as active editor", async () => { await subject.execute(sourceDirectory); expect(window.activeTextEditor?.document?.fileName).not.to.equal(targetDirectory.path); }); }); }); }); ================================================ FILE: test/command/MoveFileCommand.test.ts ================================================ import { MoveFileCommand } from "../../src/command"; import { MoveFileController } from "../../src/controller"; import * as helper from "../helper"; describe(MoveFileCommand.name, () => { const subject = new MoveFileCommand(new MoveFileController(helper.createExtensionContext())); beforeEach(async () => { await helper.beforeEach(); helper.createGetConfigurationStub({ "moveFile.typeahead.enabled": false, "inputBox.path": "root" }); }); afterEach(helper.afterEach); describe("as command", () => { describe("with open text document", () => { beforeEach(async () => { await helper.openDocument(helper.editorFile1); helper.createShowInputBoxStub().resolves(helper.targetFile.path); helper.createShowQuickPickStub().resolves({ label: "/", description: "" }); }); afterEach(async () => { await helper.closeAllEditors(); }); helper.protocol.it("should prompt for file destination", subject, "New Location"); helper.protocol.it("should move current file to destination", subject); helper.protocol.describe("with target file in non-existent nested directory", subject); helper.protocol.describe("typeahead configuration", subject, { command: "moveFile", items: helper.quickPick.typeahead.items.workspace, }); helper.protocol.describe("inputBox configuration", subject, { editorFile: helper.editorFile1, }); }); helper.protocol.describe("without an open text document", subject); }); describe("as context menu", () => { beforeEach(async () => helper.createShowInputBoxStub().resolves(helper.targetFile.path)); helper.protocol.it("should prompt for file destination", subject, "New Location"); helper.protocol.it("should move current file to destination", subject, helper.editorFile1); }); }); ================================================ FILE: test/command/NewFileCommand.test.ts ================================================ import { expect } from "chai"; import * as fs from "fs"; import * as path from "path"; import { Uri, window, workspace } from "vscode"; import { NewFileCommand } from "../../src/command"; import { NewFileController } from "../../src/controller"; import * as helper from "../helper"; describe(NewFileCommand.name, () => { beforeEach(async () => { await helper.beforeEach(); helper.createGetConfigurationStub({ "newFile.typeahead.enabled": false, "inputBox.path": "root" }); }); afterEach(helper.afterEach); describe('when "relativeToRoot" is "false"', async () => { const subject = new NewFileCommand(new NewFileController(helper.createExtensionContext())); beforeEach(async () => { await helper.openDocument(helper.editorFile1); helper.createShowInputBoxStub().resolves(path.basename(helper.targetFile.path)); helper.createShowQuickPickStub().resolves({ label: "/", description: "" }); }); afterEach(async () => { await helper.closeAllEditors(); }); it("should prompt for file destination", async () => { await subject.execute(); const prompt = "File Name"; const value = path.join(path.dirname(helper.editorFile1.path), path.sep); const valueSelection = [value.length, value.length]; expect(window.showInputBox).to.have.been.calledWithExactly({ prompt, value, valueSelection, ignoreFocusOut: true, }); }); helper.protocol.describe("typeahead configuration", subject, { command: "newFile", items: helper.quickPick.typeahead.items.currentFile, }); helper.protocol.describe("inputBox configuration", subject, { editorFile: helper.editorFile1, expectedPath: "", }); it("should create the file at destination", async () => { await subject.execute(); const message = `${helper.targetFile.path} does not exist`; expect(fs.existsSync(helper.targetFile.fsPath), message).to.be.true; }); describe("file path ends with path separator", () => { beforeEach(async () => { const fileName = path.basename(helper.targetFile.fsPath) + path.sep; helper.createShowInputBoxStub().resolves(fileName); }); it("should create the directory at destination", async () => { await subject.execute(); const message = `${helper.targetFile.path} must be a directory`; expect(fs.statSync(helper.targetFile.fsPath).isDirectory(), message).to.be.true; }); }); describe("file path contains dot and backslash path separator", () => { beforeEach(async () => { const fileName = helper.targetFileWithDot.fsPath.replace(/\//g, "\\"); helper.createShowInputBoxStub().resolves(fileName); }); it("should create the file at destination", async () => { await subject.execute(); const message = `${helper.targetFileWithDot.path} does not exist`; expect(fs.existsSync(helper.targetFileWithDot.fsPath), message).to.be.true; }); }); helper.protocol.describe("with target file in non-existent nested directory", subject); helper.protocol.describe("when target destination exists", subject, { overwriteFileContent: "" }); helper.protocol.it("should open target file as active editor", subject); }); describe('when "relativeToRoot" is "true"', () => { const subject = new NewFileCommand(new NewFileController(helper.createExtensionContext()), { relativeToRoot: true, }); beforeEach(async () => { helper.createShowInputBoxStub().callsFake(async (options) => { if (options.value) { return path.join(options.value, "filename.txt"); } }); helper.createShowQuickPickStub().resolves({ label: "/", description: "" }); }); describe("with one workspace", () => { beforeEach(async () => { helper.createWorkspaceFoldersStub(helper.workspaceFolderA); helper.createGetWorkspaceFolderStub(); }); it("should select first workspace", async () => { await subject.execute(); expect(workspace.getWorkspaceFolder).to.have.not.been.called; const prompt = "File Name"; const value = path.join(helper.workspacePathA, path.sep); const valueSelection = [value.length, value.length]; expect(window.showInputBox).to.have.been.calledWithExactly({ prompt, value, valueSelection, ignoreFocusOut: true, }); }); helper.protocol.describe("typeahead configuration", subject, { command: "newFile", items: helper.quickPick.typeahead.items.workspace, }); }); describe("with multiple workspaces", () => { beforeEach(async () => { helper.createWorkspaceFoldersStub(helper.workspaceFolderA, helper.workspaceFolderB); helper.createStubObject(window, "showWorkspaceFolderPick").resolves(helper.workspaceFolderB); }); afterEach(async () => { helper.restoreObject(window.showWorkspaceFolderPick); }); it("should show workspace selector", async () => { await subject.execute(); expect(window.showWorkspaceFolderPick).to.have.been.called; const prompt = "File Name"; const value = path.join(helper.workspaceFolderB.uri.fsPath, path.sep); const valueSelection = [value.length, value.length]; expect(window.showInputBox).to.have.been.calledWithExactly({ prompt, value, valueSelection, ignoreFocusOut: true, }); }); describe("with open document", () => { beforeEach(async () => { helper.createGetWorkspaceFolderStub().returns(helper.workspaceFolderB); await helper.openDocument(helper.editorFile1); await subject.execute(); }); afterEach(async () => { await helper.closeAllEditors(); }); it("should show workspace selector", async () => { expect(window.showWorkspaceFolderPick).to.have.been.called; }); it("should select workspace for open file", async () => { expect(workspace.getWorkspaceFolder).to.have.been.calledWith(Uri.file(helper.editorFile1.fsPath)); }); }); helper.protocol.describe("typeahead configuration", subject, { command: "newFile", items: helper.quickPick.typeahead.items.workspace, }); }); }); }); ================================================ FILE: test/command/RemoveFileCommand.test.ts ================================================ import { expect } from "chai"; import * as fs from "fs"; import * as path from "path"; import { window } from "vscode"; import { RemoveFileCommand } from "../../src/command"; import { RemoveFileController } from "../../src/controller"; import * as helper from "../helper"; describe(RemoveFileCommand.name, () => { const subject = new RemoveFileCommand(new RemoveFileController(helper.createExtensionContext())); beforeEach(helper.beforeEach); afterEach(helper.afterEach); describe("as command", () => { describe("with open text document", () => { beforeEach(async () => { await helper.openDocument(helper.editorFile1); helper.createShowInformationMessageStub().resolves(helper.targetFile.path); helper.createGetConfigurationStub({}); }); afterEach(async () => { await helper.closeAllEditors(); }); describe("configuration", () => { describe('when "explorer.confirmDelete" is "true"', () => { beforeEach(async () => { helper.createGetConfigurationStub({ confirmDelete: true }); }); it("should show a confirmation dialog", async () => { await subject.execute(); const message = `Are you sure you want to delete '${path.basename(helper.editorFile1.path)}'?`; const action = "Move to Trash"; const options = { modal: true }; expect(window.showInformationMessage).to.have.been.calledWith(message, options, action); }); }); describe('when "explorer.confirmDelete" is "false"', () => { beforeEach(async () => { helper.createGetConfigurationStub({ confirmDelete: false }); }); it("should delete the file without confirmation", async () => { await subject.execute(); const message = `${helper.editorFile1.path} does not exist`; expect(window.showInformationMessage).to.have.not.been.called; expect(fs.existsSync(helper.editorFile1.fsPath), message).to.be.false; }); }); }); describe('when answered with "Move to Trash"', () => { it("should delete the file", async () => { await subject.execute(); const message = `${helper.editorFile1.path} does exist`; expect(fs.existsSync(helper.editorFile1.fsPath), message).to.be.false; }); }); describe('when answered with "Cancel"', () => { beforeEach(async () => { helper.createGetConfigurationStub({ confirmDelete: true }); helper.createShowInformationMessageStub().resolves(false); }); it("should leave the file untouched", async () => { try { await subject.execute(); expect.fail("Must fail"); } catch (_e) { const message = `${helper.editorFile1.path} does not exist`; expect(fs.existsSync(helper.editorFile1.fsPath), message).to.be.true; } }); }); describe("prefer uri over current editor", () => { beforeEach(async () => { helper.createGetConfigurationStub({ confirmDelete: false }); }); it("should delete the file without confirmation", async () => { await subject.execute(helper.editorFile2); const message = `${helper.editorFile2.path} does not exist`; expect(fs.existsSync(helper.editorFile2.fsPath), message).to.be.false; }); }); }); describe("without an open text document", () => { beforeEach(async () => { await helper.closeAllEditors(); helper.createShowInformationMessageStub(); }); it("should ignore the command call", async () => { try { await subject.execute(); expect.fail("Must fail"); } catch { expect(window.showInformationMessage).to.have.not.been.called; } }); }); }); }); ================================================ FILE: test/command/RenameFileCommand.test.ts ================================================ import { expect } from "chai"; import * as path from "path"; import { Uri, window } from "vscode"; import { RenameFileCommand } from "../../src/command"; import { RenameFileController } from "../../src/controller/RenameFileController"; import * as helper from "../helper"; describe(RenameFileCommand.name, () => { const subject = new RenameFileCommand(new RenameFileController(helper.createExtensionContext())); beforeEach(async () => { await helper.beforeEach(); }); afterEach(helper.afterEach); describe("as command", () => { describe("with open text document", () => { beforeEach(async () => { await helper.openDocument(helper.editorFile1); helper.createShowInputBoxStub().resolves(helper.targetFile.path); }); afterEach(async () => { await helper.closeAllEditors(); }); it("should prompt for file destination", async () => { await subject.execute(); const prompt = "New Name"; const value = path.basename(helper.editorFile1.fsPath); const valueSelection = [value.length - 9, value.length - 3]; expect(window.showInputBox).to.have.been.calledWithExactly({ prompt, value, valueSelection, ignoreFocusOut: true, }); }); helper.protocol.it("should move current file to destination", subject); helper.protocol.describe("with target file in non-existent nested directory", subject); helper.protocol.it("should open target file as active editor", subject); describe("prefer uri over current editor", () => { beforeEach(async () => { const targetFile = Uri.file(path.resolve(`${helper.editorFile2.fsPath}.tmp`)); helper.createShowInputBoxStub().resolves(targetFile.path); }); it("should prompt for file destination", async () => { await subject.execute(helper.editorFile2); const prompt = "New Name"; const value = path.basename(helper.editorFile2.fsPath); const valueSelection = [value.length - 9, value.length - 3]; expect(window.showInputBox).to.have.been.calledWithExactly({ prompt, value, valueSelection, ignoreFocusOut: true, }); }); }); }); describe("without an open text document", () => { beforeEach(async () => { await helper.closeAllEditors(); helper.createShowInputBoxStub(); }); it("should ignore the command call", async () => { try { await subject.execute(); expect.fail("Must fail"); } catch { expect(window.showInputBox).to.have.not.been.called; } }); }); }); }); ================================================ FILE: test/fixtures/file-1.rb ================================================ class FileOne; end ================================================ FILE: test/fixtures/file-2.rb ================================================ class FileTwo; end ================================================ FILE: test/helper/callbacks.ts ================================================ import { existsSync } from "fs"; import path from "path"; import { Uri, workspace } from "vscode"; import { editorFile1, editorFile2, fixtureFile1, fixtureFile2, tmpDir, workspaceFolderA, workspaceFolderB, } from "./environment"; import { restoreExecuteCommand, restoreGetConfiguration, restoreGetWorkspaceFolder, restoreShowInformationMessage, restoreShowInputBox, restoreShowQuickPick, restoreShowWorkspaceFolderPick, restoreWorkspaceFolders, } from "./stubs"; export async function beforeEach(): Promise { if (existsSync(tmpDir.fsPath)) { await workspace.fs.delete(tmpDir, { recursive: true, useTrash: false }); } await workspace.fs.copy(fixtureFile1, editorFile1, { overwrite: true }); await workspace.fs.copy(fixtureFile2, editorFile2, { overwrite: true }); await workspace.fs.createDirectory(Uri.file(path.resolve(tmpDir.fsPath, "dir-1"))); await workspace.fs.createDirectory(Uri.file(path.resolve(tmpDir.fsPath, "dir-2"))); await workspace.fs.createDirectory(workspaceFolderA.uri); await workspace.fs.createDirectory(workspaceFolderB.uri); await workspace.fs.createDirectory(Uri.file(path.resolve(workspaceFolderA.uri.fsPath, "dir-1"))); await workspace.fs.createDirectory(Uri.file(path.resolve(workspaceFolderA.uri.fsPath, "dir-2"))); await workspace.fs.createDirectory(Uri.file(path.resolve(workspaceFolderB.uri.fsPath, "dir-1"))); await workspace.fs.createDirectory(Uri.file(path.resolve(workspaceFolderB.uri.fsPath, "dir-2"))); } export async function afterEach(): Promise { if (existsSync(tmpDir.fsPath)) { await workspace.fs.delete(tmpDir, { recursive: true, useTrash: false }); } restoreExecuteCommand(); restoreGetConfiguration(); restoreGetWorkspaceFolder(); restoreShowInformationMessage(); restoreShowInputBox(); restoreShowQuickPick(); restoreShowWorkspaceFolderPick(); restoreWorkspaceFolders(); } ================================================ FILE: test/helper/environment.ts ================================================ import * as os from "os"; import * as path from "path"; import { Uri, type WorkspaceFolder } from "vscode"; export const rootDir = path.resolve(__dirname, "..", "..", ".."); export const tmpDir = Uri.file(path.resolve(os.tmpdir(), "vscode-fileutils-test")); export const fixtureFile1 = Uri.file(path.resolve(rootDir, "test", "fixtures", "file-1.rb")); export const fixtureFile2 = Uri.file(path.resolve(rootDir, "test", "fixtures", "file-2.rb")); export const editorFile1 = Uri.file(path.resolve(tmpDir.fsPath, "file-1.rb")); export const editorFile2 = Uri.file(path.resolve(tmpDir.fsPath, "file-2.rb")); export const targetFile = Uri.file(path.resolve(`${editorFile1.fsPath}.tmp`)); export const targetFileWithDot = Uri.file(path.resolve(tmpDir.fsPath, ".eslintrc.json")); export const workspacePathA = path.join(tmpDir.fsPath, "workspaceA"); export const workspacePathB = path.join(tmpDir.fsPath, "workspaceB"); export const workspaceFolderA: WorkspaceFolder = { uri: Uri.file(workspacePathA), name: "a", index: 0 }; export const workspaceFolderB: WorkspaceFolder = { uri: Uri.file(workspacePathB), name: "b", index: 1 }; ================================================ FILE: test/helper/functions.ts ================================================ import retry from "bluebird-retry"; import { TextDecoder } from "util"; import { commands, type ExtensionContext, type Uri, window, workspace } from "vscode"; const textDecoder = new TextDecoder("utf-8"); export async function readFile(file: Uri): Promise { return textDecoder.decode(await workspace.fs.readFile(file)); } export function createExtensionContext(): ExtensionContext { const context = { globalState: { get() { return {}; }, async update(): Promise { return; }, }, }; return context as unknown as ExtensionContext; } export async function openDocument(document: Uri): Promise { const tryOpenDocument = async () => { const textDocument = await workspace.openTextDocument(document); await window.showTextDocument(textDocument); }; await retry(() => tryOpenDocument(), { max_tries: 4, interval: 500 }); } export async function closeAllEditors(): Promise { await commands.executeCommand("workbench.action.closeAllEditors"); } ================================================ FILE: test/helper/index.ts ================================================ import { use } from "chai"; import * as mocha from "mocha"; import sinonChai from "sinon-chai"; import type { Command } from "../../src/command"; import { steps } from "./steps"; import type { Rest } from "./steps/types"; export * from "./callbacks"; export * from "./environment"; export * from "./functions"; export * from "./stubs"; use(sinonChai); export const protocol = { describe(name: string, subject: Command, ...rest: Rest): mocha.Suite { const step = steps.describe[name](subject, ...rest); return mocha.describe(name, step); }, it(name: string, subject: Command, ...rest: Rest): mocha.Test { const step = steps.it[name](subject, ...rest); return mocha.it(name, step); }, }; export const quickPick = { typeahead: { items: { workspace: [ { description: "- workspace root", label: "/" }, { description: undefined, label: "/dir-1" }, { description: undefined, label: "/dir-2" }, ], currentFile: [ { description: "- current file", label: "/" }, { description: undefined, label: "/dir-1" }, { description: undefined, label: "/dir-2" }, { description: undefined, label: "/workspaceA" }, { description: undefined, label: "/workspaceA/dir-1" }, { description: undefined, label: "/workspaceA/dir-2" }, { description: undefined, label: "/workspaceB" }, { description: undefined, label: "/workspaceB/dir-1" }, { description: undefined, label: "/workspaceB/dir-2" }, ], }, options: { placeHolder: "First, select an existing path to create relative to (larger projects may take a moment to load)", ignoreFocusOut: true, }, }, }; ================================================ FILE: test/helper/steps/describe.ts ================================================ import { expect } from "chai"; import * as mocha from "mocha"; import * as path from "path"; import sinon from "sinon"; import { type QuickPickItem, Uri, window, workspace } from "vscode"; import type { Command } from "../../../src/command"; import { quickPick } from ".."; import { editorFile2, targetFile, tmpDir, workspaceFolderA } from "../environment"; import { closeAllEditors, readFile } from "../functions"; import { createGetConfigurationStub, createGetWorkspaceFolderStub, createShowInformationMessageStub, createShowInputBoxStub, createWorkspaceFoldersStub, } from "../stubs"; import type { FuncVoid, Step } from "./types"; export const describe: Step = { "with target file in non-existent nested directory"(subject: Command): FuncVoid { return () => { const targetDir = path.resolve(tmpDir.fsPath, "level-1", "level-2", "level-3"); mocha.beforeEach(async () => createShowInputBoxStub().resolves(path.resolve(targetDir, "file.rb"))); mocha.it("should create nested directories", async () => { await subject.execute(); const textEditor = window.activeTextEditor; expect(textEditor); const dirname = path.dirname(textEditor?.document.fileName ?? ""); const directories: string[] = dirname.split(path.sep); expect(directories.pop()).to.equal("level-3"); expect(directories.pop()).to.equal("level-2"); expect(directories.pop()).to.equal("level-1"); }); }; }, "when target destination exists"(subject: Command, config?: Record): FuncVoid { return () => { mocha.beforeEach(async () => { await workspace.fs.copy(editorFile2, targetFile, { overwrite: true }); createShowInformationMessageStub().resolves({ title: "placeholder" }); }); mocha.it("should prompt with confirmation dialog to overwrite destination file", async () => { await subject.execute(); const message = `File '${targetFile.path}' already exists.`; const action = "Overwrite"; const options = { modal: true }; expect(window.showInformationMessage).to.have.been.calledWith(message, options, action); }); mocha.describe('when answered with "Overwrite"', () => { mocha.it("should overwrite the existig file", async () => { await subject.execute(); const fileContent = await readFile(targetFile); const expectedFileContent = config && "overwriteFileContent" in config ? config.overwriteFileContent : "class FileOne; end"; expect(fileContent).to.equal(expectedFileContent); }); }); mocha.describe('when answered with "Cancel"', () => { mocha.beforeEach(async () => createShowInformationMessageStub().resolves(false)); mocha.it("should leave existing file untouched", async () => { try { await subject.execute(); expect.fail("must fail"); } catch (_e) { const fileContent = await readFile(targetFile); expect(fileContent).to.equal("class FileTwo; end"); } }); }); }; }, "without an open text document"(subject: Command): FuncVoid { return () => { mocha.beforeEach(async () => { await closeAllEditors(); createShowInputBoxStub(); }); mocha.it("should ignore the command call", async () => { try { await subject.execute(); expect.fail("must fail"); } catch { expect(window.showInputBox).to.have.not.been.called; } }); }; }, "typeahead configuration"(subject: Command, options: { command: string; items: QuickPickItem[] }): FuncVoid { const { command, items } = options; return () => { mocha.describe(`when "${command}.typeahead.enabled" is "true"`, () => { mocha.beforeEach(async () => { createGetConfigurationStub({ [`${command}.typeahead.enabled`]: true }); createWorkspaceFoldersStub(workspaceFolderA); }); mocha.it("should show the quick pick dialog", async () => { await subject.execute(); expect(window.showQuickPick).to.have.been.calledOnceWith( sinon.match(items), sinon.match(quickPick.typeahead.options) ); }); }); mocha.describe(`when "${command}.typeahead.enabled" is "false"`, () => { mocha.beforeEach(async () => { createGetConfigurationStub({ [`${command}.typeahead.enabled`]: false }); }); mocha.it("should not show the quick pick dialog", async () => { await subject.execute(); expect(window.showQuickPick).to.have.not.been.called; }); }); }; }, "inputBox configuration"(subject: Command, options: { editorFile: Uri; expectedPath?: string }): FuncVoid { const { editorFile, expectedPath } = options; const runs = [ { pathType: "workspace", pathTypeIndicator: "@" }, { pathType: "workspace", pathTypeIndicator: "" }, { pathType: "workspace", pathTypeIndicator: ":" }, { pathType: "workspace", pathTypeIndicator: " " }, ]; return () => { runs.forEach(({ pathType, pathTypeIndicator }) => { mocha.describe( `when "inputBox.pathType" is "${pathType}" and "inputBox.pathTypeIndicator" is "${pathTypeIndicator}"`, () => { mocha.beforeEach(async () => { createGetConfigurationStub({ "inputBox.pathType": pathType, "inputBox.pathTypeIndicator": pathTypeIndicator, }); const workspaceFolder = path.dirname(editorFile.path); createWorkspaceFoldersStub({ uri: Uri.file(workspaceFolder), name: "workspace-a", index: 0, }); createGetWorkspaceFolderStub().returns(workspaceFolder); }); mocha.it("should show the quick pick dialog", async () => { await subject.execute(); const expectedValue = `${pathTypeIndicator}/${ expectedPath ?? path.basename(editorFile.path) }`; expect(window.showInputBox).to.have.been.calledOnceWith({ prompt: sinon.match.string, value: sinon.match(expectedValue), valueSelection: sinon.match.array, ignoreFocusOut: true, }); }); } ); }); }; }, }; ================================================ FILE: test/helper/steps/index.ts ================================================ import { describe } from "./describe"; import { it } from "./it"; import type { Step } from "./types"; export const steps: Record = { describe, it, }; ================================================ FILE: test/helper/steps/it.ts ================================================ import { expect } from "chai"; import * as fs from "fs"; import { type Uri, window } from "vscode"; import type { Command } from "../../../src/command"; import { editorFile1, targetFile } from "../environment"; import type { FuncVoid, Step } from "./types"; export const it: Step = { "should open target file as active editor"(subject: Command, uri?: Uri): FuncVoid { return async () => { await subject.execute(uri); expect(window.activeTextEditor?.document.fileName).to.equal(targetFile.path); }; }, "should move current file to destination"(subject: Command, uri?: Uri): FuncVoid { return async () => { await subject.execute(uri); const message = `${targetFile} does not exist`; expect(fs.existsSync(targetFile.fsPath), message).to.be.true; }; }, "should prompt for file destination"(subject: Command, prompt: string): FuncVoid { return async () => { await subject.execute(editorFile1); const value = editorFile1.path; const valueSelection = [value.length - 9, value.length - 3]; expect(window.showInputBox).to.have.been.calledWithExactly({ prompt, value, valueSelection, ignoreFocusOut: true, }); }; }, }; it["should duplicate current file to destination"] = it["should move current file to destination"]; ================================================ FILE: test/helper/steps/types.ts ================================================ import type { Command } from "../../../src/command"; export type FuncVoid = () => void; // biome-ignore lint/suspicious/noExplicitAny: Test framework needs flexible parameter types export type Rest = any; export interface Step { [key: string]: (subject: Command, ...rest: Rest[]) => FuncVoid; } ================================================ FILE: test/helper/stubs.ts ================================================ import * as sinon from "sinon"; import { commands, type WorkspaceFolder, window, workspace } from "vscode"; export function createGetWorkspaceFolderStub(): sinon.SinonStub { return createStubObject(workspace, "getWorkspaceFolder"); } export function restoreGetWorkspaceFolder(): void { restoreObject(workspace.getWorkspaceFolder); } export function createWorkspaceFoldersStub(...workspaceFolders: WorkspaceFolder[]): sinon.SinonStub { return createStubObject(workspace, "workspaceFolders").get(() => workspaceFolders); } export function restoreWorkspaceFolders(): void { restoreObject(workspace.workspaceFolders); } export function createExecuteCommandStub(): sinon.SinonStub { return createStubObject(commands, "executeCommand"); } export function restoreExecuteCommand(): void { restoreObject(commands.executeCommand); } export function createGetConfigurationStub(keys: Record): sinon.SinonStub { const config = { get: (key: string) => keys[key] }; return createStubObject(workspace, "getConfiguration").returns(config); } export function restoreGetConfiguration(): void { restoreObject(workspace.getConfiguration); } export function createShowInputBoxStub(): sinon.SinonStub { return createStubObject(window, "showInputBox"); } export function restoreShowInputBox(): void { restoreObject(window.showInputBox); } export function createShowQuickPickStub(): sinon.SinonStub { return createStubObject(window, "showQuickPick"); } export function restoreShowQuickPick(): void { restoreObject(window.showQuickPick); } export function createShowWorkspaceFolderPickStub(): sinon.SinonStub { return createStubObject(window, "showWorkspaceFolderPick"); } export function restoreShowWorkspaceFolderPick(): void { restoreObject(window.showWorkspaceFolderPick); } export function createShowInformationMessageStub(): sinon.SinonStub { return createStubObject(window, "showInformationMessage"); } export function restoreShowInformationMessage(): void { restoreObject(window.showInformationMessage); } // biome-ignore lint/suspicious/noExplicitAny: Handler needs to work with various VS Code API objects type Handler = any; export function createStubObject(handler: Handler, functionName: string): sinon.SinonStub { const target: sinon.SinonStub | undefined = handler[functionName]; const stub: sinon.SinonStub = target && "restore" in target ? target : sinon.stub(handler, functionName); return stub; } export function restoreObject(object: unknown): void { const stub = object as sinon.SinonStub; if (stub?.restore) { stub.restore(); } } ================================================ FILE: test/index.ts ================================================ import glob from "fast-glob"; import Mocha from "mocha"; import * as path from "path"; export async function run(): Promise { const mocha = new Mocha({ reporter: "list", ui: "bdd", color: true, }); const testsRoot = path.resolve(__dirname, ".."); const files = await glob("**/**.test.js", { cwd: testsRoot }); console.log("Number of test files to run:", files.length); // Add files to the test suite files.forEach((file) => mocha.addFile(path.resolve(testsRoot, file))); // Run the mocha test return new Promise((resolve, reject) => { try { mocha.run((failures: number) => { if (failures > 0) { reject(new Error(`${failures} tests failed.`)); } else { resolve(); } }); } catch (err) { reject(err); } }); } ================================================ FILE: test/runTest.ts ================================================ import { runTests } from "@vscode/test-electron"; import * as path from "path"; async function main() { try { // The folder containing the Extension Manifest package.json // Passed to `--extensionDevelopmentPath` const extensionDevelopmentPath = path.resolve(__dirname, "../../"); // The path to test runner // Passed to --extensionTestsPath const extensionTestsPath = path.resolve(__dirname, "./index"); // Download VS Code, unzip it and run the integration test await runTests({ extensionDevelopmentPath, extensionTestsPath, launchArgs: ["--disable-extensions"], }); } catch (_err) { // tslint:disable-next-line: no-console console.error("Failed to run tests"); process.exit(1); } } main(); ================================================ FILE: tsconfig.json ================================================ { "extends": "@tsconfig/node22/tsconfig.json", "compilerOptions": { "rootDir": ".", "outDir": "out", "sourceMap": true, "removeComments": true, "resolveJsonModule": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "allowSyntheticDefaultImports": true }, "exclude": [ "node_modules", ".vscode-test" ] }