├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .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 ├── images ├── demo.gif ├── howto.png ├── icon-96x96.png └── icon.png ├── main.ts ├── package-lock.json ├── 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 /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 4 | #------------------------------------------------------------------------------------------------------------- 5 | 6 | FROM node:19 7 | 8 | # Avoid warnings by switching to noninteractive 9 | ENV DEBIAN_FRONTEND=noninteractive 10 | 11 | # Configure apt and install packages 12 | RUN apt-get update \ 13 | && apt-get -y install --no-install-recommends apt-utils 2>&1 \ 14 | # 15 | # Verify git and needed tools are installed 16 | && apt-get install -y git procps \ 17 | # 18 | # Remove outdated npm from /opt and install via package 19 | # so it can be easily updated via apt-get upgrade npm 20 | && rm -rf /opt/npm-* \ 21 | && rm -f /usr/local/bin/npm \ 22 | && rm -f /usr/local/bin/npmpkg \ 23 | && apt-get install -y curl apt-transport-https lsb-release \ 24 | && curl -sS https://dl.npmpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \ 25 | && echo "deb https://dl.npmpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/npm.list \ 26 | && apt-get update \ 27 | && apt-get -y install --no-install-recommends npm \ 28 | # 29 | # Install tslint and typescript globally 30 | && npm install -g tslint typescript \ 31 | # 32 | # Clean up 33 | && apt-get autoremove -y \ 34 | && apt-get clean -y \ 35 | && rm -rf /var/lib/apt/lists/* 36 | 37 | # Switch back to dialog for any ad-hoc use of apt-get 38 | ENV DEBIAN_FRONTEND=dialog 39 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/vscode-remote/devcontainer.json for format details. 2 | { 3 | "name": "Node.js 8 & TypeScript", 4 | "dockerFile": "Dockerfile", 5 | "extensions": [ 6 | "ms-vscode.vscode-typescript-tslint-plugin", 7 | "sleistner.vscode-fileutils" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | max_line_length = 120 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | indent_style = space 8 | indent_size = 4 9 | 10 | [{*.yml, *.yaml, *.sh, package.json}] 11 | indent_size = 2 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | "**/*.js" 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "@enter-at/typescript-prettier" 4 | ], 5 | rules: { 6 | "class-methods-use-this": "off", 7 | "import/prefer-default-export": "off" 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributing is easy: 4 | 5 | * You can report bugs and request features using the [issues page][issues]. 6 | 7 | [issues]: https://github.com/sleistner/vscode-fileutils/issues 8 | 9 | 10 | We love pull requests from everyone: 11 | 12 | * Fork the project 13 | * Download source code and install dependencies 14 | ```bash 15 | git clone git@github.com:your-username/vscode-fileutils.git 16 | cd vscode-fileutils 17 | npm install 18 | code . 19 | ``` 20 | * Make the respective code changes. 21 | * Go to the debugger in VS Code, choose `Launch Extension` and click run. You can test your changes. 22 | * Choose `Launch Tests` to run the tests. 23 | * Push to your fork and [submit a pull request][pr]. 24 | 25 | [pr]: https://github.com/sleistner/vscode-fileutils/compare/ 26 | 27 | At this point you're waiting on us. We like to at least comment on pull requests 28 | as soon as possible. We may suggest some changes or improvements or alternatives. 29 | 30 | 31 | **Important:** Release and changleog update are executed as TravisCI job. 32 | 33 | Please write commit messages considering Angular Commit Message Conventions. 34 | * https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits 35 | * https://blog.greenkeeper.io/introduction-to-semantic-release-33f73b117c8 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. See error 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Desktop (please complete the following information):** 23 | - VSCode Version: 24 | - OS Version: 25 | - FileUtils Extension Version: 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. 4 | Please also include relevant motivation and context. 5 | List any dependencies that are required for this change. 6 | 7 | Fixes # (issue) 8 | 9 | # Checklist: 10 | 11 | - [ ] My code follows the style guidelines of this project 12 | - [ ] I have performed a self-review of my own code 13 | - [ ] I have made corresponding changes to the documentation 14 | - [ ] My changes generate no new warnings 15 | - [ ] I have added tests that prove my fix is effective or that my feature works 16 | - [ ] New and existing unit tests pass locally with my changes 17 | - [ ] Any dependent changes have been merged and published in downstream modules 18 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | release: 11 | types: 12 | - published 13 | 14 | concurrency: 15 | group: ci-fileutils-${{ github.head_ref || github.run_id }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | lint-test: 20 | name: Lint, Test 21 | strategy: 22 | matrix: 23 | os: 24 | - ubuntu-latest 25 | - macos-latest 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | with: 31 | persist-credentials: false 32 | 33 | - name: Setup Node.js 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: 18.x 37 | 38 | - name: Install dependencies 39 | run: npm install --omit=optional --no-package-lock --no-color --no-description --ignore-scripts 40 | 41 | - name: Run code analysis 42 | run: npm run lint 43 | if: runner.os == 'Linux' 44 | 45 | - name: Run tests on Linux 46 | run: | 47 | sudo apt-get --assume-yes install libsecret-1-0 xclip; 48 | /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 49 | xvfb-run -a npm run test 50 | env: 51 | DISPLAY: ":99.0" 52 | if: runner.os == 'Linux' 53 | 54 | - name: Run tests on macOS and Windows 55 | run: npm run test 56 | if: runner.os != 'Linux' 57 | 58 | release: 59 | name: Release 60 | runs-on: ubuntu-latest 61 | needs: [lint-test] 62 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} 63 | steps: 64 | - name: Checkout 65 | uses: actions/checkout@v3 66 | with: 67 | persist-credentials: false 68 | 69 | - name: Setup Node.js 70 | uses: actions/setup-node@v3 71 | with: 72 | node-version: 18.x 73 | 74 | - name: Install dependencies 75 | run: | 76 | npm install --omit=optional --no-package-lock --no-color --no-description --ignore-scripts 77 | npm install --save-exact esbuild --no-color --no-description 78 | 79 | - name: Run semantic-release 80 | run: npm run semantic-release 81 | env: 82 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 83 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 84 | OVSX_PAT: ${{ secrets.OVSX_PAT }} 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # node-waf configuration 18 | .lock-wscript 19 | 20 | # Compiled binary addons (http://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directory 24 | node_modules 25 | 26 | # Optional npm cache directory 27 | .npm 28 | 29 | # Optional REPL history 30 | .node_repl_history 31 | 32 | out 33 | 34 | .vscode-test 35 | .idea 36 | 37 | *.vsix 38 | 39 | tmp 40 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint && npm run test 5 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branch": "master", 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | "@semantic-release/npm", 7 | "@semantic-release/changelog", 8 | "@semantic-release/git", 9 | "@semantic-release/github" 10 | ], 11 | "prepare": [ 12 | "@semantic-release/npm", 13 | "@semantic-release/changelog", 14 | "@semantic-release/git", 15 | { 16 | "path": "semantic-release-vsce", 17 | "packageVsix": "sleistner.vscode-fileutils.vsix" 18 | } 19 | ], 20 | "publish": [ 21 | "semantic-release-vsce", 22 | { 23 | "path": "@semantic-release/github", 24 | "assets": "sleistner.vscode-fileutils.vsix" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "editorconfig.editorconfig", 7 | "esbenp.prettier-vscode", 8 | "connor4312.esbuild-problem-matchers" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Launch Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--folder-uri=${workspaceFolder}/tmp"], 14 | "outFiles": ["${workspaceFolder}/out/src/**/*.js"], 15 | "preLaunchTask": "npm: watch" 16 | }, 17 | { 18 | "name": "Launch Tests", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": [ 23 | "${workspaceFolder}/test", 24 | "--extensionDevelopmentPath=${workspaceFolder}", 25 | "--extensionTestsPath=${workspaceFolder}/out/test" 26 | ], 27 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 28 | "preLaunchTask": "npm: tsc:watch" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | "editor.codeActionsOnSave": { 10 | "source.organizeImports": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "watch", 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | }, 11 | "problemMatcher": "$esbuild-watch", 12 | "isBackground": true, 13 | "label": "npm: watch" 14 | }, 15 | { 16 | "type": "npm", 17 | "script": "tsc:watch", 18 | "isBackground": true, 19 | "presentation": { 20 | "reveal": "never" 21 | }, 22 | "group": { 23 | "kind": "build", 24 | "isDefault": false 25 | }, 26 | "problemMatcher": ["$tsc-watch"], 27 | "label": "npm: tsc:watch" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | * 2 | */** 3 | 4 | !images/icon.* 5 | !README.md 6 | !CHANGELOG.md 7 | !LICENSE 8 | !out/extension.js 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [3.10.3](https://github.com/sleistner/vscode-fileutils/compare/v3.10.2...v3.10.3) (2023-07-22) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **deps:** update dependency fast-glob to v3.3.1 ([5581fa3](https://github.com/sleistner/vscode-fileutils/commit/5581fa33c582fc43bcc2f951570db60b367b9928)) 7 | 8 | ## [3.10.2](https://github.com/sleistner/vscode-fileutils/compare/v3.10.1...v3.10.2) (2023-06-30) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **deps:** update dependency fast-glob to v3.3.0 ([ad9f56d](https://github.com/sleistner/vscode-fileutils/commit/ad9f56d5a07abc9d9a0a48cc4a03ea3fcd8b4e35)) 14 | 15 | ## [3.10.1](https://github.com/sleistner/vscode-fileutils/compare/v3.10.0...v3.10.1) (2023-03-21) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * rename commands to comply with VSCode style guides ([72f6843](https://github.com/sleistner/vscode-fileutils/commit/72f6843c02d6cbec5d5588a27ef8097e1130e68e)) 21 | 22 | # [3.10.0](https://github.com/sleistner/vscode-fileutils/compare/v3.9.3...v3.10.0) (2023-01-30) 23 | 24 | 25 | ### Features 26 | 27 | * **Settings:** add option to disable context menus ([f3b1431](https://github.com/sleistner/vscode-fileutils/commit/f3b143134f62337a1082ecf30b4588e7dcfab7ae)) 28 | 29 | ## [3.9.3](https://github.com/sleistner/vscode-fileutils/compare/v3.9.2...v3.9.3) (2023-01-30) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * **NewFileController:** show workspace selector when relative to root ([e3fcf96](https://github.com/sleistner/vscode-fileutils/commit/e3fcf962ec74f5b803da963ba9bdfe4676f83aeb)) 35 | 36 | ## [3.9.2](https://github.com/sleistner/vscode-fileutils/compare/v3.9.1...v3.9.2) (2023-01-28) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * **MoveFileController:** disable brace expansion ([8874f26](https://github.com/sleistner/vscode-fileutils/commit/8874f2664c62035e31ad61656fa18d018f5fc36a)) 42 | 43 | ## [3.9.1](https://github.com/sleistner/vscode-fileutils/compare/v3.9.0...v3.9.1) (2023-01-25) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * **build:** include changelog ([550e190](https://github.com/sleistner/vscode-fileutils/commit/550e19025a52bd842e76021045a2786c4f4a8758)) 49 | 50 | # [3.9.0](https://github.com/sleistner/vscode-fileutils/compare/v3.8.0...v3.9.0) (2023-01-25) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * **RenameFileController:** always append base path ([cd6f352](https://github.com/sleistner/vscode-fileutils/commit/cd6f35276331f5ff8b73dbd88443f7f0952b6cc8)) 56 | 57 | 58 | ### Features 59 | 60 | * add inputBox pathTypeIndicator setting ([3390544](https://github.com/sleistner/vscode-fileutils/commit/3390544f5f75ac6dc481827b485a3113447d4b3b)) 61 | * **inputBox:** add path representation configuration ([82e7364](https://github.com/sleistner/vscode-fileutils/commit/82e7364b67551388a1cc58ac1ee8122975f835aa)) 62 | 63 | # [3.8.0](https://github.com/sleistner/vscode-fileutils/compare/v3.7.0...v3.8.0) (2023-01-23) 64 | 65 | 66 | ### Features 67 | 68 | * **DuplicateFile:** add typeahead support ([44ac603](https://github.com/sleistner/vscode-fileutils/commit/44ac603dd241eb61e3732172ffc6cfe80555e0c0)) 69 | * **MoveFile:** add typeahead support ([0e3e0ca](https://github.com/sleistner/vscode-fileutils/commit/0e3e0ca1f5886926758987b594461d57980f750c)) 70 | * **NewFile:** add typeahead setting ([764f614](https://github.com/sleistner/vscode-fileutils/commit/764f614e8e7a8b8d350bd3ad68f785c695bf5e01)) 71 | * **NewFolder:** add dedicated typeahead setting ([6d4359a](https://github.com/sleistner/vscode-fileutils/commit/6d4359a5f7bb4689b22d2ba7b3762c9b602839fb)) 72 | 73 | # [3.7.0](https://github.com/sleistner/vscode-fileutils/compare/v3.6.0...v3.7.0) (2023-01-23) 74 | 75 | 76 | ### Features 77 | 78 | * publish to open vsx repository ([f0d643f](https://github.com/sleistner/vscode-fileutils/commit/f0d643f92aae168505ca750cf9a020cb5610840e)) 79 | 80 | # [3.6.0](https://github.com/sleistner/vscode-fileutils/compare/v3.5.0...v3.6.0) (2023-01-23) 81 | 82 | 83 | ### Bug Fixes 84 | 85 | * **TreeWalker:** replace workspace.findFiles in favor of fast-glob ([37c5078](https://github.com/sleistner/vscode-fileutils/commit/37c50781b4025e31c2023ea568aa78b4ad66714d)) 86 | 87 | ## [3.5.1](https://github.com/sleistner/vscode-fileutils/compare/v3.5.0...v3.5.1) (2023-01-02) 88 | 89 | 90 | ### Bug Fixes 91 | 92 | * **ci:** update semantic release ([6c12e92](https://github.com/sleistner/vscode-fileutils/commit/6c12e92409a9a04d92193781ad1fdb8e17c99ea1)) 93 | 94 | # [3.5.0](https://github.com/sleistner/vscode-fileutils/compare/v3.4.6...v3.5.0) (2022-01-18) 95 | 96 | 97 | ### Features 98 | 99 | * **ci:** enable github actions ([3eead61](https://github.com/sleistner/vscode-fileutils/commit/3eead61d04d6adf1632503d29939e2c150147d87)) 100 | 101 | 102 | ## [3.4.6](https://github.com/sleistner/vscode-fileutils/compare/v3.4.5...v3.4.6) (2022-01-14) 103 | 104 | 105 | ### Bug Fixes 106 | 107 | * trigger gh actions release pipeline ([d9a59c9](https://github.com/sleistner/vscode-fileutils/commit/d9a59c9f974ceb2be6407aed64131e5761acb48a)) 108 | 109 | ## [3.4.5](https://github.com/sleistner/vscode-fileutils/compare/v3.4.4...v3.4.5) (2021-02-22) 110 | 111 | 112 | ### Bug Fixes 113 | 114 | * **deps:** update dependency brace-expansion to v2.0.1 ([dd094d0](https://github.com/sleistner/vscode-fileutils/commit/dd094d0)) 115 | 116 | ## [3.4.4](https://github.com/sleistner/vscode-fileutils/compare/v3.4.3...v3.4.4) (2021-02-01) 117 | 118 | 119 | ### Bug Fixes 120 | 121 | * prefer uri over current editor ([e63b27f](https://github.com/sleistner/vscode-fileutils/commit/e63b27f)) 122 | 123 | ## [3.4.3](https://github.com/sleistner/vscode-fileutils/compare/v3.4.2...v3.4.3) (2021-01-06) 124 | 125 | 126 | ### Bug Fixes 127 | 128 | * **NewFileController:** properly brace expand backslash paths ([ff95aae](https://github.com/sleistner/vscode-fileutils/commit/ff95aae)) 129 | 130 | ## [3.4.2](https://github.com/sleistner/vscode-fileutils/compare/v3.4.1...v3.4.2) (2020-11-17) 131 | 132 | 133 | ### Bug Fixes 134 | 135 | * **build:** include README ([b724700](https://github.com/sleistner/vscode-fileutils/commit/b724700)) 136 | 137 | ## [3.4.1](https://github.com/sleistner/vscode-fileutils/compare/v3.4.0...v3.4.1) (2020-11-08) 138 | 139 | 140 | ### Bug Fixes 141 | 142 | * **build:** include node_modules ([a28b0da](https://github.com/sleistner/vscode-fileutils/commit/a28b0da)) 143 | 144 | # [3.4.0](https://github.com/sleistner/vscode-fileutils/compare/v3.3.3...v3.4.0) (2020-11-06) 145 | 146 | 147 | ### Bug Fixes 148 | 149 | * **readme:** trigger release ([9314428](https://github.com/sleistner/vscode-fileutils/commit/9314428)) 150 | 151 | 152 | ### Features 153 | 154 | * **NewFileCommand:** add support for brace expansion ([5e06afc](https://github.com/sleistner/vscode-fileutils/commit/5e06afc)) 155 | 156 | ## [3.3.3](https://github.com/sleistner/vscode-fileutils/compare/v3.3.2...v3.3.3) (2020-10-26) 157 | 158 | 159 | ### Bug Fixes 160 | 161 | * **New Folder or File Relative to Current View:** cancel execution if no editor is open ([858fea6](https://github.com/sleistner/vscode-fileutils/commit/858fea6)) 162 | 163 | ## [3.3.2](https://github.com/sleistner/vscode-fileutils/compare/v3.3.1...v3.3.2) (2020-10-26) 164 | 165 | 166 | ### Bug Fixes 167 | 168 | * **package:** update extension main file entry ([4892f84](https://github.com/sleistner/vscode-fileutils/commit/4892f84)) 169 | 170 | ## [3.3.1](https://github.com/sleistner/vscode-fileutils/compare/v3.3.0...v3.3.1) (2020-10-25) 171 | 172 | 173 | ### Bug Fixes 174 | 175 | * **duplicate:** prevent directories to be opened as document ([dc1c9f0](https://github.com/sleistner/vscode-fileutils/commit/dc1c9f0)) 176 | 177 | # [3.3.0](https://github.com/sleistner/vscode-fileutils/compare/v3.2.0...v3.3.0) (2020-10-25) 178 | 179 | 180 | ### Features 181 | 182 | * **menus:** add file releated commands to tab and editor context ([a8b748e](https://github.com/sleistner/vscode-fileutils/commit/a8b748e)) 183 | 184 | # [3.2.0](https://github.com/sleistner/vscode-fileutils/compare/v3.1.1...v3.2.0) (2020-10-25) 185 | 186 | 187 | ### Features 188 | 189 | * update icon ([5c2156b](https://github.com/sleistner/vscode-fileutils/commit/5c2156b)) 190 | 191 | ## [3.1.1](https://github.com/sleistner/vscode-fileutils/compare/v3.1.0...v3.1.1) (2020-10-23) 192 | 193 | 194 | ### Bug Fixes 195 | 196 | * **Rename, Move:** keep file in editor group ([5478345](https://github.com/sleistner/vscode-fileutils/commit/5478345)) 197 | 198 | # [3.1.0](https://github.com/sleistner/vscode-fileutils/compare/v3.0.1...v3.1.0) (2020-10-18) 199 | 200 | 201 | ### Features 202 | 203 | * **move/rename:** trigger update imports when moving file ([7a40237](https://github.com/sleistner/vscode-fileutils/commit/7a40237)) 204 | 205 | ## [3.0.1](https://github.com/sleistner/vscode-fileutils/compare/v3.0.0...v3.0.1) (2020-01-15) 206 | 207 | 208 | ### Bug Fixes 209 | 210 | * **FileItem:** ensure file exists before deleting it ([7a44326](https://github.com/sleistner/vscode-fileutils/commit/7a44326)) 211 | 212 | # [3.0.0](https://github.com/sleistner/vscode-fileutils/compare/v2.14.9...v3.0.0) (2019-09-03) 213 | 214 | 215 | ### Bug Fixes 216 | 217 | * **TreeWalker:** handle large directory structures safely ([c419c78](https://github.com/sleistner/vscode-fileutils/commit/c419c78)) 218 | 219 | 220 | ### BREAKING CHANGES 221 | 222 | * **TreeWalker:** The configuration option "typeahead.exclude" has been 223 | removed in favour of VS Code native "files.exclude" option. 224 | 225 | ## [2.14.9](https://github.com/sleistner/vscode-fileutils/compare/v2.14.8...v2.14.9) (2019-08-26) 226 | 227 | 228 | ### Bug Fixes 229 | 230 | * **RemoveFileCommand:** ensure only delete file tab was closed ([557e794](https://github.com/sleistner/vscode-fileutils/commit/557e794)) 231 | 232 | ## [2.14.8](https://github.com/sleistner/vscode-fileutils/compare/v2.14.7...v2.14.8) (2019-08-26) 233 | 234 | 235 | ### Bug Fixes 236 | 237 | * **NewFileCommand:** show quickpick on large directory structures ([8c8c537](https://github.com/sleistner/vscode-fileutils/commit/8c8c537)) 238 | 239 | ## [2.14.7](https://github.com/sleistner/vscode-fileutils/compare/v2.14.6...v2.14.7) (2019-08-23) 240 | 241 | 242 | ### Bug Fixes 243 | 244 | * **NewFileCommand:** show folder selector ([38fb33f](https://github.com/sleistner/vscode-fileutils/commit/38fb33f)) 245 | 246 | ## [2.14.6](https://github.com/sleistner/vscode-fileutils/compare/v2.14.5...v2.14.6) (2019-08-20) 247 | 248 | 249 | ### Bug Fixes 250 | 251 | * missing callback in remote environments ([63ef29a](https://github.com/sleistner/vscode-fileutils/commit/63ef29a)) 252 | 253 | ## [2.14.5](https://github.com/sleistner/vscode-fileutils/compare/v2.14.4...v2.14.5) (2019-06-03) 254 | 255 | 256 | ### Bug Fixes 257 | 258 | * **CopyFileName:** forward and process tab uri ([68ae985](https://github.com/sleistner/vscode-fileutils/commit/68ae985)) 259 | 260 | ## [2.14.4](https://github.com/sleistner/vscode-fileutils/compare/v2.14.3...v2.14.4) (2019-05-29) 261 | 262 | 263 | ### Bug Fixes 264 | 265 | * **FileItem:** update trash import ([850dfff](https://github.com/sleistner/vscode-fileutils/commit/850dfff)) 266 | * **package:** update trash to version 5.0.0 ([51f7017](https://github.com/sleistner/vscode-fileutils/commit/51f7017)) 267 | 268 | ## [2.14.3](https://github.com/sleistner/vscode-fileutils/compare/v2.14.2...v2.14.3) (2019-05-29) 269 | 270 | 271 | ### Bug Fixes 272 | 273 | * **contribution:** reorder conext menu items ([2883402](https://github.com/sleistner/vscode-fileutils/commit/2883402)) 274 | 275 | ## [2.14.2](https://github.com/sleistner/vscode-fileutils/compare/v2.14.1...v2.14.2) (2019-05-29) 276 | 277 | 278 | ### Bug Fixes 279 | 280 | * **package:** update fs-extra to version 8.0.0 ([86ff0b9](https://github.com/sleistner/vscode-fileutils/commit/86ff0b9)) 281 | 282 | ## [2.14.1](https://github.com/sleistner/vscode-fileutils/compare/v2.14.0...v2.14.1) (2019-05-29) 283 | 284 | 285 | ### Bug Fixes 286 | 287 | * icon position ([a273e32](https://github.com/sleistner/vscode-fileutils/commit/a273e32)) 288 | 289 | # [2.14.0](https://github.com/sleistner/vscode-fileutils/compare/v2.13.7...v2.14.0) (2019-05-29) 290 | 291 | 292 | ### Features 293 | 294 | * **editor/title/context:** add rename, remove and copy command ([bb0482e](https://github.com/sleistner/vscode-fileutils/commit/bb0482e)) 295 | 296 | ## [2.13.7](https://github.com/sleistner/vscode-fileutils/compare/v2.13.6...v2.13.7) (2019-04-20) 297 | 298 | 299 | ### Bug Fixes 300 | 301 | * icon color ([21f4eb4](https://github.com/sleistner/vscode-fileutils/commit/21f4eb4)) 302 | 303 | ## [2.13.6](https://github.com/sleistner/vscode-fileutils/compare/v2.13.5...v2.13.6) (2019-04-20) 304 | 305 | 306 | ### Bug Fixes 307 | 308 | * **NewFileCommand:** prompt to select workspace ([8335975](https://github.com/sleistner/vscode-fileutils/commit/8335975)) 309 | 310 | ## [2.13.4](https://github.com/sleistner/vscode-fileutils/compare/v2.13.3...v2.13.4) (2019-01-03) 311 | 312 | 313 | ### Bug Fixes 314 | 315 | * **README:** remove unsupported category ([4a13e08](https://github.com/sleistner/vscode-fileutils/commit/4a13e08)) 316 | 317 | ## [2.13.3](https://github.com/sleistner/vscode-fileutils/compare/v2.13.2...v2.13.3) (2018-11-11) 318 | 319 | 320 | ### Bug Fixes 321 | 322 | * **releaserc:** enable release notes plugin ([b88a7c6](https://github.com/sleistner/vscode-fileutils/commit/b88a7c6)) 323 | * **releaserc:** enable release notes plugin ([eabac50](https://github.com/sleistner/vscode-fileutils/commit/eabac50)) 324 | 325 | ## 2.13.0 (2018-11-10) 326 | 327 | ### Features 328 | - `File: Rename` 329 | - `File: Move` 330 | [iliashkolyar](https://github.com/iliashkolyar) Add configuration to support whether to close old tabs [PR#67](https://github.com/sleistner/vscode-fileutils/pull/67) 331 | 332 | ## 2.12.0 (2018-11-02) 333 | 334 | ### Fixes 335 | 336 | - [iliashkolyar](https://github.com/iliashkolyar) Support file operations on non-textual files [PR#63](https://github.com/sleistner/vscode-fileutils/pull/63) 337 | 338 | ### Features 339 | 340 | - `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) 341 | 342 | ## 2.10.3 (2018-06-15) 343 | 344 | ### Fixes 345 | 346 | - `File: New File`, Show quick pick view only if more than 1 choice available. 347 | 348 | ## 2.10.0 (2018-06-14) 349 | 350 | ### Features 351 | 352 | - `File: New File`, Autocomplete paths when creating a new file. 353 | [PR#48](https://github.com/sleistner/vscode-fileutils/pull/48) 354 | Inspired and heavily borrowed from [https://github.com/patbenatar/vscode-advanced-new-file](https://github.com/patbenatar/vscode-advanced-new-file) 355 | 356 | ## 2.9.0 (2018-05-24) 357 | 358 | ### Features 359 | 360 | - `File: New File`, Adding a trailing / to the supplied target name causes the creation of a new directory. 361 | [PR#25](https://github.com/sleistner/vscode-fileutils/pull/25) 362 | 363 | ## 2.8.1 (2018-02-25) 364 | 365 | ### Fixes 366 | 367 | - Extension can not be loaded due to missing dependency. 368 | 369 | ## 2.8.0 (2018-02-25) 370 | 371 | ### Features 372 | 373 | - `File: Delete`, Add configuration `fileutils.delete.useTrash` in order to move files to trash. 374 | - `File: Delete`, Add configuration `fileutils.delete.confirm` to toggle confirmation dialog. 375 | 376 | ## 2.7.1 (2017-10-25) 377 | 378 | ### Fixes: 379 | 380 | - Renaming and other actions move editor to first group 381 | 382 | ## 2.7.0 (2017-10-05) 383 | 384 | ### Features: 385 | 386 | - [lazyc97](https://github.com/lazyc97) Select filename when inputbox shows up [PR#23](https://github.com/sleistner/vscode-fileutils/pull/23) 387 | 388 | ## 2.6.1 (2017-06-12) 389 | 390 | ### Fixes: 391 | 392 | - Keyboard shortcuts failed to execute 393 | 394 | ## 2.4.1 (2017-03-06) 395 | 396 | ### Features: 397 | 398 | - Enable modal confirmation dialogs 399 | 400 | ## 2.3.4 (2017-03-06) 401 | 402 | ### Fixes: 403 | 404 | - File-New File or Folder failed to execute 405 | 406 | ## 2.3.3 (2017-01-12) 407 | 408 | ### Fixes: 409 | 410 | - File-Duplicate from the context menu doesn't work on Windows 411 | 412 | ## 2.3.1 (2016-10-14) 413 | 414 | ### Features: 415 | 416 | - file browser context menu 417 | 418 | Duplicate 419 | 420 | - file editor context menu 421 | 422 | Duplicate 423 | 424 | - file editor context menu 425 | 426 | Move 427 | 428 | ## 2.0.0 (2016-07-18) 429 | 430 | ### Features: 431 | 432 | - file browser context menu 433 | 434 | Move 435 | 436 | Moves the selected file or directory. 437 | 438 | _(Also creates nested directories)_ 439 | 440 | ### Breaking Changes: 441 | 442 | - command prefix `extensions` has been renamed to `fileutils` 443 | 444 | ## 1.1.0 (2016-05-04) 445 | 446 | ### Features: 447 | 448 | - command 449 | 450 | File: New File Relative to Current View 451 | 452 | Adds a new file relative to file open in active editor. 453 | 454 | _(Also creates nested directories)_ 455 | 456 | - command 457 | 458 | File: New File Relative to Project Root 459 | 460 | Adds a new file relative to project root. 461 | 462 | _(Also creates nested directories)_ 463 | 464 | - command 465 | 466 | File: New Folder Relative to Current View 467 | 468 | Adds a new directory relative to file open in active editor. 469 | 470 | _(Also creates nested directories)_ 471 | 472 | - command 473 | 474 | File: New Folder Relative to Project Root 475 | 476 | Adds a new directory relative to project root. 477 | 478 | _(Also creates nested directories)_ 479 | 480 | ## 1.0.0 (2016-05-03) 481 | 482 | Features: 483 | 484 | - command 485 | 486 | File: Rename 487 | 488 | Renames the file open in active editor. 489 | 490 | _(Also creates nested directories)_ 491 | 492 | - command 493 | 494 | File: Move 495 | 496 | Moves the file open in active editor. 497 | 498 | _(Also creates nested directories)_ 499 | 500 | - command 501 | 502 | File: Duplicate 503 | 504 | Duplicates the file open in active editor. 505 | 506 | _(Also creates nested directories)_ 507 | 508 | - command 509 | 510 | File: Remove 511 | 512 | Deletes the file open in active editor. 513 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Steffen Leistner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![icon](images/icon-96x96.png) 2 | 3 | # File Utils - Visual Studio Code Extension 4 | 5 | [![Known Vulnerabilities](https://snyk.io/test/github/sleistner/vscode-fileutils/badge.svg)](https://snyk.io/test/github/sleistner/vscode-fileutils) 6 | [![Renovate](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com) 7 | [![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) 8 | 9 | --- 10 | 11 | A convenient way of creating, duplicating, moving, renaming, deleting files and directories. 12 | 13 | _Inspired by [Sidebar Enhancements](https://github.com/titoBouzout/SideBarEnhancements) for Sublime._ 14 | 15 | # How to use 16 | 17 | ![demo](images/demo.gif) 18 | 19 | ## Using the context menu: 20 | 21 | - Right click on a file or folder in the Explorer pane or in a file tab. 22 | - Select one of those command: `Copy Name`, `Duplicate...`, `Move...`, `Rename...`, `Delete...`. 23 | - For comand `Copy name`, the file name is copied to the clipboard. 24 | - For other command, answer the additional info requested: 25 | - Duplicate: update path to duplicate near where the command palette is displayed. 26 | - Move: update path to move to near where the command palette is displayed. 27 | - Rename: update name in Explorer pane or near where the command palette is displayed 28 | - Delete: answer Visual Studio Code delete dialog. 29 | 30 | ### Using the command palette: 31 | 32 | - Bring up the command palette, and select "File Utils: ". 33 | - Select one of the commands mentioned below. 34 | - Press [Enter] to confirm, or [Escape] to cancel. 35 | 36 | ![howto](images/howto.png) 37 | 38 | ## Brace Expansion 39 | 40 | > Brace expansion is a mechanism by which arbitrary strings may be generated. 41 | 42 | Example file name input 43 | 44 | ```bash 45 | /tmp/{a,b,c}/index.{cpp,ts,scss} 46 | ``` 47 | 48 | will generate the following files 49 | 50 | ```bash 51 | ➜ tree /tmp 52 | /tmp 53 | ├── a 54 | │ ├── index.cpp 55 | │ ├── index.scss 56 | │ └── index.ts 57 | ├── b 58 | │ ├── index.cpp 59 | │ ├── index.scss 60 | │ └── index.ts 61 | └── c 62 | ├── index.cpp 63 | ├── index.scss 64 | └── index.ts 65 | ``` 66 | 67 | ## Note 68 | 69 | Non-existent folders are created automatically. 70 | 71 | # Changelog 72 | 73 | - [https://github.com/sleistner/vscode-fileutils/blob/master/CHANGELOG.md](https://github.com/sleistner/vscode-fileutils/blob/master/CHANGELOG.md) 74 | 75 | # How to contribute 76 | 77 | - [https://github.com/sleistner/vscode-fileutils/blob/master/CONTRIBUTING.md](https://github.com/sleistner/vscode-fileutils/blob/master/CONTRIBUTING.md) 78 | 79 | # Disclaimer 80 | 81 | **Important:** This extension due to the nature of it's purpose will create 82 | files on your hard drive and if necessary create the respective folder structure. 83 | While it should not override any files during this process, I'm not giving any guarantees 84 | or take any responsibility in case of lost data. 85 | 86 | # Contributors 87 | 88 | - [Steffen Leistner](https://github.com/sleistner) 89 | - [Ilia Shkolyar](https://github.com/iliashkolyar) 90 | 91 | # License 92 | 93 | MIT 94 | 95 | # Credits 96 | 97 | ## Icon 98 | 99 | - [Janosch, Green Tropical Waters - Utilities Icon](https://iconarchive.com/show/tropical-waters-folders-icons-by-janosch500/Utilities-icon.html) 100 | -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleistner/vscode-fileutils/a7d6be1448b9712a6ee6effa64c1d257fe7e5833/images/demo.gif -------------------------------------------------------------------------------- /images/howto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleistner/vscode-fileutils/a7d6be1448b9712a6ee6effa64c1d257fe7e5833/images/howto.png -------------------------------------------------------------------------------- /images/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleistner/vscode-fileutils/a7d6be1448b9712a6ee6effa64c1d257fe7e5833/images/icon-96x96.png -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleistner/vscode-fileutils/a7d6be1448b9712a6ee6effa64c1d257fe7e5833/images/icon.png -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | export { activate } from "./src/extension"; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-fileutils", 3 | "displayName": "File Utils", 4 | "description": "A convenient way of creating, duplicating, moving, renaming and deleting files and directories.", 5 | "version": "3.10.3", 6 | "private": true, 7 | "license": "MIT", 8 | "publisher": "sleistner", 9 | "engines": { 10 | "vscode": "^1.74.0" 11 | }, 12 | "categories": [ 13 | "Other" 14 | ], 15 | "keywords": [ 16 | "utils", 17 | "files", 18 | "move", 19 | "duplicate", 20 | "rename" 21 | ], 22 | "icon": "images/icon.png", 23 | "galleryBanner": { 24 | "color": "#1c2237", 25 | "theme": "dark" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/sleistner/vscode-fileutils/issues" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/sleistner/vscode-fileutils.git" 33 | }, 34 | "homepage": "https://github.com/sleistner/vscode-fileutils/blob/master/README.md", 35 | "main": "./out/extension.js", 36 | "activationEvents": [ 37 | "onCommand:fileutils.renameFile", 38 | "onCommand:fileutils.moveFile", 39 | "onCommand:fileutils.duplicateFile", 40 | "onCommand:fileutils.removeFile", 41 | "onCommand:fileutils.newFile", 42 | "onCommand:fileutils.newFileAtRoot", 43 | "onCommand:fileutils.newFolder", 44 | "onCommand:fileutils.newFolderAtRoot", 45 | "onCommand:fileutils.copyFileName" 46 | ], 47 | "contributes": { 48 | "commands": [ 49 | { 50 | "command": "fileutils.renameFile", 51 | "category": "File Utils", 52 | "title": "Rename..." 53 | }, 54 | { 55 | "command": "fileutils.moveFile", 56 | "category": "File Utils", 57 | "title": "Move..." 58 | }, 59 | { 60 | "command": "fileutils.duplicateFile", 61 | "category": "File Utils", 62 | "title": "Duplicate..." 63 | }, 64 | { 65 | "command": "fileutils.removeFile", 66 | "category": "File Utils", 67 | "title": "Delete" 68 | }, 69 | { 70 | "command": "fileutils.newFile", 71 | "category": "File Utils", 72 | "title": "New File Relative to Current View..." 73 | }, 74 | { 75 | "command": "fileutils.newFileAtRoot", 76 | "category": "File Utils", 77 | "title": "New File Relative to Project Root..." 78 | }, 79 | { 80 | "command": "fileutils.newFolder", 81 | "category": "File Utils", 82 | "title": "New Folder Relative to Current View..." 83 | }, 84 | { 85 | "command": "fileutils.newFolderAtRoot", 86 | "category": "File Utils", 87 | "title": "New Folder Relative to Project Root..." 88 | }, 89 | { 90 | "command": "fileutils.copyFileName", 91 | "category": "File Utils", 92 | "title": "Copy Name" 93 | } 94 | ], 95 | "menus": { 96 | "explorer/context": [ 97 | { 98 | "command": "fileutils.moveFile", 99 | "group": "7_modification", 100 | "when": "config.fileutils.menus.context.explorer =~ /moveFile/" 101 | }, 102 | { 103 | "command": "fileutils.duplicateFile", 104 | "group": "7_modification", 105 | "when": "config.fileutils.menus.context.explorer =~ /duplicateFile/" 106 | }, 107 | { 108 | "command": "fileutils.newFileAtRoot", 109 | "group": "2_workspace", 110 | "when": "config.fileutils.menus.context.explorer =~ /newFileAtRoot/" 111 | }, 112 | { 113 | "command": "fileutils.newFolderAtRoot", 114 | "group": "2_workspace", 115 | "when": "config.fileutils.menus.context.explorer =~ /newFolderAtRoot/" 116 | }, 117 | { 118 | "command": "fileutils.copyFileName", 119 | "group": "6_copypath", 120 | "when": "config.fileutils.menus.context.explorer =~ /copyFileName/" 121 | } 122 | ], 123 | "editor/context": [ 124 | { 125 | "command": "fileutils.copyFileName", 126 | "group": "1_copypath", 127 | "when": "config.fileutils.menus.context.editor =~ /copyFileName/ && resourceScheme != output" 128 | }, 129 | { 130 | "command": "fileutils.renameFile", 131 | "group": "1_modification@1", 132 | "when": "config.fileutils.menus.context.editor =~ /renameFile/ && resourceScheme != output" 133 | }, 134 | { 135 | "command": "fileutils.moveFile", 136 | "group": "1_modification@2", 137 | "when": "config.fileutils.menus.context.editor =~ /moveFile/ && resourceScheme != output" 138 | }, 139 | { 140 | "command": "fileutils.duplicateFile", 141 | "group": "1_modification@3", 142 | "when": "config.fileutils.menus.context.editor =~ /duplicateFile/ && resourceScheme != output" 143 | }, 144 | { 145 | "command": "fileutils.removeFile", 146 | "group": "1_modification@4", 147 | "when": "config.fileutils.menus.context.editor =~ /removeFile/ && resourceScheme != output" 148 | } 149 | ], 150 | "editor/title/context": [ 151 | { 152 | "command": "fileutils.copyFileName", 153 | "group": "1_copypath", 154 | "when": "config.fileutils.menus.context.editorTitle =~ /copyFileName/" 155 | }, 156 | { 157 | "command": "fileutils.renameFile", 158 | "group": "1_modification@1", 159 | "when": "config.fileutils.menus.context.editorTitle =~ /renameFile/" 160 | }, 161 | { 162 | "command": "fileutils.moveFile", 163 | "group": "1_modification@2", 164 | "when": "config.fileutils.menus.context.editorTitle =~ /moveFile/" 165 | }, 166 | { 167 | "command": "fileutils.duplicateFile", 168 | "group": "1_modification@3", 169 | "when": "config.fileutils.menus.context.editorTitle =~ /duplicateFile/" 170 | }, 171 | { 172 | "command": "fileutils.removeFile", 173 | "group": "1_modification@4", 174 | "when": "config.fileutils.menus.context.editorTitle =~ /removeFile/" 175 | } 176 | ] 177 | }, 178 | "configuration": { 179 | "type": "object", 180 | "title": "File Utils", 181 | "properties": { 182 | "fileutils.typeahead.enabled": { 183 | "type": "boolean", 184 | "default": true, 185 | "description": "Controls whether to show a directory selector for new file and new folder command.", 186 | "markdownDeprecationMessage": "**Deprecated**: Please use `#fileutils.newFile.typeahead.enabled#` or `#fileutils.newFolder.typeahead.enabled#` instead.", 187 | "deprecationMessage": "Deprecated: Please use fileutils.newFile.typeahead.enabled or fileutils.newFolder.typeahead.enabled instead." 188 | }, 189 | "fileutils.duplicateFile.typeahead.enabled": { 190 | "type": "boolean", 191 | "default": false, 192 | "description": "Controls whether to show a directory selector for the duplicate file command." 193 | }, 194 | "fileutils.moveFile.typeahead.enabled": { 195 | "type": "boolean", 196 | "default": false, 197 | "description": "Controls whether to show a directory selector for the move file command." 198 | }, 199 | "fileutils.newFile.typeahead.enabled": { 200 | "type": "boolean", 201 | "default": true, 202 | "description": "Controls whether to show a directory selector for the new file command." 203 | }, 204 | "fileutils.newFolder.typeahead.enabled": { 205 | "type": "boolean", 206 | "default": true, 207 | "description": "Controls whether to show a directory selector for new folder command." 208 | }, 209 | "fileutils.inputBox.pathType": { 210 | "type": "string", 211 | "default": "root", 212 | "enum": [ 213 | "root", 214 | "workspace" 215 | ], 216 | "enumDescriptions": [ 217 | "Absolute file path of the opened workspace or folder (e.g. /Users/Development/myWorkspace)", 218 | "Relative file path of the opened workspace or folder (e.g. /myWorkspace)" 219 | ], 220 | "description": "Controls the path that is shown in the input box." 221 | }, 222 | "fileutils.inputBox.pathTypeIndicator": { 223 | "type": "string", 224 | "default": "@", 225 | "maxLength": 50, 226 | "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'.", 227 | "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`." 228 | }, 229 | "fileutils.menus.context.explorer": { 230 | "type": "array", 231 | "default": [ 232 | "moveFile", 233 | "duplicateFile", 234 | "newFileAtRoot", 235 | "newFolderAtRoot", 236 | "copyFileName" 237 | ], 238 | "items": { 239 | "type": "string", 240 | "enum": [ 241 | "moveFile", 242 | "duplicateFile", 243 | "newFileAtRoot", 244 | "newFolderAtRoot", 245 | "copyFileName" 246 | ], 247 | "enumDescriptions": [ 248 | "Move", 249 | "Duplicate", 250 | "New File Relative to Project Root", 251 | "New Folder Relative to Project Root", 252 | "Copy Name" 253 | ] 254 | }, 255 | "uniqueItems": true, 256 | "description": "Controls whether to show the command in the explorer context menu.", 257 | "order": 90 258 | }, 259 | "fileutils.menus.context.editor": { 260 | "type": "array", 261 | "default": [ 262 | "renameFile", 263 | "moveFile", 264 | "duplicateFile", 265 | "removeFile", 266 | "copyFileName" 267 | ], 268 | "items": { 269 | "type": "string", 270 | "enum": [ 271 | "renameFile", 272 | "moveFile", 273 | "duplicateFile", 274 | "removeFile", 275 | "copyFileName" 276 | ], 277 | "enumDescriptions": [ 278 | "Rename", 279 | "Move", 280 | "Duplicate", 281 | "Remove", 282 | "Copy Name" 283 | ] 284 | }, 285 | "uniqueItems": true, 286 | "description": "Controls whether to show the command in the editor context menu.", 287 | "order": 100 288 | }, 289 | "fileutils.menus.context.editorTitle": { 290 | "type": "array", 291 | "default": [ 292 | "renameFile", 293 | "moveFile", 294 | "duplicateFile", 295 | "removeFile", 296 | "copyFileName" 297 | ], 298 | "items": { 299 | "type": "string", 300 | "enum": [ 301 | "renameFile", 302 | "moveFile", 303 | "duplicateFile", 304 | "removeFile", 305 | "copyFileName" 306 | ], 307 | "enumDescriptions": [ 308 | "Rename", 309 | "Move", 310 | "Duplicate", 311 | "Remove", 312 | "Copy Name" 313 | ] 314 | }, 315 | "uniqueItems": true, 316 | "description": "Controls whether to show the command in the editor title context menu.", 317 | "order": 110 318 | } 319 | } 320 | } 321 | }, 322 | "scripts": { 323 | "vscode:prepublish": "npm run -S esbuild-base -- --minify", 324 | "esbuild-base": "npm run clean && esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node", 325 | "watch": "scripts/dev-env && npm run -S esbuild-base -- --sourcemap --watch", 326 | "tsc:watch": "tsc -watch -p ./", 327 | "pretest": "tsc -p ./", 328 | "test": "node ./out/test/runTest.js", 329 | "lint": "eslint './{src,test}/**/*.ts'", 330 | "lint:fix": "npm run lint -- --fix", 331 | "semantic-release": "semantic-release", 332 | "prepare": "[ ! -x ./node_modules/.bin/husky ] && exit 0; husky install", 333 | "clean": "rimraf out" 334 | }, 335 | "devDependencies": { 336 | "@enter-at/eslint-config-typescript-prettier": "1.7.17", 337 | "@semantic-release/changelog": "6.0.3", 338 | "@semantic-release/git": "10.0.1", 339 | "@tsconfig/node18": "1.0.3", 340 | "@types/bluebird": "3.5.41", 341 | "@types/bluebird-retry": "0.11.7", 342 | "@types/brace-expansion": "1.1.1", 343 | "@types/chai": "4.3.9", 344 | "@types/mocha": "10.0.3", 345 | "@types/node": "18.11.18", 346 | "@types/sinon": "10.0.20", 347 | "@types/sinon-chai": "3.2.11", 348 | "@types/vscode": "1.74.0", 349 | "@vscode/test-electron": "2.3.5", 350 | "bluebird": "3.7.2", 351 | "bluebird-retry": "0.11.0", 352 | "chai": "4.3.10", 353 | "esbuild": "0.19.5", 354 | "eslint": "8.52.0", 355 | "husky": "8.0.3", 356 | "mocha": "10.2.0", 357 | "prettier": "2.8.8", 358 | "semantic-release": "20.1.3", 359 | "semantic-release-vsce": "5.6.3", 360 | "sinon": "15.2.0", 361 | "sinon-chai": "3.7.0", 362 | "typescript": "4.9.5" 363 | }, 364 | "dependencies": { 365 | "brace-expansion": "2.0.1", 366 | "fast-glob": "3.3.1" 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "ignoreDeps": ["@types/node", "@types/vscode"], 6 | "packageRules": [ 7 | { 8 | "updateTypes": ["minor", "patch", "pin", "digest"], 9 | "automerge": true 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /scripts/dev-env: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -rf ./tmp 4 | mkdir -p ./tmp/{app,workspace,scripts} 5 | touch ./tmp/{app,workspace,scripts}/{foo,bar,baz}.ts 6 | -------------------------------------------------------------------------------- /src/FileItem.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import { Uri, workspace, WorkspaceEdit } from "vscode"; 4 | 5 | function assertTargetPath(targetPath: Uri | undefined): asserts targetPath is Uri { 6 | if (targetPath === undefined) { 7 | throw new Error("Missing target path"); 8 | } 9 | } 10 | 11 | export class FileItem { 12 | private SourcePath: Uri; 13 | private TargetPath: Uri | undefined; 14 | 15 | constructor(sourcePath: Uri | string, targetPath?: Uri | string, private IsDir: boolean = false) { 16 | this.SourcePath = this.toUri(sourcePath); 17 | if (targetPath !== undefined) { 18 | this.TargetPath = this.toUri(targetPath); 19 | } 20 | } 21 | 22 | get name(): string { 23 | return path.basename(this.SourcePath.path); 24 | } 25 | 26 | get path(): Uri { 27 | return this.SourcePath; 28 | } 29 | 30 | get targetPath(): Uri | undefined { 31 | return this.TargetPath; 32 | } 33 | 34 | get exists(): boolean { 35 | if (this.targetPath === undefined) { 36 | return false; 37 | } 38 | return fs.existsSync(this.targetPath.fsPath); 39 | } 40 | 41 | get isDir(): boolean { 42 | return this.IsDir; 43 | } 44 | 45 | public async move(): Promise { 46 | assertTargetPath(this.targetPath); 47 | 48 | const edit = new WorkspaceEdit(); 49 | edit.renameFile(this.path, this.targetPath, { overwrite: true }); 50 | const result = await workspace.applyEdit(edit); 51 | 52 | if (!result) { 53 | throw new Error(`Failed to move file "${this.targetPath.fsPath}."`); 54 | } 55 | 56 | this.SourcePath = this.targetPath; 57 | return this; 58 | } 59 | 60 | public async duplicate(): Promise { 61 | assertTargetPath(this.targetPath); 62 | 63 | try { 64 | await workspace.fs.copy(this.path, this.targetPath, { overwrite: true }); 65 | return new FileItem(this.targetPath, undefined, this.isDir); 66 | } catch (error) { 67 | throw new Error(`Failed to duplicate file "${this.targetPath.fsPath}. (${error})"`); 68 | } 69 | } 70 | 71 | public async remove(): Promise { 72 | const edit = new WorkspaceEdit(); 73 | edit.deleteFile(this.path, { recursive: true, ignoreIfNotExists: true }); 74 | const result = await workspace.applyEdit(edit); 75 | 76 | if (!result) { 77 | throw new Error(`Failed to delete file "${this.path.fsPath}."`); 78 | } 79 | 80 | return this; 81 | } 82 | 83 | public async create(mkDir?: boolean): Promise { 84 | assertTargetPath(this.targetPath); 85 | 86 | if (this.exists) { 87 | await workspace.fs.delete(this.targetPath, { recursive: true }); 88 | } 89 | 90 | if (mkDir === true || this.isDir) { 91 | await workspace.fs.createDirectory(this.targetPath); 92 | } else { 93 | await workspace.fs.writeFile(this.targetPath, new Uint8Array()); 94 | } 95 | 96 | return new FileItem(this.targetPath, undefined, this.isDir); 97 | } 98 | 99 | private toUri(uriOrString: Uri | string): Uri { 100 | return uriOrString instanceof Uri ? uriOrString : Uri.file(uriOrString); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/command/BaseCommand.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from "vscode"; 2 | import { FileController } from "../controller"; 3 | import { FileItem } from "../FileItem"; 4 | import { Command, CommandConstructorOptions } from "./Command"; 5 | 6 | interface ExecuteControllerOptions { 7 | openFileInEditor?: boolean; 8 | } 9 | 10 | export abstract class BaseCommand implements Command { 11 | constructor(protected controller: T, readonly options?: CommandConstructorOptions) {} 12 | 13 | public abstract execute(uri?: Uri): Promise; 14 | 15 | protected async executeController( 16 | fileItem: FileItem | undefined, 17 | options?: ExecuteControllerOptions 18 | ): Promise { 19 | if (fileItem) { 20 | const result = await this.controller.execute({ fileItem }); 21 | if (options?.openFileInEditor) { 22 | await this.controller.openFileInEditor(result); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/command/Command.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from "vscode"; 2 | 3 | export interface CommandConstructorOptions { 4 | relativeToRoot?: boolean; 5 | } 6 | 7 | export interface Command { 8 | execute(uri?: Uri): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /src/command/CopyFileNameCommand.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from "vscode"; 2 | import { CopyFileNameController } from "../controller/CopyFileNameController"; 3 | import { BaseCommand } from "./BaseCommand"; 4 | 5 | export class CopyFileNameCommand extends BaseCommand { 6 | public async execute(uri?: Uri): Promise { 7 | const dialogOptions = { uri }; 8 | const fileItem = await this.controller.showDialog(dialogOptions); 9 | await this.executeController(fileItem); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/command/DuplicateFileCommand.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from "vscode"; 2 | import { MoveFileController } from "../controller/MoveFileController"; 3 | import { getConfiguration } from "../lib/config"; 4 | import { BaseCommand } from "./BaseCommand"; 5 | 6 | export class DuplicateFileCommand extends BaseCommand { 7 | public async execute(uri?: Uri): Promise { 8 | const typeahead = getConfiguration("duplicateFile.typeahead.enabled") === true; 9 | const dialogOptions = { prompt: "Duplicate As", uri, typeahead }; 10 | const fileItem = await this.controller.showDialog(dialogOptions); 11 | await this.executeController(fileItem, { openFileInEditor: !fileItem?.isDir }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/command/MoveFileCommand.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from "vscode"; 2 | import { MoveFileController } from "../controller/MoveFileController"; 3 | import { getConfiguration } from "../lib/config"; 4 | import { BaseCommand } from "./BaseCommand"; 5 | 6 | export class MoveFileCommand extends BaseCommand { 7 | public async execute(uri?: Uri): Promise { 8 | const typeahead = getConfiguration("moveFile.typeahead.enabled") === true; 9 | const dialogOptions = { prompt: "New Location", uri, typeahead }; 10 | const fileItem = await this.controller.showDialog(dialogOptions); 11 | await this.executeController(fileItem); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/command/NewFileCommand.ts: -------------------------------------------------------------------------------- 1 | import { NewFileController } from "../controller/NewFileController"; 2 | import { getConfiguration } from "../lib/config"; 3 | import { BaseCommand } from "./BaseCommand"; 4 | 5 | export class NewFileCommand extends BaseCommand { 6 | public async execute(): Promise { 7 | const typeahead = this.typeahead; 8 | const relativeToRoot = this.options?.relativeToRoot ?? false; 9 | const dialogOptions = { prompt: "File Name", relativeToRoot, typeahead }; 10 | const fileItems = await this.controller.showDialog(dialogOptions); 11 | 12 | if (fileItems) { 13 | const executions = [...fileItems].map(async (fileItem) => { 14 | const result = await this.controller.execute({ fileItem }); 15 | await this.controller.openFileInEditor(result); 16 | }); 17 | await Promise.all(executions); 18 | } 19 | } 20 | 21 | protected get typeahead(): boolean { 22 | return (getConfiguration("newFile.typeahead.enabled") ?? getConfiguration("typeahead.enabled")) === true; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/command/NewFolderCommand.ts: -------------------------------------------------------------------------------- 1 | import { getConfiguration } from "../lib/config"; 2 | import { NewFileCommand } from "./NewFileCommand"; 3 | 4 | export class NewFolderCommand extends NewFileCommand { 5 | public async execute(): Promise { 6 | const typeahead = this.typeahead; 7 | const relativeToRoot = this.options?.relativeToRoot ?? false; 8 | const dialogOptions = { prompt: "Folder Name", relativeToRoot, typeahead }; 9 | const fileItems = await this.controller.showDialog(dialogOptions); 10 | 11 | if (fileItems) { 12 | const executions = [...fileItems].map(async (fileItem) => { 13 | await this.controller.execute({ fileItem, isDir: true }); 14 | }); 15 | await Promise.all(executions); 16 | } 17 | } 18 | 19 | protected get typeahead(): boolean { 20 | return (getConfiguration("newFolder.typeahead.enabled") ?? getConfiguration("typeahead.enabled")) === true; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/command/RemoveFileCommand.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from "vscode"; 2 | import { RemoveFileController } from "../controller"; 3 | import { BaseCommand } from "./BaseCommand"; 4 | 5 | export class RemoveFileCommand extends BaseCommand { 6 | public async execute(uri?: Uri): Promise { 7 | const fileItem = await this.controller.showDialog({ uri }); 8 | await this.executeController(fileItem); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/command/RenameFileCommand.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from "vscode"; 2 | import { RenameFileController } from "../controller/RenameFileController"; 3 | import { BaseCommand } from "./BaseCommand"; 4 | 5 | export class RenameFileCommand extends BaseCommand { 6 | public async execute(uri?: Uri): Promise { 7 | const dialogOptions = { prompt: "New Name", uri }; 8 | const fileItem = await this.controller.showDialog(dialogOptions); 9 | await this.executeController(fileItem); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/command/index.ts: -------------------------------------------------------------------------------- 1 | export { NewFileCommand } from "./NewFileCommand"; 2 | export { NewFolderCommand } from "./NewFolderCommand"; 3 | export { RenameFileCommand } from "./RenameFileCommand"; 4 | export { RemoveFileCommand } from "./RemoveFileCommand"; 5 | export { DuplicateFileCommand } from "./DuplicateFileCommand"; 6 | export { MoveFileCommand } from "./MoveFileCommand"; 7 | export { CopyFileNameCommand } from "./CopyFileNameCommand"; 8 | export { Command } from "./Command"; 9 | -------------------------------------------------------------------------------- /src/controller/BaseFileController.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { 3 | commands, 4 | env, 5 | ExtensionContext, 6 | InputBoxOptions, 7 | TextEditor, 8 | Uri, 9 | window, 10 | workspace, 11 | WorkspaceFolder, 12 | } from "vscode"; 13 | import { FileItem } from "../FileItem"; 14 | import { Cache } from "../lib/Cache"; 15 | import { getConfiguration } from "../lib/config"; 16 | import { DialogOptions, ExecuteOptions, FileController, SourcePathOptions } from "./FileController"; 17 | import { TypeAheadController } from "./TypeAheadController"; 18 | 19 | enum InputBoxPathType { 20 | Root = "root", 21 | Workspace = "workspace", 22 | } 23 | 24 | type TargetPathInputBoxOptions = InputBoxOptions & Required>; 25 | 26 | export interface TargetPathInputBoxValueOptions extends DialogOptions { 27 | workspaceFolderPath?: string; 28 | pathType: InputBoxPathType; 29 | } 30 | 31 | export abstract class BaseFileController implements FileController { 32 | constructor(protected context: ExtensionContext) {} 33 | 34 | public abstract showDialog(options?: DialogOptions): Promise; 35 | 36 | public abstract execute(options: ExecuteOptions): Promise; 37 | 38 | private get pathTypeIndicator(): string { 39 | return getConfiguration("inputBox.pathTypeIndicator") ?? ""; 40 | } 41 | 42 | public async openFileInEditor(fileItem: FileItem): Promise { 43 | if (fileItem.isDir) { 44 | return; 45 | } 46 | 47 | const textDocument = await workspace.openTextDocument(fileItem.path); 48 | if (!textDocument) { 49 | throw new Error("Could not open file!"); 50 | } 51 | 52 | const editor = await window.showTextDocument(textDocument); 53 | if (!editor) { 54 | throw new Error("Could not show document!"); 55 | } 56 | 57 | return editor; 58 | } 59 | 60 | public async closeCurrentFileEditor(): Promise { 61 | return commands.executeCommand("workbench.action.closeActiveEditor"); 62 | } 63 | 64 | protected async getTargetPath(sourcePath: string, options: DialogOptions): Promise { 65 | const { prompt } = options; 66 | 67 | const pathType = this.getInputBoxPathType(); 68 | const workspaceFolderPath = await this.getWorkspaceFolderPath(); 69 | const value = await this.getTargetPathInputBoxValue(sourcePath, { 70 | ...options, 71 | workspaceFolderPath, 72 | pathType, 73 | }); 74 | 75 | const targetPath = await this.showTargetPathInputBox({ 76 | prompt, 77 | value, 78 | }); 79 | 80 | const shouldRestoreAbsolutePath = targetPath && workspaceFolderPath && pathType === InputBoxPathType.Workspace; 81 | 82 | if (shouldRestoreAbsolutePath) { 83 | return path.join( 84 | workspaceFolderPath, 85 | targetPath.replace(new RegExp(`^(${this.pathTypeIndicator}|${workspaceFolderPath})`, "g"), "") 86 | ); 87 | } 88 | 89 | return targetPath; 90 | } 91 | 92 | protected async showTargetPathInputBox(options: TargetPathInputBoxOptions): Promise { 93 | const { prompt, value } = options; 94 | const valueSelection = this.getFilenameSelection(value); 95 | 96 | return await window.showInputBox({ 97 | prompt, 98 | value, 99 | valueSelection, 100 | ignoreFocusOut: true, 101 | }); 102 | } 103 | 104 | private getInputBoxPathType(): InputBoxPathType { 105 | const pathType: InputBoxPathType | undefined = getConfiguration("inputBox.pathType"); 106 | 107 | if (pathType && Object.values(InputBoxPathType).includes(pathType)) { 108 | return pathType; 109 | } 110 | return InputBoxPathType.Root; 111 | } 112 | 113 | protected async getTargetPathInputBoxValue( 114 | sourcePath: string, 115 | options: TargetPathInputBoxValueOptions 116 | ): Promise { 117 | const { workspaceFolderPath, pathType } = options; 118 | 119 | if (pathType === InputBoxPathType.Workspace && workspaceFolderPath) { 120 | return sourcePath.replace(workspaceFolderPath, this.pathTypeIndicator); 121 | } 122 | return sourcePath; 123 | } 124 | 125 | protected getFilenameSelection(value: string): [number, number] { 126 | return [value.length, value.length]; 127 | } 128 | 129 | public async getSourcePath({ ignoreIfNotExists, uri }: SourcePathOptions = {}): Promise { 130 | if (uri?.fsPath) { 131 | return uri.fsPath; 132 | } 133 | // Attempting to get the fileName from the activeTextEditor. 134 | // Works for text files only. 135 | const activeEditor = window.activeTextEditor; 136 | if (activeEditor && activeEditor.document && activeEditor.document.fileName) { 137 | return activeEditor.document.fileName; 138 | } 139 | 140 | // No activeTextEditor means that we don't have an active file or 141 | // the active file is a non-text file (e.g. binary files such as images). 142 | // Since there is no actual API to differentiate between the scenarios, we try to retrieve 143 | // the path for a non-textual file before throwing an error. 144 | const sourcePath = await this.getSourcePathForNonTextFile(); 145 | if (!sourcePath && ignoreIfNotExists !== true) { 146 | throw new Error(); 147 | } 148 | 149 | return sourcePath; 150 | } 151 | 152 | protected getCache(namespace: string): Cache { 153 | return new Cache(this.context.globalState, namespace); 154 | } 155 | 156 | protected async ensureWritableFile(fileItem: FileItem): Promise { 157 | if (!fileItem.exists) { 158 | return fileItem; 159 | } 160 | 161 | if (fileItem.targetPath === undefined) { 162 | throw new Error("Missing target path"); 163 | } 164 | 165 | const message = `File '${fileItem.targetPath.path}' already exists.`; 166 | const action = "Overwrite"; 167 | const overwrite = await window.showInformationMessage(message, { modal: true }, action); 168 | if (overwrite) { 169 | return fileItem; 170 | } 171 | throw new Error(); 172 | } 173 | 174 | private async getSourcePathForNonTextFile(): Promise { 175 | // Since there is no API to get details of non-textual files, the following workaround is performed: 176 | // 1. Saving the original clipboard data to a local variable. 177 | const originalClipboardData = await env.clipboard.readText(); 178 | 179 | // 2. Populating the clipboard with an empty string 180 | await env.clipboard.writeText(""); 181 | 182 | // 3. Calling the copyPathOfActiveFile that populates the clipboard with the source path of the active file. 183 | // If there is no active file - the clipboard will not be populated and it will stay with the empty string. 184 | await commands.executeCommand("workbench.action.files.copyPathOfActiveFile"); 185 | 186 | // 4. Get the clipboard data after the API call 187 | const postAPICallClipboardData = await env.clipboard.readText(); 188 | 189 | // 5. Return the saved original clipboard data to the clipboard so this method 190 | // will not interfere with the clipboard's content. 191 | await env.clipboard.writeText(originalClipboardData); 192 | 193 | // 6. Return the clipboard data from the API call (which could be an empty string if it failed). 194 | return postAPICallClipboardData; 195 | } 196 | 197 | protected async getWorkspaceFolderPath(relativeToRoot?: boolean): Promise; 198 | protected async getWorkspaceFolderPath(): Promise { 199 | const workspaceFolder = await this.selectWorkspaceFolder(); 200 | return workspaceFolder?.uri.fsPath; 201 | } 202 | 203 | protected async selectWorkspaceFolder(): Promise { 204 | if (workspace.workspaceFolders && workspace.workspaceFolders.length === 1) { 205 | return workspace.workspaceFolders[0]; 206 | } 207 | 208 | const sourcePath = await this.getSourcePath({ ignoreIfNotExists: true }); 209 | const uri = Uri.file(sourcePath); 210 | return workspace.getWorkspaceFolder(uri) || window.showWorkspaceFolderPick(); 211 | } 212 | 213 | protected get isMultiRootWorkspace(): boolean { 214 | return workspace.workspaceFolders !== undefined && workspace.workspaceFolders.length > 1; 215 | } 216 | 217 | protected async getFileSourcePathAtRoot(rootPath: string, options: SourcePathOptions): Promise { 218 | const { relativeToRoot = false, typeahead } = options; 219 | let sourcePath = rootPath; 220 | 221 | if (typeahead) { 222 | const cache = this.getCache(`workspace:${sourcePath}`); 223 | const typeAheadController = new TypeAheadController(cache, relativeToRoot); 224 | sourcePath = await typeAheadController.showDialog(sourcePath); 225 | } 226 | 227 | if (!sourcePath) { 228 | throw new Error(); 229 | } 230 | 231 | return sourcePath; 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/controller/CopyFileNameController.ts: -------------------------------------------------------------------------------- 1 | import { env } from "vscode"; 2 | import { FileItem } from "../FileItem"; 3 | import { BaseFileController } from "./BaseFileController"; 4 | import { DialogOptions, ExecuteOptions } from "./FileController"; 5 | 6 | export class CopyFileNameController extends BaseFileController { 7 | public async showDialog(options: DialogOptions): Promise { 8 | const { uri } = options; 9 | const sourcePath = await this.getSourcePath({ uri }); 10 | 11 | if (!sourcePath) { 12 | throw new Error(); 13 | } 14 | return new FileItem(sourcePath); 15 | } 16 | 17 | public async execute(options: ExecuteOptions): Promise { 18 | await env.clipboard.writeText(options.fileItem.name); 19 | return options.fileItem; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/controller/DuplicateFileController.ts: -------------------------------------------------------------------------------- 1 | import { FileItem } from "../FileItem"; 2 | import { ExecuteOptions } from "./FileController"; 3 | import { MoveFileController } from "./MoveFileController"; 4 | 5 | export class DuplicateFileController extends MoveFileController { 6 | public async execute(options: ExecuteOptions): Promise { 7 | const { fileItem } = options; 8 | await this.ensureWritableFile(fileItem); 9 | return fileItem.duplicate(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/controller/FileController.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor, Uri } from "vscode"; 2 | import { FileItem } from "../FileItem"; 3 | 4 | export interface DialogOptions { 5 | prompt?: string; 6 | uri?: Uri; 7 | typeahead?: boolean; 8 | } 9 | 10 | export interface ExecuteOptions { 11 | fileItem: FileItem; 12 | } 13 | 14 | export interface SourcePathOptions { 15 | relativeToRoot?: boolean; 16 | ignoreIfNotExists?: boolean; 17 | uri?: Uri; 18 | typeahead?: boolean; 19 | } 20 | 21 | export interface FileController { 22 | showDialog(options?: DialogOptions): Promise; 23 | execute(options: ExecuteOptions): Promise; 24 | openFileInEditor(fileItem: FileItem): Promise; 25 | closeCurrentFileEditor(): Promise; 26 | getSourcePath(options?: SourcePathOptions): Promise; 27 | } 28 | -------------------------------------------------------------------------------- /src/controller/MoveFileController.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { FileType, Uri, workspace } from "vscode"; 3 | import { FileItem } from "../FileItem"; 4 | import { BaseFileController, TargetPathInputBoxValueOptions } from "./BaseFileController"; 5 | import { DialogOptions, ExecuteOptions } from "./FileController"; 6 | 7 | export class MoveFileController extends BaseFileController { 8 | public async showDialog(options: DialogOptions): Promise { 9 | const { uri } = options; 10 | const sourcePath = await this.getSourcePath({ uri }); 11 | 12 | if (!sourcePath) { 13 | throw new Error(); 14 | } 15 | 16 | const targetPath = await this.getTargetPath(sourcePath, options); 17 | 18 | if (targetPath) { 19 | const isDir = (await workspace.fs.stat(Uri.file(sourcePath))).type === FileType.Directory; 20 | return new FileItem(sourcePath, targetPath, isDir); 21 | } 22 | } 23 | 24 | public async execute(options: ExecuteOptions): Promise { 25 | const { fileItem } = options; 26 | await this.ensureWritableFile(fileItem); 27 | return fileItem.move(); 28 | } 29 | 30 | protected async getTargetPathInputBoxValue( 31 | sourcePath: string, 32 | options: TargetPathInputBoxValueOptions 33 | ): Promise { 34 | const value = await this.getFullTargetPathInputBoxValue(sourcePath, options); 35 | return super.getTargetPathInputBoxValue(value, options); 36 | } 37 | 38 | private async getFullTargetPathInputBoxValue( 39 | sourcePath: string, 40 | options: TargetPathInputBoxValueOptions 41 | ): Promise { 42 | const { typeahead, workspaceFolderPath } = options; 43 | 44 | if (!typeahead) { 45 | return sourcePath; 46 | } 47 | 48 | if (!workspaceFolderPath) { 49 | throw new Error(); 50 | } 51 | 52 | const rootPath = await this.getFileSourcePathAtRoot(workspaceFolderPath, { relativeToRoot: true, typeahead }); 53 | const fileName = path.basename(sourcePath); 54 | 55 | return path.join(rootPath, fileName); 56 | } 57 | 58 | protected getFilenameSelection(value: string): [number, number] { 59 | const basename = path.basename(value); 60 | const start = value.length - basename.length; 61 | const dot = basename.lastIndexOf("."); 62 | const exclusiveEndIndex = dot <= 0 ? value.length : start + dot; 63 | 64 | return [start, exclusiveEndIndex]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/controller/NewFileController.ts: -------------------------------------------------------------------------------- 1 | import expand from "brace-expansion"; 2 | import * as path from "path"; 3 | import { window } from "vscode"; 4 | import { FileItem } from "../FileItem"; 5 | import { BaseFileController, TargetPathInputBoxValueOptions } from "./BaseFileController"; 6 | import { DialogOptions, ExecuteOptions, SourcePathOptions } from "./FileController"; 7 | 8 | export interface NewFileDialogOptions extends Omit { 9 | relativeToRoot?: boolean; 10 | } 11 | 12 | export interface NewFileExecuteOptions extends ExecuteOptions { 13 | isDir?: boolean; 14 | } 15 | 16 | export class NewFileController extends BaseFileController { 17 | public async showDialog(options: NewFileDialogOptions): Promise { 18 | const { relativeToRoot = false, typeahead } = options; 19 | const sourcePath = await this.getNewFileSourcePath({ relativeToRoot, typeahead }); 20 | const targetPath = await this.getTargetPath(sourcePath, options); 21 | 22 | if (!targetPath) { 23 | return; 24 | } 25 | 26 | return expand(targetPath.replace(/\\/g, "/")).map((filePath) => { 27 | const realPath = path.resolve(sourcePath, filePath); 28 | const isDir = filePath.endsWith("/"); 29 | return new FileItem(sourcePath, realPath, isDir); 30 | }); 31 | } 32 | 33 | public async execute(options: NewFileExecuteOptions): Promise { 34 | const { fileItem, isDir = false } = options; 35 | await this.ensureWritableFile(fileItem); 36 | try { 37 | return fileItem.create(isDir); 38 | } catch { 39 | throw new Error(`Error creating file '${fileItem.path}'.`); 40 | } 41 | } 42 | 43 | protected async getTargetPathInputBoxValue( 44 | sourcePath: string, 45 | options: TargetPathInputBoxValueOptions 46 | ): Promise { 47 | const value = path.join(sourcePath, path.sep); 48 | return super.getTargetPathInputBoxValue(value, options); 49 | } 50 | 51 | public async getNewFileSourcePath({ relativeToRoot, typeahead }: SourcePathOptions): Promise { 52 | const rootPath = await this.getRootPath(relativeToRoot === true); 53 | 54 | if (!rootPath) { 55 | throw new Error(); 56 | } 57 | 58 | return this.getFileSourcePathAtRoot(rootPath, { relativeToRoot, typeahead }); 59 | } 60 | 61 | private async getRootPath(relativeToRoot: boolean): Promise { 62 | if (relativeToRoot) { 63 | return this.getWorkspaceFolderPath(relativeToRoot); 64 | } 65 | return path.dirname(await this.getSourcePath()); 66 | } 67 | 68 | protected async getWorkspaceFolderPath(relativeToRoot: boolean): Promise { 69 | const requiresWorkspaceFolderPick = relativeToRoot && this.isMultiRootWorkspace; 70 | if (requiresWorkspaceFolderPick) { 71 | const workspaceFolder = await window.showWorkspaceFolderPick(); 72 | return workspaceFolder?.uri.fsPath; 73 | } 74 | 75 | return super.getWorkspaceFolderPath(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/controller/RemoveFileController.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { window, workspace } from "vscode"; 3 | import { FileItem } from "../FileItem"; 4 | import { BaseFileController } from "./BaseFileController"; 5 | import { DialogOptions, ExecuteOptions } from "./FileController"; 6 | 7 | export class RemoveFileController extends BaseFileController { 8 | public async showDialog(options: DialogOptions): Promise { 9 | const { uri } = options; 10 | const sourcePath = await this.getSourcePath({ uri }); 11 | 12 | if (!sourcePath) { 13 | throw new Error(); 14 | } 15 | 16 | if (this.confirmDelete === false) { 17 | return new FileItem(sourcePath); 18 | } 19 | 20 | const message = `Are you sure you want to delete '${path.basename(sourcePath)}'?`; 21 | const action = "Move to Trash"; 22 | const remove = await window.showInformationMessage(message, { modal: true }, action); 23 | if (remove) { 24 | return new FileItem(sourcePath); 25 | } 26 | } 27 | 28 | public async execute(options: ExecuteOptions): Promise { 29 | const { fileItem } = options; 30 | try { 31 | await fileItem.remove(); 32 | } catch (e) { 33 | throw new Error(`Error deleting file '${fileItem.path}'.`); 34 | } 35 | return fileItem; 36 | } 37 | 38 | private get confirmDelete(): boolean { 39 | return workspace.getConfiguration("explorer", null).get("confirmDelete") === true; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/controller/RenameFileController.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { DialogOptions } from "./FileController"; 3 | import { MoveFileController } from "./MoveFileController"; 4 | 5 | export class RenameFileController extends MoveFileController { 6 | protected async getTargetPath(sourcePath: string, options: DialogOptions): Promise { 7 | const { prompt } = options; 8 | const value = path.basename(sourcePath); 9 | const targetPath = await this.showTargetPathInputBox({ prompt, value }); 10 | 11 | if (targetPath) { 12 | const basePath = path.dirname(sourcePath); 13 | return path.join(basePath, targetPath.replace(basePath, "")); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/controller/TypeAheadController.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { QuickPickItem, window } from "vscode"; 3 | import { Cache } from "../lib/Cache"; 4 | import { TreeWalker } from "../lib/TreeWalker"; 5 | 6 | async function waitForIOEvents(): Promise { 7 | return new Promise((resolve) => setImmediate(resolve)); 8 | } 9 | 10 | const ROOT_PATH = "/"; 11 | 12 | export class TypeAheadController { 13 | constructor(private cache: Cache, private relativeToRoot: boolean = false) {} 14 | 15 | public async showDialog(sourcePath: string): Promise { 16 | const items = await this.buildQuickPickItems(sourcePath); 17 | 18 | const item = items.length === 1 ? items[0] : await this.showQuickPick(items); 19 | 20 | if (!item) { 21 | throw new Error(); 22 | } 23 | 24 | const selection = item.label; 25 | this.cache.put("last", selection); 26 | 27 | return path.join(sourcePath, selection); 28 | } 29 | 30 | private async buildQuickPickItems(sourcePath: string): Promise { 31 | const lastEntry: string = this.cache.get("last"); 32 | const header = this.buildQuickPickItemsHeader(lastEntry); 33 | 34 | const directories = (await this.getDirectoriesAtSourcePath(sourcePath)) 35 | .filter((directory) => directory !== lastEntry && directory !== ROOT_PATH) 36 | .map((directory) => this.buildQuickPickItem(directory)); 37 | 38 | if (directories.length === 0 && header.length === 1) { 39 | return header; 40 | } 41 | 42 | return [...header, ...directories]; 43 | } 44 | 45 | private async getDirectoriesAtSourcePath(sourcePath: string): Promise { 46 | await waitForIOEvents(); 47 | const treeWalker = new TreeWalker(); 48 | return treeWalker.directories(sourcePath); 49 | } 50 | 51 | private buildQuickPickItemsHeader(lastEntry: string | undefined): QuickPickItem[] { 52 | const items = [ 53 | this.buildQuickPickItem(ROOT_PATH, `- ${this.relativeToRoot ? "workspace root" : "current file"}`), 54 | ]; 55 | 56 | if (lastEntry && lastEntry !== ROOT_PATH) { 57 | items.push(this.buildQuickPickItem(lastEntry, "- last selection")); 58 | } 59 | 60 | return items; 61 | } 62 | 63 | private buildQuickPickItem(label: string, description?: string | undefined): QuickPickItem { 64 | return { description, label }; 65 | } 66 | 67 | private async showQuickPick(items: readonly QuickPickItem[]) { 68 | const hint = "larger projects may take a moment to load"; 69 | const placeHolder = `First, select an existing path to create relative to (${hint})`; 70 | return window.showQuickPick(items, { placeHolder, ignoreFocusOut: true }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/controller/index.ts: -------------------------------------------------------------------------------- 1 | export { DuplicateFileController } from "./DuplicateFileController"; 2 | export { FileController } from "./FileController"; 3 | export { MoveFileController } from "./MoveFileController"; 4 | export { NewFileController } from "./NewFileController"; 5 | export { RemoveFileController } from "./RemoveFileController"; 6 | export { CopyFileNameController } from "./CopyFileNameController"; 7 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { 3 | Command, 4 | CopyFileNameCommand, 5 | DuplicateFileCommand, 6 | MoveFileCommand, 7 | NewFileCommand, 8 | NewFolderCommand, 9 | RemoveFileCommand, 10 | RenameFileCommand, 11 | } from "./command"; 12 | import { 13 | CopyFileNameController, 14 | DuplicateFileController, 15 | MoveFileController, 16 | NewFileController, 17 | RemoveFileController, 18 | } from "./controller"; 19 | import { RenameFileController } from "./controller/RenameFileController"; 20 | 21 | function handleError(err: Error) { 22 | if (err && err.message) { 23 | vscode.window.showErrorMessage(err.message); 24 | } 25 | return err; 26 | } 27 | 28 | function register(context: vscode.ExtensionContext, command: Command, commandName: string) { 29 | const proxy = (...args: never[]) => command.execute(...args).catch(handleError); 30 | const disposable = vscode.commands.registerCommand(`fileutils.${commandName}`, proxy); 31 | 32 | context.subscriptions.push(disposable); 33 | } 34 | 35 | export function activate(context: vscode.ExtensionContext): void { 36 | const copyFileNameController = new CopyFileNameController(context); 37 | const duplicateFileController = new DuplicateFileController(context); 38 | const moveFileController = new MoveFileController(context); 39 | const newFileController = new NewFileController(context); 40 | const removeFileController = new RemoveFileController(context); 41 | const renameFileController = new RenameFileController(context); 42 | 43 | register(context, new CopyFileNameCommand(copyFileNameController), "copyFileName"); 44 | register(context, new DuplicateFileCommand(duplicateFileController), "duplicateFile"); 45 | register(context, new MoveFileCommand(moveFileController), "moveFile"); 46 | register(context, new NewFileCommand(newFileController, { relativeToRoot: true }), "newFileAtRoot"); 47 | register(context, new NewFileCommand(newFileController), "newFile"); 48 | register(context, new NewFolderCommand(newFileController, { relativeToRoot: true }), "newFolderAtRoot"); 49 | register(context, new NewFolderCommand(newFileController), "newFolder"); 50 | register(context, new RemoveFileCommand(removeFileController), "removeFile"); 51 | register(context, new RenameFileCommand(renameFileController), "renameFile"); 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/Cache.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export class Cache { 4 | private cache: { [key: string]: unknown }; 5 | 6 | constructor(private storage: vscode.Memento, private namespace: string) { 7 | this.cache = storage.get(this.namespace, {}); 8 | } 9 | 10 | public put(key: string, value: unknown): void { 11 | this.cache[key] = value; 12 | this.storage.update(this.namespace, this.cache); 13 | } 14 | 15 | public get(key: string, defaultValue?: unknown): T { 16 | return (key in this.cache ? this.cache[key] : defaultValue) as T; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/TreeWalker.ts: -------------------------------------------------------------------------------- 1 | import glob from "fast-glob"; 2 | import * as path from "path"; 3 | import { workspace } from "vscode"; 4 | 5 | interface ExtendedProcess { 6 | noAsar: boolean; 7 | } 8 | 9 | export class TreeWalker { 10 | public async directories(sourcePath: string): Promise { 11 | try { 12 | this.ensureFailSafeFileLookup(); 13 | const files = await glob("**", { 14 | cwd: sourcePath, 15 | onlyDirectories: true, 16 | ignore: this.getExcludePatterns(), 17 | }); 18 | return files.map((file) => path.join(path.sep, file)).sort(); 19 | } catch (err) { 20 | const details = (err as Error).message; 21 | throw new Error(`Unable to list subdirectories for directory "${sourcePath}". Details: (${details})`); 22 | } 23 | } 24 | 25 | private getExcludePatterns(): string[] { 26 | const exclude = new Set([ 27 | ...Object.keys(workspace.getConfiguration("search.exclude")), 28 | ...Object.keys(workspace.getConfiguration("files.exclude")), 29 | ]); 30 | return Array.from(exclude); 31 | } 32 | 33 | private ensureFailSafeFileLookup() { 34 | (process as unknown as ExtendedProcess).noAsar = true; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/config.ts: -------------------------------------------------------------------------------- 1 | import { workspace } from "vscode"; 2 | 3 | export function getConfiguration(key: string): T | undefined { 4 | return workspace.getConfiguration("fileutils", null).get(key); 5 | } 6 | -------------------------------------------------------------------------------- /test/command/CopyFileNameCommand.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { env } from "vscode"; 3 | import { CopyFileNameCommand } from "../../src/command"; 4 | import { CopyFileNameController } from "../../src/controller"; 5 | import { FileItem } from "../../src/FileItem"; 6 | import * as helper from "../helper"; 7 | 8 | describe(CopyFileNameCommand.name, () => { 9 | const clipboardInitialTestData = "SOME_TEXT"; 10 | const subject = new CopyFileNameCommand(new CopyFileNameController(helper.createExtensionContext())); 11 | 12 | beforeEach(helper.beforeEach); 13 | 14 | afterEach(helper.afterEach); 15 | 16 | describe("as command", () => { 17 | afterEach(async () => { 18 | await env.clipboard.writeText(clipboardInitialTestData); 19 | }); 20 | 21 | describe("with open text document", () => { 22 | beforeEach(async () => helper.openDocument(helper.editorFile1)); 23 | 24 | afterEach(async () => helper.closeAllEditors()); 25 | 26 | it("should put the file name to the clipboard", async () => { 27 | await subject.execute(); 28 | const clipboardData = await env.clipboard.readText(); 29 | expect(clipboardData).to.equal(new FileItem(helper.editorFile1).name); 30 | }); 31 | }); 32 | 33 | describe("without an open text document", () => { 34 | beforeEach(async () => { 35 | await helper.closeAllEditors(); 36 | await env.clipboard.writeText(clipboardInitialTestData); 37 | }); 38 | 39 | it("should ignore the command call and not change the clipboard data", async () => { 40 | try { 41 | await subject.execute(); 42 | expect.fail("must fail"); 43 | } catch (e) { 44 | const clipboardData = await env.clipboard.readText(); 45 | expect(clipboardData).to.equal(clipboardInitialTestData); 46 | } 47 | }); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/command/DuplicateFileCommand.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import { Uri, window, workspace } from "vscode"; 5 | import { DuplicateFileCommand } from "../../src/command/DuplicateFileCommand"; 6 | import { DuplicateFileController } from "../../src/controller"; 7 | import * as helper from "../helper"; 8 | 9 | describe(DuplicateFileCommand.name, () => { 10 | const subject = new DuplicateFileCommand(new DuplicateFileController(helper.createExtensionContext())); 11 | 12 | beforeEach(async () => { 13 | await helper.beforeEach(); 14 | helper.createGetConfigurationStub({ "duplicateFile.typeahead.enabled": false, "inputBox.path": "root" }); 15 | }); 16 | 17 | afterEach(helper.afterEach); 18 | 19 | describe("as command", () => { 20 | describe("with open text document", () => { 21 | beforeEach(async () => { 22 | await helper.openDocument(helper.editorFile1); 23 | helper.createShowInputBoxStub().resolves(helper.targetFile.path); 24 | helper.createShowQuickPickStub().resolves({ label: "/", description: "" }); 25 | }); 26 | 27 | afterEach(async () => { 28 | await helper.closeAllEditors(); 29 | }); 30 | 31 | helper.protocol.it("should prompt for file destination", subject, "Duplicate As"); 32 | helper.protocol.it("should duplicate current file to destination", subject); 33 | helper.protocol.describe("with target file in non-existent nested directory", subject); 34 | helper.protocol.describe("when target destination exists", subject); 35 | helper.protocol.it("should open target file as active editor", subject); 36 | 37 | helper.protocol.describe("typeahead configuration", subject, { 38 | command: "duplicateFile", 39 | items: helper.quickPick.typeahead.items.workspace, 40 | }); 41 | 42 | helper.protocol.describe("inputBox configuration", subject, { 43 | editorFile: helper.editorFile1, 44 | }); 45 | }); 46 | 47 | helper.protocol.describe("without an open text document", subject); 48 | }); 49 | 50 | describe("as context menu", () => { 51 | describe("with selected file", () => { 52 | beforeEach(async () => helper.createShowInputBoxStub().resolves(helper.targetFile.path)); 53 | 54 | helper.protocol.it("should prompt for file destination", subject, "Duplicate As"); 55 | helper.protocol.it("should duplicate current file to destination", subject, helper.editorFile1); 56 | helper.protocol.it("should open target file as active editor", subject, helper.editorFile1); 57 | }); 58 | 59 | describe("with selected directory", () => { 60 | const sourceDirectory = Uri.file(path.resolve(helper.tmpDir.path, "duplicate-source-dir")); 61 | const targetDirectory = Uri.file(path.resolve(helper.tmpDir.path, "duplicate-target-dir")); 62 | 63 | beforeEach(async () => { 64 | await workspace.fs.createDirectory(sourceDirectory); 65 | helper.createShowInputBoxStub().resolves(targetDirectory.path); 66 | }); 67 | 68 | afterEach(async () => { 69 | await workspace.fs.delete(sourceDirectory, { recursive: true, useTrash: false }); 70 | await workspace.fs.delete(targetDirectory, { recursive: true, useTrash: false }); 71 | }); 72 | 73 | it("should prompt for file destination", async () => { 74 | await subject.execute(sourceDirectory); 75 | const value = sourceDirectory.path; 76 | const valueSelection = [value.length - (value.split(path.sep).pop() as string).length, value.length]; 77 | const prompt = "Duplicate As"; 78 | expect(window.showInputBox).to.have.been.calledWithExactly({ 79 | prompt, 80 | value, 81 | valueSelection, 82 | ignoreFocusOut: true, 83 | }); 84 | }); 85 | 86 | it("should duplicate current file to destination", async () => { 87 | await subject.execute(sourceDirectory); 88 | const message = `${targetDirectory} does not exist`; 89 | expect(fs.existsSync(targetDirectory.fsPath), message).to.be.true; 90 | }); 91 | 92 | it("should not open target file as active editor", async () => { 93 | await subject.execute(sourceDirectory); 94 | expect(window.activeTextEditor?.document?.fileName).not.to.equal(targetDirectory.path); 95 | }); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/command/MoveFileCommand.test.ts: -------------------------------------------------------------------------------- 1 | import { MoveFileCommand } from "../../src/command"; 2 | import { MoveFileController } from "../../src/controller"; 3 | import * as helper from "../helper"; 4 | 5 | describe(MoveFileCommand.name, () => { 6 | const subject = new MoveFileCommand(new MoveFileController(helper.createExtensionContext())); 7 | 8 | beforeEach(async () => { 9 | await helper.beforeEach(); 10 | helper.createGetConfigurationStub({ "moveFile.typeahead.enabled": false, "inputBox.path": "root" }); 11 | }); 12 | 13 | afterEach(helper.afterEach); 14 | 15 | describe("as command", () => { 16 | describe("with open text document", () => { 17 | beforeEach(async () => { 18 | await helper.openDocument(helper.editorFile1); 19 | helper.createShowInputBoxStub().resolves(helper.targetFile.path); 20 | helper.createShowQuickPickStub().resolves({ label: "/", description: "" }); 21 | }); 22 | 23 | afterEach(async () => { 24 | await helper.closeAllEditors(); 25 | }); 26 | 27 | helper.protocol.it("should prompt for file destination", subject, "New Location"); 28 | helper.protocol.it("should move current file to destination", subject); 29 | helper.protocol.describe("with target file in non-existent nested directory", subject); 30 | 31 | helper.protocol.describe("typeahead configuration", subject, { 32 | command: "moveFile", 33 | items: helper.quickPick.typeahead.items.workspace, 34 | }); 35 | 36 | helper.protocol.describe("inputBox configuration", subject, { 37 | editorFile: helper.editorFile1, 38 | }); 39 | }); 40 | 41 | helper.protocol.describe("without an open text document", subject); 42 | }); 43 | 44 | describe("as context menu", () => { 45 | beforeEach(async () => helper.createShowInputBoxStub().resolves(helper.targetFile.path)); 46 | 47 | helper.protocol.it("should prompt for file destination", subject, "New Location"); 48 | helper.protocol.it("should move current file to destination", subject, helper.editorFile1); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/command/NewFileCommand.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import { Uri, window, workspace } from "vscode"; 5 | import { NewFileCommand } from "../../src/command"; 6 | import { NewFileController } from "../../src/controller"; 7 | import * as helper from "../helper"; 8 | 9 | describe(NewFileCommand.name, () => { 10 | beforeEach(async () => { 11 | await helper.beforeEach(); 12 | helper.createGetConfigurationStub({ "newFile.typeahead.enabled": false, "inputBox.path": "root" }); 13 | }); 14 | 15 | afterEach(helper.afterEach); 16 | 17 | describe('when "relativeToRoot" is "false"', async () => { 18 | const subject = new NewFileCommand(new NewFileController(helper.createExtensionContext())); 19 | 20 | beforeEach(async () => { 21 | await helper.openDocument(helper.editorFile1); 22 | helper.createShowInputBoxStub().resolves(path.basename(helper.targetFile.path)); 23 | helper.createShowQuickPickStub().resolves({ label: "/", description: "" }); 24 | }); 25 | 26 | afterEach(async () => { 27 | await helper.closeAllEditors(); 28 | }); 29 | 30 | it("should prompt for file destination", async () => { 31 | await subject.execute(); 32 | const prompt = "File Name"; 33 | const value = path.join(path.dirname(helper.editorFile1.path), path.sep); 34 | const valueSelection = [value.length, value.length]; 35 | expect(window.showInputBox).to.have.been.calledWithExactly({ 36 | prompt, 37 | value, 38 | valueSelection, 39 | ignoreFocusOut: true, 40 | }); 41 | }); 42 | 43 | helper.protocol.describe("typeahead configuration", subject, { 44 | command: "newFile", 45 | items: helper.quickPick.typeahead.items.currentFile, 46 | }); 47 | 48 | helper.protocol.describe("inputBox configuration", subject, { 49 | editorFile: helper.editorFile1, 50 | expectedPath: "", 51 | }); 52 | 53 | it("should create the file at destination", async () => { 54 | await subject.execute(); 55 | const message = `${helper.targetFile.path} does not exist`; 56 | expect(fs.existsSync(helper.targetFile.fsPath), message).to.be.true; 57 | }); 58 | 59 | describe("file path ends with path separator", () => { 60 | beforeEach(async () => { 61 | const fileName = path.basename(helper.targetFile.fsPath) + path.sep; 62 | helper.createShowInputBoxStub().resolves(fileName); 63 | }); 64 | 65 | it("should create the directory at destination", async () => { 66 | await subject.execute(); 67 | const message = `${helper.targetFile.path} must be a directory`; 68 | expect(fs.statSync(helper.targetFile.fsPath).isDirectory(), message).to.be.true; 69 | }); 70 | }); 71 | 72 | describe("file path contains dot and backslash path separator", () => { 73 | beforeEach(async () => { 74 | const fileName = helper.targetFileWithDot.fsPath.replace(/\//g, "\\"); 75 | helper.createShowInputBoxStub().resolves(fileName); 76 | }); 77 | 78 | it("should create the file at destination", async () => { 79 | await subject.execute(); 80 | const message = `${helper.targetFileWithDot.path} does not exist`; 81 | expect(fs.existsSync(helper.targetFileWithDot.fsPath), message).to.be.true; 82 | }); 83 | }); 84 | 85 | helper.protocol.describe("with target file in non-existent nested directory", subject); 86 | helper.protocol.describe("when target destination exists", subject, { overwriteFileContent: "" }); 87 | helper.protocol.it("should open target file as active editor", subject); 88 | }); 89 | 90 | describe('when "relativeToRoot" is "true"', () => { 91 | const subject = new NewFileCommand(new NewFileController(helper.createExtensionContext()), { 92 | relativeToRoot: true, 93 | }); 94 | 95 | beforeEach(async () => { 96 | helper.createShowInputBoxStub().callsFake(async (options) => { 97 | if (options.value) { 98 | return path.join(options.value, "filename.txt"); 99 | } 100 | }); 101 | helper.createShowQuickPickStub().resolves({ label: "/", description: "" }); 102 | }); 103 | 104 | describe("with one workspace", () => { 105 | beforeEach(async () => { 106 | helper.createWorkspaceFoldersStub(helper.workspaceFolderA); 107 | helper.createGetWorkspaceFolderStub(); 108 | }); 109 | 110 | it("should select first workspace", async () => { 111 | await subject.execute(); 112 | expect(workspace.getWorkspaceFolder).to.have.not.been.called; 113 | 114 | const prompt = "File Name"; 115 | const value = path.join(helper.workspacePathA, path.sep); 116 | const valueSelection = [value.length, value.length]; 117 | expect(window.showInputBox).to.have.been.calledWithExactly({ 118 | prompt, 119 | value, 120 | valueSelection, 121 | ignoreFocusOut: true, 122 | }); 123 | }); 124 | 125 | helper.protocol.describe("typeahead configuration", subject, { 126 | command: "newFile", 127 | items: helper.quickPick.typeahead.items.workspace, 128 | }); 129 | }); 130 | 131 | describe("with multiple workspaces", () => { 132 | beforeEach(async () => { 133 | helper.createWorkspaceFoldersStub(helper.workspaceFolderA, helper.workspaceFolderB); 134 | helper.createStubObject(window, "showWorkspaceFolderPick").resolves(helper.workspaceFolderB); 135 | }); 136 | 137 | afterEach(async () => { 138 | helper.restoreObject(window.showWorkspaceFolderPick); 139 | }); 140 | 141 | it("should show workspace selector", async () => { 142 | await subject.execute(); 143 | expect(window.showWorkspaceFolderPick).to.have.been.called; 144 | 145 | const prompt = "File Name"; 146 | const value = path.join(helper.workspaceFolderB.uri.fsPath, path.sep); 147 | const valueSelection = [value.length, value.length]; 148 | expect(window.showInputBox).to.have.been.calledWithExactly({ 149 | prompt, 150 | value, 151 | valueSelection, 152 | ignoreFocusOut: true, 153 | }); 154 | }); 155 | 156 | describe("with open document", () => { 157 | beforeEach(async () => { 158 | helper.createGetWorkspaceFolderStub().returns(helper.workspaceFolderB); 159 | await helper.openDocument(helper.editorFile1); 160 | await subject.execute(); 161 | }); 162 | 163 | afterEach(async () => { 164 | await helper.closeAllEditors(); 165 | }); 166 | 167 | it("should show workspace selector", async () => { 168 | expect(window.showWorkspaceFolderPick).to.have.been.called; 169 | }); 170 | 171 | it("should select workspace for open file", async () => { 172 | expect(workspace.getWorkspaceFolder).to.have.been.calledWith(Uri.file(helper.editorFile1.fsPath)); 173 | }); 174 | }); 175 | 176 | helper.protocol.describe("typeahead configuration", subject, { 177 | command: "newFile", 178 | items: helper.quickPick.typeahead.items.workspace, 179 | }); 180 | }); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /test/command/RemoveFileCommand.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import { window } from "vscode"; 5 | import { RemoveFileCommand } from "../../src/command"; 6 | import { RemoveFileController } from "../../src/controller"; 7 | import * as helper from "../helper"; 8 | 9 | describe(RemoveFileCommand.name, () => { 10 | const subject = new RemoveFileCommand(new RemoveFileController(helper.createExtensionContext())); 11 | 12 | beforeEach(helper.beforeEach); 13 | 14 | afterEach(helper.afterEach); 15 | 16 | describe("as command", () => { 17 | describe("with open text document", () => { 18 | beforeEach(async () => { 19 | await helper.openDocument(helper.editorFile1); 20 | helper.createShowInformationMessageStub().resolves(helper.targetFile.path); 21 | helper.createGetConfigurationStub({}); 22 | }); 23 | 24 | afterEach(async () => { 25 | await helper.closeAllEditors(); 26 | }); 27 | 28 | describe("configuration", () => { 29 | describe('when "explorer.confirmDelete" is "true"', () => { 30 | beforeEach(async () => { 31 | helper.createGetConfigurationStub({ confirmDelete: true }); 32 | }); 33 | 34 | it("should show a confirmation dialog", async () => { 35 | await subject.execute(); 36 | const message = `Are you sure you want to delete '${path.basename(helper.editorFile1.path)}'?`; 37 | const action = "Move to Trash"; 38 | const options = { modal: true }; 39 | expect(window.showInformationMessage).to.have.been.calledWith(message, options, action); 40 | }); 41 | }); 42 | 43 | describe('when "explorer.confirmDelete" is "false"', () => { 44 | beforeEach(async () => { 45 | helper.createGetConfigurationStub({ confirmDelete: false }); 46 | }); 47 | 48 | it("should delete the file without confirmation", async () => { 49 | await subject.execute(); 50 | const message = `${helper.editorFile1.path} does not exist`; 51 | expect(window.showInformationMessage).to.have.not.been.called; 52 | expect(fs.existsSync(helper.editorFile1.fsPath), message).to.be.false; 53 | }); 54 | }); 55 | }); 56 | 57 | describe('when answered with "Move to Trash"', () => { 58 | it("should delete the file", async () => { 59 | await subject.execute(); 60 | const message = `${helper.editorFile1.path} does exist`; 61 | expect(fs.existsSync(helper.editorFile1.fsPath), message).to.be.false; 62 | }); 63 | }); 64 | 65 | describe('when answered with "Cancel"', () => { 66 | beforeEach(async () => { 67 | helper.createGetConfigurationStub({ confirmDelete: true }); 68 | helper.createShowInformationMessageStub().resolves(false); 69 | }); 70 | 71 | it("should leave the file untouched", async () => { 72 | try { 73 | await subject.execute(); 74 | expect.fail("Must fail"); 75 | } catch (e) { 76 | const message = `${helper.editorFile1.path} does not exist`; 77 | expect(fs.existsSync(helper.editorFile1.fsPath), message).to.be.true; 78 | } 79 | }); 80 | }); 81 | 82 | describe("prefer uri over current editor", () => { 83 | beforeEach(async () => { 84 | helper.createGetConfigurationStub({ confirmDelete: false }); 85 | }); 86 | 87 | it("should delete the file without confirmation", async () => { 88 | await subject.execute(helper.editorFile2); 89 | const message = `${helper.editorFile2.path} does not exist`; 90 | expect(fs.existsSync(helper.editorFile2.fsPath), message).to.be.false; 91 | }); 92 | }); 93 | }); 94 | 95 | describe("without an open text document", () => { 96 | beforeEach(async () => { 97 | await helper.closeAllEditors(); 98 | helper.createShowInformationMessageStub(); 99 | }); 100 | 101 | it("should ignore the command call", async () => { 102 | try { 103 | await subject.execute(); 104 | expect.fail("Must fail"); 105 | } catch { 106 | expect(window.showInformationMessage).to.have.not.been.called; 107 | } 108 | }); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /test/command/RenameFileCommand.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import * as path from "path"; 3 | import { Uri, window } from "vscode"; 4 | import { RenameFileCommand } from "../../src/command"; 5 | import { RenameFileController } from "../../src/controller/RenameFileController"; 6 | import * as helper from "../helper"; 7 | 8 | describe(RenameFileCommand.name, () => { 9 | const subject = new RenameFileCommand(new RenameFileController(helper.createExtensionContext())); 10 | 11 | beforeEach(async () => { 12 | await helper.beforeEach(); 13 | }); 14 | 15 | afterEach(helper.afterEach); 16 | 17 | describe("as command", () => { 18 | describe("with open text document", () => { 19 | beforeEach(async () => { 20 | await helper.openDocument(helper.editorFile1); 21 | helper.createShowInputBoxStub().resolves(helper.targetFile.path); 22 | }); 23 | 24 | afterEach(async () => { 25 | await helper.closeAllEditors(); 26 | }); 27 | 28 | it("should prompt for file destination", async () => { 29 | await subject.execute(); 30 | const prompt = "New Name"; 31 | const value = path.basename(helper.editorFile1.fsPath); 32 | const valueSelection = [value.length - 9, value.length - 3]; 33 | expect(window.showInputBox).to.have.been.calledWithExactly({ 34 | prompt, 35 | value, 36 | valueSelection, 37 | ignoreFocusOut: true, 38 | }); 39 | }); 40 | 41 | helper.protocol.it("should move current file to destination", subject); 42 | helper.protocol.describe("with target file in non-existent nested directory", subject); 43 | helper.protocol.it("should open target file as active editor", subject); 44 | 45 | describe("prefer uri over current editor", () => { 46 | beforeEach(async () => { 47 | const targetFile = Uri.file(path.resolve(`${helper.editorFile2.fsPath}.tmp`)); 48 | helper.createShowInputBoxStub().resolves(targetFile.path); 49 | }); 50 | 51 | it("should prompt for file destination", async () => { 52 | await subject.execute(helper.editorFile2); 53 | const prompt = "New Name"; 54 | const value = path.basename(helper.editorFile2.fsPath); 55 | const valueSelection = [value.length - 9, value.length - 3]; 56 | expect(window.showInputBox).to.have.been.calledWithExactly({ 57 | prompt, 58 | value, 59 | valueSelection, 60 | ignoreFocusOut: true, 61 | }); 62 | }); 63 | }); 64 | }); 65 | 66 | describe("without an open text document", () => { 67 | beforeEach(async () => { 68 | await helper.closeAllEditors(); 69 | helper.createShowInputBoxStub(); 70 | }); 71 | 72 | it("should ignore the command call", async () => { 73 | try { 74 | await subject.execute(); 75 | expect.fail("Must fail"); 76 | } catch { 77 | expect(window.showInputBox).to.have.not.been.called; 78 | } 79 | }); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/fixtures/file-1.rb: -------------------------------------------------------------------------------- 1 | class FileOne; end -------------------------------------------------------------------------------- /test/fixtures/file-2.rb: -------------------------------------------------------------------------------- 1 | class FileTwo; end -------------------------------------------------------------------------------- /test/helper/callbacks.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import path from "path"; 3 | import { Uri, workspace } from "vscode"; 4 | import { 5 | editorFile1, 6 | editorFile2, 7 | fixtureFile1, 8 | fixtureFile2, 9 | tmpDir, 10 | workspaceFolderA, 11 | workspaceFolderB, 12 | } from "./environment"; 13 | import { 14 | restoreExecuteCommand, 15 | restoreGetConfiguration, 16 | restoreGetWorkspaceFolder, 17 | restoreShowInformationMessage, 18 | restoreShowInputBox, 19 | restoreShowQuickPick, 20 | restoreShowWorkspaceFolderPick, 21 | restoreWorkspaceFolders, 22 | } from "./stubs"; 23 | 24 | export async function beforeEach(): Promise { 25 | if (existsSync(tmpDir.fsPath)) { 26 | await workspace.fs.delete(tmpDir, { recursive: true, useTrash: false }); 27 | } 28 | await workspace.fs.copy(fixtureFile1, editorFile1, { overwrite: true }); 29 | await workspace.fs.copy(fixtureFile2, editorFile2, { overwrite: true }); 30 | 31 | await workspace.fs.createDirectory(Uri.file(path.resolve(tmpDir.fsPath, "dir-1"))); 32 | await workspace.fs.createDirectory(Uri.file(path.resolve(tmpDir.fsPath, "dir-2"))); 33 | 34 | await workspace.fs.createDirectory(workspaceFolderA.uri); 35 | await workspace.fs.createDirectory(workspaceFolderB.uri); 36 | 37 | await workspace.fs.createDirectory(Uri.file(path.resolve(workspaceFolderA.uri.fsPath, "dir-1"))); 38 | await workspace.fs.createDirectory(Uri.file(path.resolve(workspaceFolderA.uri.fsPath, "dir-2"))); 39 | 40 | await workspace.fs.createDirectory(Uri.file(path.resolve(workspaceFolderB.uri.fsPath, "dir-1"))); 41 | await workspace.fs.createDirectory(Uri.file(path.resolve(workspaceFolderB.uri.fsPath, "dir-2"))); 42 | } 43 | 44 | export async function afterEach(): Promise { 45 | if (existsSync(tmpDir.fsPath)) { 46 | await workspace.fs.delete(tmpDir, { recursive: true, useTrash: false }); 47 | } 48 | restoreExecuteCommand(); 49 | restoreGetConfiguration(); 50 | restoreGetWorkspaceFolder(); 51 | restoreShowInformationMessage(); 52 | restoreShowInputBox(); 53 | restoreShowQuickPick(); 54 | restoreShowWorkspaceFolderPick(); 55 | restoreWorkspaceFolders(); 56 | } 57 | -------------------------------------------------------------------------------- /test/helper/environment.ts: -------------------------------------------------------------------------------- 1 | import * as os from "os"; 2 | import * as path from "path"; 3 | import { Uri, WorkspaceFolder } from "vscode"; 4 | 5 | export const rootDir = path.resolve(__dirname, "..", "..", ".."); 6 | export const tmpDir = Uri.file(path.resolve(os.tmpdir(), "vscode-fileutils-test")); 7 | 8 | export const fixtureFile1 = Uri.file(path.resolve(rootDir, "test", "fixtures", "file-1.rb")); 9 | export const fixtureFile2 = Uri.file(path.resolve(rootDir, "test", "fixtures", "file-2.rb")); 10 | 11 | export const editorFile1 = Uri.file(path.resolve(tmpDir.fsPath, "file-1.rb")); 12 | export const editorFile2 = Uri.file(path.resolve(tmpDir.fsPath, "file-2.rb")); 13 | 14 | export const targetFile = Uri.file(path.resolve(`${editorFile1.fsPath}.tmp`)); 15 | export const targetFileWithDot = Uri.file(path.resolve(tmpDir.fsPath, ".eslintrc.json")); 16 | 17 | export const workspacePathA = path.join(tmpDir.fsPath, "workspaceA"); 18 | export const workspacePathB = path.join(tmpDir.fsPath, "workspaceB"); 19 | 20 | export const workspaceFolderA: WorkspaceFolder = { uri: Uri.file(workspacePathA), name: "a", index: 0 }; 21 | export const workspaceFolderB: WorkspaceFolder = { uri: Uri.file(workspacePathB), name: "b", index: 1 }; 22 | -------------------------------------------------------------------------------- /test/helper/functions.ts: -------------------------------------------------------------------------------- 1 | import retry from "bluebird-retry"; 2 | import { TextDecoder } from "util"; 3 | import { commands, ExtensionContext, Uri, window, workspace } from "vscode"; 4 | 5 | const textDecoder = new TextDecoder("utf-8"); 6 | 7 | export async function readFile(file: Uri): Promise { 8 | return textDecoder.decode(await workspace.fs.readFile(file)); 9 | } 10 | 11 | export function createExtensionContext(): ExtensionContext { 12 | const context = { 13 | globalState: { 14 | get() { 15 | return {}; 16 | }, 17 | async update(): Promise { 18 | return; 19 | }, 20 | }, 21 | }; 22 | return context as unknown as ExtensionContext; 23 | } 24 | 25 | export async function openDocument(document: Uri): Promise { 26 | const tryOpenDocument = async () => { 27 | const textDocument = await workspace.openTextDocument(document); 28 | await window.showTextDocument(textDocument); 29 | }; 30 | await retry(() => tryOpenDocument(), { max_tries: 4, interval: 500 }); 31 | } 32 | 33 | export async function closeAllEditors(): Promise { 34 | await commands.executeCommand("workbench.action.closeAllEditors"); 35 | } 36 | -------------------------------------------------------------------------------- /test/helper/index.ts: -------------------------------------------------------------------------------- 1 | import { use } from "chai"; 2 | import * as mocha from "mocha"; 3 | import sinonChai from "sinon-chai"; 4 | import { Command } from "../../src/command"; 5 | import { steps } from "./steps"; 6 | import { Rest } from "./steps/types"; 7 | 8 | export * from "./callbacks"; 9 | export * from "./environment"; 10 | export * from "./functions"; 11 | export * from "./stubs"; 12 | 13 | use(sinonChai); 14 | 15 | export const protocol = { 16 | describe(name: string, subject: Command, ...rest: Rest): mocha.Suite { 17 | const step = steps.describe[name](subject, ...rest); 18 | return mocha.describe(name, step); 19 | }, 20 | it(name: string, subject: Command, ...rest: Rest): mocha.Test { 21 | const step = steps.it[name](subject, ...rest); 22 | return mocha.it(name, step); 23 | }, 24 | }; 25 | 26 | export const quickPick = { 27 | typeahead: { 28 | items: { 29 | workspace: [ 30 | { description: "- workspace root", label: "/" }, 31 | { description: undefined, label: "/dir-1" }, 32 | { description: undefined, label: "/dir-2" }, 33 | ], 34 | currentFile: [ 35 | { description: "- current file", label: "/" }, 36 | { description: undefined, label: "/dir-1" }, 37 | { description: undefined, label: "/dir-2" }, 38 | { description: undefined, label: "/workspaceA" }, 39 | { description: undefined, label: "/workspaceA/dir-1" }, 40 | { description: undefined, label: "/workspaceA/dir-2" }, 41 | { description: undefined, label: "/workspaceB" }, 42 | { description: undefined, label: "/workspaceB/dir-1" }, 43 | { description: undefined, label: "/workspaceB/dir-2" }, 44 | ], 45 | }, 46 | options: { 47 | placeHolder: 48 | "First, select an existing path to create relative to (larger projects may take a moment to load)", 49 | ignoreFocusOut: true, 50 | }, 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /test/helper/steps/describe.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import * as mocha from "mocha"; 3 | import * as path from "path"; 4 | import sinon from "sinon"; 5 | import { QuickPickItem, Uri, window, workspace } from "vscode"; 6 | import { quickPick } from ".."; 7 | import { Command } from "../../../src/command"; 8 | import { editorFile2, targetFile, tmpDir, workspaceFolderA } from "../environment"; 9 | import { closeAllEditors, readFile } from "../functions"; 10 | import { 11 | createGetConfigurationStub, 12 | createGetWorkspaceFolderStub, 13 | createShowInformationMessageStub, 14 | createShowInputBoxStub, 15 | createWorkspaceFoldersStub, 16 | } from "../stubs"; 17 | import { FuncVoid, Step } from "./types"; 18 | 19 | export const describe: Step = { 20 | "with target file in non-existent nested directory"(subject: Command): FuncVoid { 21 | return () => { 22 | const targetDir = path.resolve(tmpDir.fsPath, "level-1", "level-2", "level-3"); 23 | 24 | mocha.beforeEach(async () => createShowInputBoxStub().resolves(path.resolve(targetDir, "file.rb"))); 25 | 26 | mocha.it("should create nested directories", async () => { 27 | await subject.execute(); 28 | const textEditor = window.activeTextEditor; 29 | expect(textEditor); 30 | 31 | const dirname = path.dirname(textEditor?.document.fileName ?? ""); 32 | const directories: string[] = dirname.split(path.sep); 33 | 34 | expect(directories.pop()).to.equal("level-3"); 35 | expect(directories.pop()).to.equal("level-2"); 36 | expect(directories.pop()).to.equal("level-1"); 37 | }); 38 | }; 39 | }, 40 | "when target destination exists"(subject: Command, config?: Record): FuncVoid { 41 | return () => { 42 | mocha.beforeEach(async () => { 43 | await workspace.fs.copy(editorFile2, targetFile, { overwrite: true }); 44 | createShowInformationMessageStub().resolves({ title: "placeholder" }); 45 | }); 46 | 47 | mocha.it("should prompt with confirmation dialog to overwrite destination file", async () => { 48 | await subject.execute(); 49 | const message = `File '${targetFile.path}' already exists.`; 50 | const action = "Overwrite"; 51 | const options = { modal: true }; 52 | expect(window.showInformationMessage).to.have.been.calledWith(message, options, action); 53 | }); 54 | 55 | mocha.describe('when answered with "Overwrite"', () => { 56 | mocha.it("should overwrite the existig file", async () => { 57 | await subject.execute(); 58 | const fileContent = await readFile(targetFile); 59 | const expectedFileContent = 60 | config && "overwriteFileContent" in config ? config.overwriteFileContent : "class FileOne; end"; 61 | expect(fileContent).to.equal(expectedFileContent); 62 | }); 63 | }); 64 | 65 | mocha.describe('when answered with "Cancel"', () => { 66 | mocha.beforeEach(async () => createShowInformationMessageStub().resolves(false)); 67 | 68 | mocha.it("should leave existing file untouched", async () => { 69 | try { 70 | await subject.execute(); 71 | expect.fail("must fail"); 72 | } catch (e) { 73 | const fileContent = await readFile(targetFile); 74 | expect(fileContent).to.equal("class FileTwo; end"); 75 | } 76 | }); 77 | }); 78 | }; 79 | }, 80 | "without an open text document"(subject: Command): FuncVoid { 81 | return () => { 82 | mocha.beforeEach(async () => { 83 | await closeAllEditors(); 84 | createShowInputBoxStub(); 85 | }); 86 | 87 | mocha.it("should ignore the command call", async () => { 88 | try { 89 | await subject.execute(); 90 | expect.fail("must fail"); 91 | } catch { 92 | expect(window.showInputBox).to.have.not.been.called; 93 | } 94 | }); 95 | }; 96 | }, 97 | "typeahead configuration"(subject: Command, options: { command: string; items: QuickPickItem[] }): FuncVoid { 98 | const { command, items } = options; 99 | return () => { 100 | mocha.describe(`when "${command}.typeahead.enabled" is "true"`, () => { 101 | mocha.beforeEach(async () => { 102 | createGetConfigurationStub({ [`${command}.typeahead.enabled`]: true }); 103 | createWorkspaceFoldersStub(workspaceFolderA); 104 | }); 105 | 106 | mocha.it("should show the quick pick dialog", async () => { 107 | await subject.execute(); 108 | expect(window.showQuickPick).to.have.been.calledOnceWith( 109 | sinon.match(items), 110 | sinon.match(quickPick.typeahead.options) 111 | ); 112 | }); 113 | }); 114 | 115 | mocha.describe(`when "${command}.typeahead.enabled" is "false"`, () => { 116 | mocha.beforeEach(async () => { 117 | createGetConfigurationStub({ [`${command}.typeahead.enabled`]: false }); 118 | }); 119 | 120 | mocha.it("should not show the quick pick dialog", async () => { 121 | await subject.execute(); 122 | expect(window.showQuickPick).to.have.not.been.called; 123 | }); 124 | }); 125 | }; 126 | }, 127 | "inputBox configuration"(subject: Command, options: { editorFile: Uri; expectedPath?: string }): FuncVoid { 128 | const { editorFile, expectedPath } = options; 129 | const runs = [ 130 | { pathType: "workspace", pathTypeIndicator: "@" }, 131 | { pathType: "workspace", pathTypeIndicator: "" }, 132 | { pathType: "workspace", pathTypeIndicator: ":" }, 133 | { pathType: "workspace", pathTypeIndicator: " " }, 134 | ]; 135 | 136 | return () => { 137 | runs.forEach(({ pathType, pathTypeIndicator }) => { 138 | mocha.describe( 139 | `when "inputBox.pathType" is "${pathType}" and "inputBox.pathTypeIndicator" is "${pathTypeIndicator}"`, 140 | () => { 141 | mocha.beforeEach(async () => { 142 | createGetConfigurationStub({ 143 | "inputBox.pathType": pathType, 144 | "inputBox.pathTypeIndicator": pathTypeIndicator, 145 | }); 146 | 147 | const workspaceFolder = path.dirname(editorFile.path); 148 | createWorkspaceFoldersStub({ 149 | uri: Uri.file(workspaceFolder), 150 | name: "workspace-a", 151 | index: 0, 152 | }); 153 | createGetWorkspaceFolderStub().returns(workspaceFolder); 154 | }); 155 | 156 | mocha.it("should show the quick pick dialog", async () => { 157 | await subject.execute(); 158 | 159 | const expectedValue = `${pathTypeIndicator}/${ 160 | expectedPath ?? path.basename(editorFile.path) 161 | }`; 162 | 163 | expect(window.showInputBox).to.have.been.calledOnceWith({ 164 | prompt: sinon.match.string, 165 | value: sinon.match(expectedValue), 166 | valueSelection: sinon.match.array, 167 | ignoreFocusOut: true, 168 | }); 169 | }); 170 | } 171 | ); 172 | }); 173 | }; 174 | }, 175 | }; 176 | -------------------------------------------------------------------------------- /test/helper/steps/index.ts: -------------------------------------------------------------------------------- 1 | import { describe } from "./describe"; 2 | import { it } from "./it"; 3 | import { Step } from "./types"; 4 | 5 | export const steps: Record = { 6 | describe, 7 | it, 8 | }; 9 | -------------------------------------------------------------------------------- /test/helper/steps/it.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import * as fs from "fs"; 3 | import { Uri, window } from "vscode"; 4 | import { Command } from "../../../src/command"; 5 | import { editorFile1, targetFile } from "../environment"; 6 | import { FuncVoid, Step } from "./types"; 7 | 8 | export const it: Step = { 9 | "should open target file as active editor"(subject: Command, uri?: Uri): FuncVoid { 10 | return async () => { 11 | await subject.execute(uri); 12 | expect(window.activeTextEditor?.document.fileName).to.equal(targetFile.path); 13 | }; 14 | }, 15 | "should move current file to destination"(subject: Command, uri?: Uri): FuncVoid { 16 | return async () => { 17 | await subject.execute(uri); 18 | const message = `${targetFile} does not exist`; 19 | expect(fs.existsSync(targetFile.fsPath), message).to.be.true; 20 | }; 21 | }, 22 | "should prompt for file destination"(subject: Command, prompt: string): FuncVoid { 23 | return async () => { 24 | await subject.execute(editorFile1); 25 | const value = editorFile1.path; 26 | const valueSelection = [value.length - 9, value.length - 3]; 27 | expect(window.showInputBox).to.have.been.calledWithExactly({ 28 | prompt, 29 | value, 30 | valueSelection, 31 | ignoreFocusOut: true, 32 | }); 33 | }; 34 | }, 35 | }; 36 | 37 | it["should duplicate current file to destination"] = it["should move current file to destination"]; 38 | -------------------------------------------------------------------------------- /test/helper/steps/types.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../../src/command"; 2 | 3 | export type FuncVoid = () => void; 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | export type Rest = any; 6 | export interface Step { 7 | [key: string]: (subject: Command, ...rest: Rest) => FuncVoid; 8 | } 9 | -------------------------------------------------------------------------------- /test/helper/stubs.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from "sinon"; 2 | import { commands, window, workspace, WorkspaceFolder } from "vscode"; 3 | 4 | export function createGetWorkspaceFolderStub(): sinon.SinonStub { 5 | return createStubObject(workspace, "getWorkspaceFolder"); 6 | } 7 | 8 | export function restoreGetWorkspaceFolder(): void { 9 | restoreObject(workspace.getWorkspaceFolder); 10 | } 11 | 12 | export function createWorkspaceFoldersStub(...workspaceFolders: WorkspaceFolder[]): sinon.SinonStub { 13 | return createStubObject(workspace, "workspaceFolders").get(() => workspaceFolders); 14 | } 15 | 16 | export function restoreWorkspaceFolders(): void { 17 | restoreObject(workspace.workspaceFolders); 18 | } 19 | 20 | export function createExecuteCommandStub(): sinon.SinonStub { 21 | return createStubObject(commands, "executeCommand"); 22 | } 23 | 24 | export function restoreExecuteCommand(): void { 25 | restoreObject(commands.executeCommand); 26 | } 27 | 28 | export function createGetConfigurationStub(keys: Record): sinon.SinonStub { 29 | const config = { get: (key: string) => keys[key] }; 30 | return createStubObject(workspace, "getConfiguration").returns(config); 31 | } 32 | 33 | export function restoreGetConfiguration(): void { 34 | restoreObject(workspace.getConfiguration); 35 | } 36 | 37 | export function createShowInputBoxStub(): sinon.SinonStub { 38 | return createStubObject(window, "showInputBox"); 39 | } 40 | 41 | export function restoreShowInputBox(): void { 42 | restoreObject(window.showInputBox); 43 | } 44 | 45 | export function createShowQuickPickStub(): sinon.SinonStub { 46 | return createStubObject(window, "showQuickPick"); 47 | } 48 | 49 | export function restoreShowQuickPick(): void { 50 | restoreObject(window.showQuickPick); 51 | } 52 | 53 | export function createShowWorkspaceFolderPickStub(): sinon.SinonStub { 54 | return createStubObject(window, "showWorkspaceFolderPick"); 55 | } 56 | 57 | export function restoreShowWorkspaceFolderPick(): void { 58 | restoreObject(window.showWorkspaceFolderPick); 59 | } 60 | 61 | export function createShowInformationMessageStub(): sinon.SinonStub { 62 | return createStubObject(window, "showInformationMessage"); 63 | } 64 | 65 | export function restoreShowInformationMessage(): void { 66 | restoreObject(window.showInformationMessage); 67 | } 68 | 69 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 70 | type Handler = any; 71 | 72 | export function createStubObject(handler: Handler, functionName: string): sinon.SinonStub { 73 | const target: sinon.SinonStub | undefined = handler[functionName]; 74 | const stub: sinon.SinonStub = target && "restore" in target ? target : sinon.stub(handler, functionName); 75 | 76 | return stub; 77 | } 78 | 79 | export function restoreObject(object: unknown): void { 80 | const stub = object as sinon.SinonStub; 81 | if (stub && stub.restore) { 82 | stub.restore(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import Mocha from "mocha"; 2 | import * as path from "path"; 3 | import glob from "fast-glob"; 4 | 5 | export async function run(): Promise { 6 | const mocha = new Mocha({ 7 | reporter: "list", 8 | ui: "bdd", 9 | color: true, 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, ".."); 13 | const files = await glob("**/**.test.js", { cwd: testsRoot }); 14 | 15 | console.log("Number of test files to run:", files.length); 16 | // Add files to the test suite 17 | files.forEach((file) => mocha.addFile(path.resolve(testsRoot, file))); 18 | 19 | // Run the mocha test 20 | return new Promise((resolve, reject) => { 21 | try { 22 | mocha.run((failures: number) => { 23 | if (failures > 0) { 24 | reject(new Error(`${failures} tests failed.`)); 25 | } else { 26 | resolve(); 27 | } 28 | }); 29 | } catch (err) { 30 | reject(err); 31 | } 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | import { runTests } from "@vscode/test-electron"; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, "../../"); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, "./index"); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ 17 | extensionDevelopmentPath, 18 | extensionTestsPath, 19 | launchArgs: ["--disable-extensions"], 20 | }); 21 | } catch (err) { 22 | // tslint:disable-next-line: no-console 23 | console.error("Failed to run tests"); 24 | process.exit(1); 25 | } 26 | } 27 | 28 | main(); 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "out", 6 | 7 | "sourceMap": true, 8 | 9 | "removeComments": true, 10 | "resolveJsonModule": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "allowSyntheticDefaultImports": true 14 | }, 15 | "exclude": [ 16 | "node_modules", 17 | ".vscode-test" 18 | ] 19 | } 20 | --------------------------------------------------------------------------------