├── .eslintrc.js ├── .github ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yml │ ├── pull_request.yml │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.OLD.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── build └── Dockerfile ├── extension.ts ├── extensionBase.ts ├── extensionWeb.ts ├── gulpfile.js ├── images ├── design │ ├── vscodevim-logo.png │ └── vscodevim-logo.svg └── icon.png ├── language-configuration.json ├── package.json ├── renovate.json ├── src ├── actions │ ├── base.ts │ ├── baseMotion.ts │ ├── commands │ │ ├── actions.ts │ │ ├── commandLine.ts │ │ ├── digraphs.ts │ │ ├── documentChange.ts │ │ ├── fold.ts │ │ ├── insert.ts │ │ ├── join.ts │ │ ├── put.ts │ │ ├── replace.ts │ │ ├── scroll.ts │ │ ├── search.ts │ │ └── window.ts │ ├── include-main.ts │ ├── include-plugins.ts │ ├── languages │ │ └── python │ │ │ └── motion.ts │ ├── motion.ts │ ├── operator.ts │ ├── plugins │ │ ├── camelCaseMotion.ts │ │ ├── easymotion │ │ │ ├── easymotion.cmd.ts │ │ │ ├── easymotion.ts │ │ │ ├── markerGenerator.ts │ │ │ ├── registerMoveActions.ts │ │ │ └── types.ts │ │ ├── imswitcher.ts │ │ ├── pluginDefaultMappings.ts │ │ ├── replaceWithRegister.ts │ │ ├── sneak.ts │ │ ├── surround.ts │ │ └── targets │ │ │ ├── lastNextObjectHelper.ts │ │ │ ├── lastNextObjects.ts │ │ │ ├── searchUtils.ts │ │ │ ├── smartQuotes.ts │ │ │ ├── smartQuotesMatcher.ts │ │ │ ├── targets.ts │ │ │ └── targetsConfig.ts │ ├── types.d.ts │ └── wrapping.ts ├── cmd_line │ ├── commandLine.ts │ └── commands │ │ ├── ascii.ts │ │ ├── bang.ts │ │ ├── breakpoints.ts │ │ ├── bufferDelete.ts │ │ ├── close.ts │ │ ├── copy.ts │ │ ├── delete.ts │ │ ├── digraph.ts │ │ ├── echo.ts │ │ ├── eval.ts │ │ ├── explore.ts │ │ ├── file.ts │ │ ├── fileInfo.ts │ │ ├── goto.ts │ │ ├── gotoLine.ts │ │ ├── grep.ts │ │ ├── history.ts │ │ ├── jumps.ts │ │ ├── leftRightCenter.ts │ │ ├── let.ts │ │ ├── marks.ts │ │ ├── move.ts │ │ ├── nohl.ts │ │ ├── normal.ts │ │ ├── only.ts │ │ ├── print.ts │ │ ├── put.ts │ │ ├── pwd.ts │ │ ├── quit.ts │ │ ├── read.ts │ │ ├── redo.ts │ │ ├── register.ts │ │ ├── retab.ts │ │ ├── set.ts │ │ ├── sh.ts │ │ ├── shift.ts │ │ ├── smile.ts │ │ ├── sort.ts │ │ ├── substitute.ts │ │ ├── tab.ts │ │ ├── terminal.ts │ │ ├── undo.ts │ │ ├── vscode.ts │ │ ├── wall.ts │ │ ├── write.ts │ │ ├── writequit.ts │ │ ├── writequitall.ts │ │ └── yank.ts ├── common │ ├── matching │ │ ├── matcher.ts │ │ ├── quoteMatcher.ts │ │ └── tagMatcher.ts │ ├── motion │ │ ├── cursor.ts │ │ └── position.ts │ └── number │ │ └── numericString.ts ├── completion │ └── lineCompletionProvider.ts ├── configuration │ ├── configuration.ts │ ├── configurationValidator.ts │ ├── decoration.ts │ ├── iconfiguration.ts │ ├── iconfigurationValidator.ts │ ├── langmap.ts │ ├── notation.ts │ ├── remapper.ts │ ├── validators │ │ ├── inputMethodSwitcherValidator.ts │ │ ├── neovimValidator.ts │ │ ├── remappingValidator.ts │ │ └── vimrcValidator.ts │ ├── vimrc.ts │ └── vimrcKeyRemappingBuilder.ts ├── error.ts ├── globals.ts ├── history │ ├── historyFile.ts │ └── historyTracker.ts ├── jumps │ ├── jump.ts │ └── jumpTracker.ts ├── mode │ ├── internalSelectionsTracker.ts │ ├── mode.ts │ ├── modeData.ts │ ├── modeHandler.ts │ └── modeHandlerMap.ts ├── neovim │ └── neovim.ts ├── platform │ ├── browser │ │ ├── constants.ts │ │ ├── fs.ts │ │ └── history.ts │ └── node │ │ ├── constants.ts │ │ ├── fs.ts │ │ └── history.ts ├── register │ └── register.ts ├── state │ ├── compositionState.ts │ ├── globalState.ts │ ├── recordedState.ts │ ├── remapState.ts │ ├── replaceState.ts │ ├── searchState.ts │ ├── substituteState.ts │ └── vimState.ts ├── statusBar.ts ├── taskQueue.ts ├── textEditor.ts ├── textobject │ ├── paragraph.ts │ ├── sentence.ts │ ├── textobject.ts │ ├── util.ts │ └── word.ts ├── transformations │ ├── execute.ts │ ├── transformations.ts │ └── transformer.ts ├── util │ ├── child_process.ts │ ├── clipboard.ts │ ├── decorationUtils.ts │ ├── externalCommand.ts │ ├── logger.ts │ ├── os.ts │ ├── path.ts │ ├── selections.ts │ ├── specialKeys.ts │ ├── statusBarTextUtils.ts │ ├── util.ts │ └── vscodeContext.ts └── vimscript │ ├── exCommand.ts │ ├── exCommandParser.ts │ ├── expression │ ├── build.ts │ ├── displayValue.ts │ ├── evaluate.ts │ ├── parser.ts │ └── types.ts │ ├── lineRange.ts │ ├── parserUtils.ts │ └── pattern.ts ├── syntaxes └── vimscript.tmLanguage.json ├── test ├── actions │ ├── baseAction.test.ts │ ├── insertLine.test.ts │ ├── languages │ │ └── python │ │ │ └── motion.test.ts │ └── markMovement.test.ts ├── cmd_line │ ├── bang.test.ts │ ├── breakpoints.test.ts │ ├── bufferDelete.test.ts │ ├── command.test.ts │ ├── cursorLocation.test.ts │ ├── grep.test.ts │ ├── historyFile.test.ts │ ├── move.test.ts │ ├── normal.test.ts │ ├── only.test.ts │ ├── put.test.ts │ ├── redo.test.ts │ ├── retab.test.ts │ ├── smile.test.ts │ ├── sort.test.ts │ ├── split.test.ts │ ├── substitute.test.ts │ ├── tab.test.ts │ ├── tabCompletion.test.ts │ ├── undo.test.ts │ ├── vsplit.test.ts │ ├── writequit.test.ts │ └── yank.test.ts ├── completion │ └── lineCompletion.test.ts ├── configuration │ ├── configuration.test.ts │ ├── langmap.test.ts │ ├── notation.test.ts │ ├── remapper.test.ts │ ├── remaps.test.ts │ ├── validators │ │ ├── neovimValidator.test.ts │ │ └── remappingValidator.test.ts │ ├── vimrc.test.ts │ └── vimrcKeyRemappingBuilder.test.ts ├── error.test.ts ├── extension.test.ts ├── historyTracker.test.ts ├── index.ts ├── jumpTracker.test.ts ├── macro.test.ts ├── marks.test.ts ├── mode │ ├── modeHandler.test.ts │ ├── modeInsert.test.ts │ ├── modeNormal.test.ts │ ├── modeReplace.test.ts │ ├── modeVisual.test.ts │ ├── modeVisualBlock.test.ts │ ├── modeVisualLine.test.ts │ └── normalModeTests │ │ ├── commands.test.ts │ │ ├── dot.test.ts │ │ ├── matchingBracket.test.ts │ │ ├── motionMatchpairs.test.ts │ │ ├── motions.test.ts │ │ └── undo.test.ts ├── motion.test.ts ├── motionLineWrapping.test.ts ├── multicursor.test.ts ├── number │ ├── incrementDecrement.test.ts │ └── numericString.test.ts ├── operator │ ├── comment.test.ts │ ├── filter.test.ts │ ├── format.test.ts │ ├── put.test.ts │ ├── rot13.test.ts │ ├── shift.test.ts │ └── surrogate.test.ts ├── plugins │ ├── camelCaseMotion.test.ts │ ├── easymotion.test.ts │ ├── imswitcher.test.ts │ ├── lastNextObject.test.ts │ ├── replaceWithRegister.test.ts │ ├── smartQuotes.test.ts │ ├── sneak.test.ts │ └── surround.test.ts ├── register │ ├── register.test.ts │ └── repeatableMovement.test.ts ├── runTest.ts ├── search │ ├── motionIncSearch.test.ts │ ├── search.test.ts │ └── searchTextObject.test.ts ├── sentenceMotion.test.ts ├── state │ └── vimState.test.ts ├── testConfiguration.ts ├── testSimplifier.ts ├── testUtils.ts ├── util │ └── path.test.ts └── vimscript │ ├── exCommandParse.test.ts │ ├── expression.test.ts │ ├── lineRangeParse.test.ts │ ├── lineRangeResolve.test.ts │ └── searchOffset.test.ts ├── tsconfig.json ├── webpack.config.js ├── webpack.dev.js └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [J-Fields] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | **Describe the bug** 7 | A clear and concise description of what the bug is. 8 | 9 | **To Reproduce** 10 | Steps to reproduce the behavior: 11 | 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | If remapping-related, please attach log output: https://github.com/VSCodeVim/Vim#debugging-remappings. 23 | 24 | **Environment (please complete the following information):** 25 | 26 | 30 | 31 | - Extension (VsCodeVim) version: 32 | - VSCode version: 33 | - OS: 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 8 | 9 | **Describe the solution you'd like** 10 | A clear and concise description of what you want to happen. 11 | 12 | **Describe alternatives you've considered** 13 | A clear and concise description of any alternative solutions or features you've considered. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | **What this PR does / why we need it**: 12 | 13 | **Which issue(s) this PR fixes** 14 | 15 | 18 | 19 | **Special notes for your reviewer**: 20 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, windows-latest] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - name: Checkout VSCodeVim 16 | uses: actions/checkout@v3 17 | 18 | - name: Set up Node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 20 22 | cache: yarn 23 | 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile 26 | 27 | - name: Prettier 28 | if: matrix.os != 'windows-latest' 29 | run: yarn prettier:check 30 | 31 | - name: Lint 32 | run: yarn lint 33 | 34 | - name: Build 35 | run: gulp webpack 36 | 37 | - name: Test on ubuntu-latest 38 | if: matrix.os != 'windows-latest' 39 | run: | 40 | gulp prepare-test 41 | xvfb-run -a yarn test 42 | 43 | - name: Test on windows-latest 44 | if: matrix.os == 'windows-latest' 45 | run: | 46 | gulp prepare-test 47 | yarn test 48 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '**' 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - name: Checkout VSCodeVim 16 | uses: actions/checkout@v3 17 | 18 | - name: Set up Node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 20 22 | cache: yarn 23 | 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile 26 | 27 | - name: Prettier 28 | run: yarn prettier:check 29 | 30 | - name: Lint 31 | run: yarn lint 32 | 33 | - name: Build 34 | run: gulp webpack 35 | 36 | - name: Test 37 | run: | 38 | gulp prepare-test 39 | xvfb-run -a yarn test 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v1.[0-9]+.[0-9]+ 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout VSCodeVim 13 | uses: actions/checkout@v3 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 20 19 | cache: yarn 20 | 21 | - name: Install dependencies 22 | run: yarn install --frozen-lockfile 23 | 24 | - name: Lint 25 | run: yarn lint 26 | 27 | - name: Build 28 | run: gulp webpack 29 | 30 | - name: Test 31 | run: | 32 | gulp prepare-test 33 | xvfb-run -a yarn test 34 | 35 | - name: Build extension package 36 | id: build_vsix 37 | run: | 38 | yarn package; 39 | echo ::set-output name=vsix_path::$(ls *.vsix); 40 | 41 | - name: Create release on GitHub 42 | id: create_release 43 | uses: actions/create-release@v1 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | with: 47 | tag_name: ${{ github.ref }} 48 | release_name: ${{ github.ref }} 49 | draft: false 50 | prerelease: false 51 | 52 | - name: Upload .vsix as release asset to GitHub 53 | uses: actions/upload-release-asset@v1 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | with: 57 | upload_url: ${{ steps.create_release.outputs.upload_url }} 58 | asset_path: ${{ steps.build_vsix.outputs.vsix_path }} 59 | asset_name: ${{ steps.build_vsix.outputs.vsix_path }} 60 | asset_content_type: application/zip 61 | 62 | - name: Publish to VSCode Extension Marketplace 63 | run: yarn run vsce publish --yarn 64 | env: 65 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 66 | 67 | - name: Publish to Open VSX Registry 68 | uses: HaaLeo/publish-vscode-extension@v1 69 | id: publishToOpenVSX 70 | with: 71 | pat: ${{ secrets.OPEN_VSX_TOKEN }} 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | testing 3 | node_modules 4 | *.sw? 5 | .vscode-test 6 | .DS_Store 7 | *.vsix 8 | *.log 9 | .cache 10 | 11 | typings/* 12 | !typings/custom/ 13 | testing 14 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode-test 2 | out -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"], 7 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 8 | "unwantedRecommendations": [] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Build, Run Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outFiles": ["${workspaceRoot}/{out, node_modules}/**/*.js"], 14 | "preLaunchTask": "gulp: build-dev", 15 | "internalConsoleOptions": "openOnSessionStart" 16 | }, 17 | { 18 | "name": "Run Extension", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 23 | "stopOnEntry": false, 24 | "sourceMaps": true, 25 | "outFiles": ["${workspaceRoot}/{out, node_modules}/**/*.js"] 26 | }, 27 | { 28 | "name": "Run Tests", 29 | "type": "extensionHost", 30 | "request": "launch", 31 | "runtimeExecutable": "${execPath}", 32 | "args": [ 33 | "--extensionDevelopmentPath=${workspaceRoot}", 34 | "--extensionTestsPath=${workspaceRoot}/out/test" 35 | ], 36 | "stopOnEntry": false, 37 | "sourceMaps": true, 38 | "outFiles": ["${workspaceRoot}/{out, node_modules}/**/*.js"], 39 | "preLaunchTask": "gulp: prepare-test", 40 | "internalConsoleOptions": "openOnSessionStart" 41 | }, 42 | { 43 | "name": "Run Web Extension", 44 | "type": "pwa-extensionHost", 45 | "debugWebWorkerHost": true, 46 | "request": "launch", 47 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionDevelopmentKind=web"], 48 | "outFiles": ["${workspaceRoot}/{out, node_modules}/**/*.js"] 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.trimTrailingWhitespace": true, 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 | "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version 10 | "editor.tabSize": 2, 11 | "editor.insertSpaces": true 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | { 9 | "version": "2.0.0", 10 | "tasks": [ 11 | { 12 | "type": "gulp", 13 | "task": "default", 14 | "problemMatcher": "$tsc-watch" 15 | }, 16 | { 17 | "type": "gulp", 18 | "task": "build-dev", 19 | "problemMatcher": [] 20 | }, 21 | { 22 | "type": "gulp", 23 | "task": "prepare-test", 24 | "problemMatcher": [] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .github/** 2 | .husky/** 3 | .yarn/** 4 | .vscode/** 5 | .vscode-test/** 6 | 7 | **/*.ts 8 | *.yml 9 | 10 | src/** 11 | build/** 12 | test/** 13 | typings/** 14 | out/src/** 15 | out/test/** 16 | out/*.map 17 | node_modules/** 18 | images/design/** 19 | 20 | .gitignore 21 | .prettierignore 22 | 23 | tsconfig.json 24 | webpack.*.js 25 | gulpfile.js 26 | renovate.json 27 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Standards 4 | 5 | Be nice. Please. Everybody contributing to open source contributes out of good will in their own free time. 6 | 7 | ## Our Responsibilities 8 | 9 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 10 | 11 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 VSCodeVim 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 | 23 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please _do not_ open an issue for a security vulnerability. 6 | 7 | Instead, please send an email to jasonfields4@gmail.com with "SECURITY" in the subject. I should get back to you within 48 hours; please follow up with another email if I do not. 8 | -------------------------------------------------------------------------------- /build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN apt-get update && \ 6 | apt-get install -y xorg xvfb libxss-dev libgtk-3-0 gconf2 libnss3 libasound2 libsecret-1-0 7 | 8 | ENV CXX="g++-4.9" 9 | ENV CC="gcc-4.9" 10 | ENV DISPLAY=:99.0 11 | 12 | WORKDIR /app 13 | 14 | ENTRYPOINT ["sh", "-c", "(Xvfb $DISPLAY -screen 0 1024x768x16 &) && npm run test"] 15 | -------------------------------------------------------------------------------- /extension.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extension.ts is a lightweight wrapper around ModeHandler. It converts key 3 | * events to their string names and passes them on to ModeHandler via 4 | * handleKeyEvent(). 5 | */ 6 | import './src/actions/include-main'; 7 | import './src/actions/include-plugins'; 8 | 9 | /** 10 | * Load configuration validator 11 | */ 12 | 13 | import './src/configuration/validators/inputMethodSwitcherValidator'; 14 | import './src/configuration/validators/remappingValidator'; 15 | import './src/configuration/validators/neovimValidator'; 16 | import './src/configuration/validators/vimrcValidator'; 17 | 18 | import * as vscode from 'vscode'; 19 | import { 20 | activate as activateFunc, 21 | loadConfiguration, 22 | registerCommand, 23 | registerEventListener, 24 | } from './extensionBase'; 25 | import { Globals } from './src/globals'; 26 | import { Register } from './src/register/register'; 27 | import { vimrc } from './src/configuration/vimrc'; 28 | import * as path from 'path'; 29 | import { Logger } from './src/util/logger'; 30 | 31 | export { getAndUpdateModeHandler } from './extensionBase'; 32 | 33 | export async function activate(context: vscode.ExtensionContext) { 34 | // Set the storage path to be used by history files 35 | Globals.extensionStoragePath = context.globalStorageUri.fsPath; 36 | 37 | await activateFunc(context); 38 | 39 | registerEventListener(context, vscode.workspace.onDidSaveTextDocument, async (document) => { 40 | if (vimrc.vimrcPath && path.relative(document.fileName, vimrc.vimrcPath) === '') { 41 | await loadConfiguration(); 42 | Logger.info('Sourced new .vimrc'); 43 | } 44 | }); 45 | 46 | registerCommand( 47 | context, 48 | 'vim.editVimrc', 49 | async () => { 50 | if (vimrc.vimrcPath) { 51 | const document = await vscode.workspace.openTextDocument(vimrc.vimrcPath); 52 | await vscode.window.showTextDocument(document); 53 | } else { 54 | await vscode.window.showWarningMessage('No .vimrc found. Please set `vim.vimrc.path`.'); 55 | } 56 | }, 57 | false, 58 | ); 59 | } 60 | 61 | export async function deactivate() { 62 | await Register.saveToDisk(true); 63 | } 64 | -------------------------------------------------------------------------------- /extensionWeb.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extension.ts is a lightweight wrapper around ModeHandler. It converts key 3 | * events to their string names and passes them on to ModeHandler via 4 | * handleKeyEvent(). 5 | */ 6 | import './src/actions/include-main'; 7 | import './src/actions/include-plugins'; 8 | 9 | /** 10 | * Load configuration validator 11 | */ 12 | 13 | import './src/configuration/validators/inputMethodSwitcherValidator'; 14 | import './src/configuration/validators/remappingValidator'; 15 | 16 | import * as vscode from 'vscode'; 17 | import { activate as activateFunc } from './extensionBase'; 18 | 19 | export async function activate(context: vscode.ExtensionContext) { 20 | void activateFunc(context, false); 21 | } 22 | -------------------------------------------------------------------------------- /images/design/vscodevim-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VSCodeVim/Vim/c50f424fbe4c61e54af14e9d671d63c83d39c477/images/design/vscodevim-logo.png -------------------------------------------------------------------------------- /images/design/vscodevim-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VSCodeVim/Vim/c50f424fbe4c61e54af14e9d671d63c83d39c477/images/icon.png -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "\"" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base", "default:pinDigestsDisabled"], 4 | "automerge": false, 5 | "ignoreDeps": ["@types/vscode"], 6 | "labels": ["pr/dependency"] 7 | } 8 | -------------------------------------------------------------------------------- /src/actions/include-main.ts: -------------------------------------------------------------------------------- 1 | import './base'; 2 | import './operator'; 3 | import './motion'; 4 | import '../textobject/textobject'; 5 | 6 | // commands 7 | import './commands/insert'; 8 | import './commands/replace'; 9 | import './commands/actions'; 10 | import './commands/commandLine'; 11 | import './commands/search'; 12 | import './commands/put'; 13 | import './commands/digraphs'; 14 | import './commands/window'; 15 | import './commands/fold'; 16 | import './commands/scroll'; 17 | import './commands/join'; 18 | -------------------------------------------------------------------------------- /src/actions/include-plugins.ts: -------------------------------------------------------------------------------- 1 | // plugin 2 | import './plugins/camelCaseMotion'; 3 | import './plugins/easymotion/easymotion.cmd'; 4 | import './plugins/easymotion/registerMoveActions'; 5 | import './plugins/sneak'; 6 | import './plugins/replaceWithRegister'; 7 | import './plugins/surround'; 8 | import './plugins/targets/targets'; 9 | -------------------------------------------------------------------------------- /src/actions/plugins/easymotion/markerGenerator.ts: -------------------------------------------------------------------------------- 1 | import { Position } from 'vscode'; 2 | import { configuration } from './../../../configuration/configuration'; 3 | import { Marker } from './types'; 4 | 5 | export class MarkerGenerator { 6 | private matchesCount: number; 7 | private keyTable: string[]; 8 | private prefixKeyTable: string[]; 9 | 10 | constructor(matchesCount: number) { 11 | this.matchesCount = matchesCount; 12 | this.keyTable = this.getKeyTable(); 13 | this.prefixKeyTable = this.createPrefixKeyTable(); 14 | } 15 | 16 | public generateMarker(index: number, markerPosition: Position): Marker | null { 17 | const { keyTable, prefixKeyTable } = this; 18 | 19 | if (index >= keyTable.length - prefixKeyTable.length) { 20 | const remainder = index - (keyTable.length - prefixKeyTable.length); 21 | const currentStep = Math.floor(remainder / keyTable.length) + 1; 22 | if (currentStep > prefixKeyTable.length) { 23 | return null; 24 | } else { 25 | const prefix = prefixKeyTable[currentStep - 1]; 26 | const label = keyTable[remainder % keyTable.length]; 27 | return { 28 | name: prefix + label, 29 | position: markerPosition, 30 | }; 31 | } 32 | } else { 33 | return { 34 | name: keyTable[index], 35 | position: markerPosition, 36 | }; 37 | } 38 | } 39 | 40 | private createPrefixKeyTable(): string[] { 41 | const totalRemainder = Math.max(this.matchesCount - this.keyTable.length, 0); 42 | const totalSteps = Math.ceil(totalRemainder / this.keyTable.length); 43 | const reversed = this.keyTable.slice().reverse(); 44 | const count = Math.min(totalSteps, reversed.length); 45 | return reversed.slice(0, count); 46 | } 47 | 48 | /** 49 | * The key sequence for marker name generation 50 | */ 51 | private getKeyTable(): string[] { 52 | if (configuration.easymotionKeys) { 53 | return configuration.easymotionKeys.split(''); 54 | } else { 55 | return 'hklyuiopnm,qwertzxcvbasdgjf;'.split(''); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/actions/plugins/easymotion/types.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Position } from 'vscode'; 3 | import { Mode } from '../../../mode/mode'; 4 | import type { VimState } from '../../../state/vimState'; 5 | 6 | export type LabelPosition = 'after' | 'before'; 7 | export type JumpToAnywhere = true | false; 8 | 9 | export interface EasyMotionMoveOptionsBase { 10 | searchOptions?: 'min' | 'max'; 11 | } 12 | 13 | export interface EasyMotionCharMoveOpions extends EasyMotionMoveOptionsBase { 14 | charCount: number; 15 | labelPosition?: LabelPosition; 16 | } 17 | 18 | export interface EasyMotionWordMoveOpions extends EasyMotionMoveOptionsBase { 19 | labelPosition?: LabelPosition; 20 | jumpToAnywhere?: JumpToAnywhere; 21 | } 22 | 23 | export interface Marker { 24 | name: string; 25 | position: Position; 26 | } 27 | 28 | export class Match { 29 | public position: Position; 30 | public readonly text: string; 31 | public readonly index: number; 32 | 33 | constructor(position: Position, text: string, index: number) { 34 | this.position = position; 35 | this.text = text; 36 | this.index = index; 37 | } 38 | 39 | public toRange(): vscode.Range { 40 | return new vscode.Range(this.position, this.position.translate(0, this.text.length)); 41 | } 42 | } 43 | 44 | export interface SearchOptions { 45 | /** 46 | * The minimum bound of the search 47 | */ 48 | min?: Position; 49 | 50 | /** 51 | * The maximum bound of the search 52 | */ 53 | max?: Position; 54 | } 55 | 56 | export interface EasyMotionSearchAction { 57 | searchString: string; 58 | 59 | /** 60 | * True if it should go to Easymotion mode 61 | */ 62 | shouldFire(): boolean; 63 | 64 | /** 65 | * Command to execute when it should fire 66 | */ 67 | fire(position: Position, vimState: VimState): Promise; 68 | getMatches(position: Position, vimState: VimState): Match[]; 69 | readonly searchCharCount: number; 70 | } 71 | 72 | export interface IEasyMotion { 73 | accumulation: string; 74 | previousMode: Mode; 75 | markers: Marker[]; 76 | searchAction: EasyMotionSearchAction; 77 | 78 | addMarker(marker: Marker): void; 79 | findMarkers(nail: string, onlyVisible: boolean): Marker[]; 80 | sortedSearch( 81 | document: vscode.TextDocument, 82 | position: Position, 83 | search?: string | RegExp, 84 | options?: SearchOptions, 85 | ): Match[]; 86 | updateDecorations(editor: vscode.TextEditor): void; 87 | clearMarkers(): void; 88 | clearDecorations(editor: vscode.TextEditor): void; 89 | } 90 | -------------------------------------------------------------------------------- /src/actions/plugins/imswitcher.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '../../util/logger'; 2 | import { Mode } from '../../mode/mode'; 3 | import { configuration } from '../../configuration/configuration'; 4 | import { exec } from 'child_process'; 5 | 6 | /** 7 | * This function executes a shell command and returns the standard output as a string. 8 | */ 9 | function executeShell(cmd: string): Promise { 10 | return new Promise((resolve, reject) => { 11 | try { 12 | exec(cmd, (err, stdout, stderr) => { 13 | if (err) { 14 | reject(err); 15 | } else { 16 | resolve(stdout); 17 | } 18 | }); 19 | } catch (error) { 20 | reject(error as Error); 21 | } 22 | }); 23 | } 24 | 25 | /** 26 | * InputMethodSwitcher changes input method when mode changed 27 | */ 28 | export class InputMethodSwitcher { 29 | private execute: (cmd: string) => Promise; 30 | private savedIMKey = ''; 31 | 32 | constructor(execute: (cmd: string) => Promise = executeShell) { 33 | this.execute = execute; 34 | } 35 | 36 | public async switchInputMethod(prevMode: Mode, newMode: Mode) { 37 | if (configuration.autoSwitchInputMethod.enable !== true) { 38 | return; 39 | } 40 | // when you exit from insert-like mode, save origin input method and set it to default 41 | const isPrevModeInsertLike = this.isInsertLikeMode(prevMode); 42 | const isNewModeInsertLike = this.isInsertLikeMode(newMode); 43 | if (isPrevModeInsertLike !== isNewModeInsertLike) { 44 | if (isNewModeInsertLike) { 45 | await this.resumeIM(); 46 | } else { 47 | await this.switchToDefaultIM(); 48 | } 49 | } 50 | } 51 | 52 | // save origin input method and set input method to default 53 | private async switchToDefaultIM() { 54 | const obtainIMCmd = configuration.autoSwitchInputMethod.obtainIMCmd; 55 | try { 56 | const insertIMKey = await this.execute(obtainIMCmd); 57 | if (insertIMKey !== undefined) { 58 | this.savedIMKey = insertIMKey.trim(); 59 | } 60 | } catch (e) { 61 | Logger.error(`Error switching to default IM. err=${e}`); 62 | } 63 | 64 | const defaultIMKey = configuration.autoSwitchInputMethod.defaultIM; 65 | if (defaultIMKey !== this.savedIMKey) { 66 | await this.switchToIM(defaultIMKey); 67 | } 68 | } 69 | 70 | // resume origin inputmethod 71 | private async resumeIM() { 72 | if (this.savedIMKey !== configuration.autoSwitchInputMethod.defaultIM) { 73 | await this.switchToIM(this.savedIMKey); 74 | } 75 | } 76 | 77 | private async switchToIM(imKey: string) { 78 | let switchIMCmd = configuration.autoSwitchInputMethod.switchIMCmd; 79 | if (imKey !== '' && imKey !== undefined) { 80 | switchIMCmd = switchIMCmd.replace('{im}', imKey); 81 | try { 82 | await this.execute(switchIMCmd); 83 | } catch (e) { 84 | Logger.error(`Error switching to IM. err=${e}`); 85 | } 86 | } 87 | } 88 | 89 | private isInsertLikeMode(mode: Mode): boolean { 90 | return [Mode.Insert, Mode.Replace, Mode.SurroundInputMode].includes(mode); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/actions/plugins/pluginDefaultMappings.ts: -------------------------------------------------------------------------------- 1 | import { IConfiguration, IKeyRemapping } from '../../configuration/iconfiguration'; 2 | 3 | export class PluginDefaultMappings { 4 | // plugin authers may add entries here 5 | private static defaultMappings: Array<{ 6 | mode: string; 7 | configSwitch: string; 8 | mapping: IKeyRemapping; 9 | }> = [ 10 | // default maps for surround 11 | { 12 | mode: 'normalModeKeyBindingsNonRecursive', 13 | configSwitch: 'surround', 14 | mapping: { before: ['y', 's'], after: [''] }, 15 | }, 16 | { 17 | mode: 'normalModeKeyBindingsNonRecursive', 18 | configSwitch: 'surround', 19 | mapping: { before: ['y', 's', 's'], after: ['', ''] }, 20 | }, 21 | { 22 | mode: 'normalModeKeyBindingsNonRecursive', 23 | configSwitch: 'surround', 24 | mapping: { before: ['c', 's'], after: [''] }, 25 | }, 26 | { 27 | mode: 'normalModeKeyBindingsNonRecursive', 28 | configSwitch: 'surround', 29 | mapping: { before: ['d', 's'], after: [''] }, 30 | }, 31 | ]; 32 | 33 | public static getPluginDefaultMappings(mode: string, config: IConfiguration): IKeyRemapping[] { 34 | return this.defaultMappings 35 | .filter((m) => m.mode === mode && config[m.configSwitch]) 36 | .map((m) => m.mapping); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/actions/plugins/replaceWithRegister.ts: -------------------------------------------------------------------------------- 1 | import { configuration } from '../../configuration/configuration'; 2 | import { Mode } from '../../mode/mode'; 3 | import { Register, RegisterMode } from '../../register/register'; 4 | import { VimState } from '../../state/vimState'; 5 | import { BaseOperator } from '../operator'; 6 | import { RegisterAction } from './../base'; 7 | import { StatusBar } from '../../statusBar'; 8 | import { VimError, ErrorCode } from '../../error'; 9 | import { Position, Range } from 'vscode'; 10 | import { PositionDiff } from '../../common/motion/position'; 11 | 12 | @RegisterAction 13 | class ReplaceOperator extends BaseOperator { 14 | public keys = ['g', 'r']; 15 | public modes = [Mode.Normal, Mode.Visual, Mode.VisualLine]; 16 | 17 | public override doesActionApply(vimState: VimState, keysPressed: string[]): boolean { 18 | return configuration.replaceWithRegister && super.doesActionApply(vimState, keysPressed); 19 | } 20 | 21 | public override couldActionApply(vimState: VimState, keysPressed: string[]): boolean { 22 | return configuration.replaceWithRegister && super.doesActionApply(vimState, keysPressed); 23 | } 24 | 25 | public async run(vimState: VimState, start: Position, end: Position): Promise { 26 | const range = 27 | vimState.currentRegisterMode === RegisterMode.LineWise 28 | ? new Range(start.getLineBegin(), end.getLineEndIncludingEOL()) 29 | : new Range(start, end.getRight()); 30 | 31 | const register = await Register.get(vimState.recordedState.registerName, this.multicursorIndex); 32 | if (register === undefined) { 33 | StatusBar.displayError( 34 | vimState, 35 | VimError.fromCode(ErrorCode.NothingInRegister, vimState.recordedState.registerName), 36 | ); 37 | return; 38 | } 39 | 40 | const replaceWith = register.text as string; 41 | 42 | vimState.recordedState.transformer.addTransformation({ 43 | type: 'replaceText', 44 | range, 45 | text: replaceWith, 46 | diff: PositionDiff.exactPosition(getCursorPosition(vimState, range, replaceWith)), 47 | }); 48 | 49 | await vimState.setCurrentMode(Mode.Normal); 50 | } 51 | } 52 | 53 | const getCursorPosition = (vimState: VimState, range: Range, replaceWith: string): Position => { 54 | const { 55 | recordedState: { actionKeys }, 56 | } = vimState; 57 | const lines = replaceWith.split('\n'); 58 | const wasRunAsLineAction = actionKeys.indexOf('r') === 0 && actionKeys.length === 1; // ie. grr 59 | const registerAndRangeAreSingleLines = lines.length === 1 && range.isSingleLine; 60 | const singleLineAction = registerAndRangeAreSingleLines && !wasRunAsLineAction; 61 | 62 | return singleLineAction 63 | ? cursorAtEndOfReplacement(range, replaceWith) 64 | : cursorAtFirstNonBlankCharOfLine(range.start.line, lines[0]); 65 | }; 66 | 67 | const cursorAtEndOfReplacement = (range: Range, replacement: string) => 68 | new Position(range.start.line, Math.max(0, range.start.character + replacement.length - 1)); 69 | 70 | const cursorAtFirstNonBlankCharOfLine = (line: number, text: string) => 71 | new Position(line, text.match(/\S/)?.index ?? 0); 72 | -------------------------------------------------------------------------------- /src/actions/plugins/targets/lastNextObjects.ts: -------------------------------------------------------------------------------- 1 | import { RegisterAction } from '../../base'; 2 | import { 3 | MoveAroundCaret, 4 | MoveAroundCurlyBrace, 5 | MoveAroundParentheses, 6 | MoveAroundSquareBracket, 7 | MoveInsideCaret, 8 | MoveInsideCurlyBrace, 9 | MoveInsideParentheses, 10 | MoveInsideSquareBracket, 11 | } from '../../motion'; 12 | import { LastObject, NextObject } from './lastNextObjectHelper'; 13 | 14 | @RegisterAction 15 | class MoveInsideNextParentheses extends NextObject(MoveInsideParentheses) { 16 | override readonly charToFind: string = '('; 17 | } 18 | 19 | @RegisterAction 20 | class MoveInsideLastParentheses extends LastObject(MoveInsideParentheses) { 21 | override readonly charToFind: string = ')'; 22 | } 23 | 24 | @RegisterAction 25 | class MoveAroundNextParentheses extends NextObject(MoveAroundParentheses) { 26 | override readonly charToFind: string = '('; 27 | } 28 | 29 | @RegisterAction 30 | class MoveAroundLastParentheses extends LastObject(MoveAroundParentheses) { 31 | override readonly charToFind: string = ')'; 32 | } 33 | 34 | @RegisterAction 35 | class MoveInsideNextCurlyBrace extends NextObject(MoveInsideCurlyBrace) { 36 | override readonly charToFind: string = '{'; 37 | } 38 | 39 | @RegisterAction 40 | class MoveInsideLastCurlyBrace extends LastObject(MoveInsideCurlyBrace) { 41 | override readonly charToFind: string = '}'; 42 | } 43 | 44 | @RegisterAction 45 | class MoveAroundNextCurlyBrace extends NextObject(MoveAroundCurlyBrace) { 46 | override readonly charToFind: string = '{'; 47 | } 48 | 49 | @RegisterAction 50 | class MoveAroundLastCurlyBrace extends LastObject(MoveAroundCurlyBrace) { 51 | override readonly charToFind: string = '}'; 52 | } 53 | 54 | @RegisterAction 55 | class MoveInsideNextSquareBracket extends NextObject(MoveInsideSquareBracket) { 56 | override readonly charToFind: string = '['; 57 | } 58 | 59 | @RegisterAction 60 | class MoveInsideLastSquareBracket extends LastObject(MoveInsideSquareBracket) { 61 | override readonly charToFind: string = ']'; 62 | } 63 | 64 | @RegisterAction 65 | class MoveAroundNextSquareBracket extends NextObject(MoveAroundSquareBracket) { 66 | override readonly charToFind: string = '['; 67 | } 68 | 69 | @RegisterAction 70 | class MoveAroundLastSquareBracket extends LastObject(MoveAroundSquareBracket) { 71 | override readonly charToFind: string = ']'; 72 | } 73 | 74 | @RegisterAction 75 | class MoveInsideNextCaret extends NextObject(MoveInsideCaret) { 76 | override readonly charToFind: string = '<'; 77 | } 78 | 79 | @RegisterAction 80 | class MoveInsideLastCaret extends LastObject(MoveInsideCaret) { 81 | override readonly charToFind: string = '>'; 82 | } 83 | 84 | @RegisterAction 85 | class MoveAroundNextCaret extends NextObject(MoveAroundCaret) { 86 | override readonly charToFind: string = '<'; 87 | } 88 | 89 | @RegisterAction 90 | class MoveAroundLastCaret extends LastObject(MoveAroundCaret) { 91 | override readonly charToFind: string = '>'; 92 | } 93 | -------------------------------------------------------------------------------- /src/actions/plugins/targets/targets.ts: -------------------------------------------------------------------------------- 1 | // targets sub-plugins 2 | import './smartQuotes'; 3 | import './lastNextObjects'; 4 | -------------------------------------------------------------------------------- /src/actions/plugins/targets/targetsConfig.ts: -------------------------------------------------------------------------------- 1 | import { configuration } from '../../../configuration/configuration'; 2 | 3 | export function useSmartQuotes(): boolean { 4 | return ( 5 | (configuration.targets.enable === true && configuration.targets.smartQuotes.enable !== false) || 6 | (configuration.targets.enable === undefined && 7 | configuration.targets.smartQuotes.enable === true) 8 | ); 9 | } 10 | 11 | export function bracketObjectsEnabled(): boolean { 12 | return ( 13 | (configuration.targets.enable === true && 14 | configuration.targets.bracketObjects.enable !== false) || 15 | (configuration.targets.enable === undefined && 16 | configuration.targets.bracketObjects.enable === true) 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/actions/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { Position } from 'vscode'; 2 | import type { VimState } from '../state/vimState'; 3 | 4 | export type ActionType = 'command' | 'motion' | 'operator' | 'number'; 5 | 6 | export interface IBaseAction { 7 | name: string; 8 | readonly actionType: ActionType; 9 | readonly isJump: boolean; 10 | readonly createsUndoPoint: boolean; 11 | 12 | keysPressed: string[]; 13 | multicursorIndex: number | undefined; 14 | 15 | readonly preservesDesiredColumn: boolean; 16 | } 17 | 18 | export interface IBaseCommand extends IBaseAction { 19 | exec(position: Position, vimState: VimState): Promise; 20 | } 21 | 22 | export interface IBaseOperator extends IBaseAction { 23 | run(vimState: VimState, start: Position, stop: Position): Promise; 24 | runRepeat(vimState: VimState, position: Position, count: number): Promise; 25 | } 26 | -------------------------------------------------------------------------------- /src/actions/wrapping.ts: -------------------------------------------------------------------------------- 1 | import { configuration } from './../configuration/configuration'; 2 | import { Mode } from './../mode/mode'; 3 | 4 | /** 5 | * See https://vimhelp.org/options.txt.html#%27whichwrap%27 6 | * 7 | * @returns true if the given key should cause the cursor to wrap around line boundary 8 | */ 9 | export const shouldWrapKey = (mode: Mode, key: string): boolean => { 10 | let k: string; 11 | if (key === '') { 12 | k = [Mode.Insert, Mode.Replace].includes(mode) ? '[' : '<'; 13 | } else if (key === '') { 14 | k = [Mode.Insert, Mode.Replace].includes(mode) ? ']' : '>'; 15 | } else if (['', '', ''].includes(key)) { 16 | k = 'b'; 17 | } else if (key === ' ') { 18 | k = 's'; 19 | } else if (['h', 'l', '~'].includes(key)) { 20 | k = key; 21 | } else { 22 | throw new Error(`shouldWrapKey called with unexpected key='${key}'`); 23 | } 24 | return configuration.whichwrap.split(',').includes(k); 25 | }; 26 | -------------------------------------------------------------------------------- /src/cmd_line/commands/ascii.ts: -------------------------------------------------------------------------------- 1 | import { CommandUnicodeName } from '../../actions/commands/actions'; 2 | import { VimState } from '../../state/vimState'; 3 | import { ExCommand } from '../../vimscript/exCommand'; 4 | 5 | export class AsciiCommand extends ExCommand { 6 | async execute(vimState: VimState): Promise { 7 | await new CommandUnicodeName().exec(vimState.cursorStopPosition, vimState); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/cmd_line/commands/bang.ts: -------------------------------------------------------------------------------- 1 | import { VimState } from '../../state/vimState'; 2 | import { PositionDiff } from '../../common/motion/position'; 3 | import { externalCommand } from '../../util/externalCommand'; 4 | import { LineRange } from '../../vimscript/lineRange'; 5 | import { ExCommand } from '../../vimscript/exCommand'; 6 | import { all, Parser } from 'parsimmon'; 7 | 8 | export interface IBangCommandArguments { 9 | command: string; 10 | } 11 | 12 | export class BangCommand extends ExCommand { 13 | public static readonly argParser: Parser = all.map( 14 | (command) => 15 | new BangCommand({ 16 | command, 17 | }), 18 | ); 19 | 20 | protected _arguments: IBangCommandArguments; 21 | constructor(args: IBangCommandArguments) { 22 | super(); 23 | this._arguments = args; 24 | } 25 | 26 | public override neovimCapable(): boolean { 27 | return true; 28 | } 29 | 30 | private getReplaceDiff(text: string): PositionDiff { 31 | const lines = text.split('\n'); 32 | const numNewlines = lines.length - 1; 33 | const check = lines[0].match(/^\s*/); 34 | const numWhitespace = check ? check[0].length : 0; 35 | 36 | return PositionDiff.exactCharacter({ 37 | lineOffset: -numNewlines, 38 | character: numWhitespace, 39 | }); 40 | } 41 | 42 | async execute(vimState: VimState): Promise { 43 | await externalCommand.run(this._arguments.command); 44 | } 45 | 46 | override async executeWithRange(vimState: VimState, range: LineRange): Promise { 47 | const resolvedRange = range.resolveToRange(vimState); 48 | 49 | // pipe in stdin from lines in range 50 | const input = vimState.document.getText(resolvedRange); 51 | const output = await externalCommand.run(this._arguments.command, input); 52 | 53 | // place cursor at the start of the replaced text and first non-whitespace character 54 | const diff = this.getReplaceDiff(output); 55 | 56 | vimState.recordedState.transformer.addTransformation({ 57 | type: 'replaceText', 58 | text: output, 59 | range: resolvedRange, 60 | diff, 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/cmd_line/commands/bufferDelete.ts: -------------------------------------------------------------------------------- 1 | import { alt, optWhitespace, Parser, seq, whitespace } from 'parsimmon'; 2 | import * as vscode from 'vscode'; 3 | 4 | import * as error from '../../error'; 5 | import { VimState } from '../../state/vimState'; 6 | import { StatusBar } from '../../statusBar'; 7 | import { ExCommand } from '../../vimscript/exCommand'; 8 | import { bangParser, fileNameParser, numberParser } from '../../vimscript/parserUtils'; 9 | 10 | interface IBufferDeleteCommandArguments { 11 | bang: boolean; 12 | buffers: Array; 13 | } 14 | 15 | // 16 | // Implements :bd 17 | // http://vimdoc.sourceforge.net/htmldoc/windows.html#buffers 18 | // 19 | export class BufferDeleteCommand extends ExCommand { 20 | public static readonly argParser: Parser = seq( 21 | bangParser.skip(optWhitespace), 22 | alt(numberParser, fileNameParser).sepBy(whitespace), 23 | ).map(([bang, buffers]) => new BufferDeleteCommand({ bang, buffers })); 24 | 25 | public readonly arguments: IBufferDeleteCommandArguments; 26 | constructor(args: IBufferDeleteCommandArguments) { 27 | super(); 28 | this.arguments = args; 29 | } 30 | 31 | async execute(vimState: VimState): Promise { 32 | if (vimState.document.isDirty && !this.arguments.bang) { 33 | throw error.VimError.fromCode(error.ErrorCode.NoWriteSinceLastChange); 34 | } 35 | 36 | if (this.arguments.buffers.length === 0) { 37 | await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); 38 | } else { 39 | for (const buffer of this.arguments.buffers) { 40 | if (typeof buffer === 'string') { 41 | // TODO 42 | StatusBar.setText( 43 | vimState, 44 | ':bd[elete][!] {bufname} is not yet implemented (PRs are welcome!)', 45 | true, 46 | ); 47 | continue; 48 | } 49 | 50 | try { 51 | await vscode.commands.executeCommand(`workbench.action.openEditorAtIndex${buffer}`); 52 | } catch (e) { 53 | throw error.VimError.fromCode(error.ErrorCode.NoBuffersDeleted); 54 | } 55 | 56 | await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/cmd_line/commands/close.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from 'parsimmon'; 2 | import * as vscode from 'vscode'; 3 | 4 | import * as error from '../../error'; 5 | import { VimState } from '../../state/vimState'; 6 | import { ExCommand } from '../../vimscript/exCommand'; 7 | import { bangParser } from '../../vimscript/parserUtils'; 8 | 9 | // 10 | // Implements :close 11 | // http://vimdoc.sourceforge.net/htmldoc/windows.html#:close 12 | // 13 | export class CloseCommand extends ExCommand { 14 | public static readonly argParser: Parser = bangParser.map( 15 | (bang) => new CloseCommand(bang), 16 | ); 17 | 18 | public readonly bang: boolean; 19 | constructor(bang: boolean) { 20 | super(); 21 | this.bang = bang; 22 | } 23 | 24 | async execute(vimState: VimState): Promise { 25 | if (vimState.document.isDirty && !this.bang) { 26 | throw error.VimError.fromCode(error.ErrorCode.NoWriteSinceLastChange); 27 | } 28 | 29 | if (vscode.window.visibleTextEditors.length === 1) { 30 | throw error.VimError.fromCode(error.ErrorCode.CannotCloseLastWindow); 31 | } 32 | 33 | const oldViewColumn = vimState.editor.viewColumn; 34 | await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); 35 | 36 | if ( 37 | vscode.window.activeTextEditor !== undefined && 38 | vscode.window.activeTextEditor.viewColumn === oldViewColumn 39 | ) { 40 | await vscode.commands.executeCommand('workbench.action.previousEditor'); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/cmd_line/commands/copy.ts: -------------------------------------------------------------------------------- 1 | import { optWhitespace, Parser } from 'parsimmon'; 2 | import { Position, Range } from 'vscode'; 3 | import { PositionDiff } from '../../common/motion/position'; 4 | import { ErrorCode, VimError } from '../../error'; 5 | import { VimState } from '../../state/vimState'; 6 | import { StatusBar } from '../../statusBar'; 7 | import { ExCommand } from '../../vimscript/exCommand'; 8 | import { Address, LineRange } from '../../vimscript/lineRange'; 9 | 10 | export class CopyCommand extends ExCommand { 11 | public static readonly argParser: Parser = optWhitespace 12 | .then(Address.parser.fallback(undefined)) 13 | .map((address) => new CopyCommand(address)); 14 | 15 | private address?: Address; 16 | constructor(address?: Address) { 17 | super(); 18 | this.address = address; 19 | } 20 | 21 | public override neovimCapable(): boolean { 22 | return true; 23 | } 24 | 25 | private copyLines(vimState: VimState, sourceStart: number, sourceEnd: number) { 26 | const dest = this.address?.resolve(vimState, 'left', false); 27 | if (dest === undefined || dest < -1 || dest > vimState.document.lineCount) { 28 | StatusBar.displayError(vimState, VimError.fromCode(ErrorCode.InvalidAddress)); 29 | return; 30 | } 31 | 32 | if (sourceEnd < sourceStart) { 33 | [sourceStart, sourceEnd] = [sourceEnd, sourceStart]; 34 | } 35 | 36 | const copiedText = vimState.document.getText( 37 | new Range(new Position(sourceStart, 0), new Position(sourceEnd, 0).getLineEnd()), 38 | ); 39 | 40 | let text: string; 41 | let position: Position; 42 | if (dest === -1) { 43 | text = copiedText + '\n'; 44 | position = new Position(0, 0); 45 | } else { 46 | text = '\n' + copiedText; 47 | position = new Position(dest, 0).getLineEnd(); 48 | } 49 | 50 | const lines = copiedText.split('\n'); 51 | const cursorPosition = new Position( 52 | Math.max(dest + lines.length, 0), 53 | lines[lines.length - 1].match(/\S/)?.index ?? 0, 54 | ); 55 | 56 | vimState.recordedState.transformer.insert( 57 | position, 58 | text, 59 | PositionDiff.exactPosition(cursorPosition), 60 | ); 61 | } 62 | 63 | public async execute(vimState: VimState): Promise { 64 | const line = vimState.cursors[0].stop.line; 65 | this.copyLines(vimState, line, line); 66 | } 67 | 68 | public override async executeWithRange(vimState: VimState, range: LineRange): Promise { 69 | const { start, end } = range.resolve(vimState); 70 | this.copyLines(vimState, start, end); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/cmd_line/commands/digraph.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | // eslint-disable-next-line id-denylist 4 | import { any, Parser, seq, whitespace } from 'parsimmon'; 5 | import { DefaultDigraphs } from '../../actions/commands/digraphs'; 6 | import { Digraph } from '../../configuration/iconfiguration'; 7 | import { VimState } from '../../state/vimState'; 8 | import { TextEditor } from '../../textEditor'; 9 | import { ExCommand } from '../../vimscript/exCommand'; 10 | import { bangParser, numberParser } from '../../vimscript/parserUtils'; 11 | import { configuration } from './../../configuration/configuration'; 12 | 13 | export interface IDigraphsCommandArguments { 14 | bang: boolean; 15 | newDigraph: [string, string, number[]] | undefined; 16 | } 17 | 18 | interface DigraphQuickPickItem extends vscode.QuickPickItem { 19 | charCodes: number[]; 20 | } 21 | 22 | export class DigraphsCommand extends ExCommand { 23 | public static readonly argParser: Parser = seq( 24 | bangParser, 25 | whitespace.then(seq(any, any, whitespace.then(numberParser).atLeast(1))).fallback(undefined), 26 | ).map(([bang, newDigraph]) => new DigraphsCommand({ bang, newDigraph })); 27 | 28 | private readonly arguments: IDigraphsCommandArguments; 29 | constructor(args: IDigraphsCommandArguments) { 30 | super(); 31 | this.arguments = args; 32 | } 33 | 34 | private makeQuickPicks(digraphs: Array<[string, Digraph]>): DigraphQuickPickItem[] { 35 | return digraphs.map(([shortcut, [charDesc, charCodes]]) => { 36 | if (!Array.isArray(charCodes)) { 37 | charCodes = [charCodes]; 38 | } 39 | return { 40 | label: shortcut, 41 | description: `${charDesc} (user)`, 42 | charCodes, 43 | }; 44 | }); 45 | } 46 | 47 | async execute(vimState: VimState): Promise { 48 | if (this.arguments.newDigraph) { 49 | const digraph = this.arguments.newDigraph[0] + this.arguments.newDigraph[1]; 50 | const charCodes = this.arguments.newDigraph[2]; 51 | DefaultDigraphs.set(digraph, [String.fromCharCode(...charCodes), charCodes]); 52 | } else { 53 | const digraphKeyAndContent = this.makeQuickPicks( 54 | Object.entries(configuration.digraphs), 55 | ).concat(this.makeQuickPicks([...DefaultDigraphs.entries()])); 56 | 57 | void vscode.window.showQuickPick(digraphKeyAndContent).then(async (val) => { 58 | if (val) { 59 | const char = String.fromCharCode(...val.charCodes); 60 | await TextEditor.insert(vimState.editor, char); 61 | } 62 | }); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/cmd_line/commands/echo.ts: -------------------------------------------------------------------------------- 1 | import { optWhitespace, Parser, whitespace } from 'parsimmon'; 2 | import { VimState } from '../../state/vimState'; 3 | import { StatusBar } from '../../statusBar'; 4 | import { ExCommand } from '../../vimscript/exCommand'; 5 | import { EvaluationContext } from '../../vimscript/expression/evaluate'; 6 | import { expressionParser } from '../../vimscript/expression/parser'; 7 | import { Expression } from '../../vimscript/expression/types'; 8 | import { displayValue } from '../../vimscript/expression/displayValue'; 9 | 10 | export class EchoCommand extends ExCommand { 11 | public static argParser(echoArgs: { sep: string; error: boolean }): Parser { 12 | return optWhitespace 13 | .then(expressionParser.sepBy(whitespace)) 14 | .map((expressions) => new EchoCommand(echoArgs, expressions)); 15 | } 16 | 17 | private sep: string; 18 | private error: boolean; 19 | private expressions: Expression[]; 20 | private constructor(args: { sep: string; error: boolean }, expressions: Expression[]) { 21 | super(); 22 | this.sep = args.sep; 23 | this.error = args.error; 24 | this.expressions = expressions; 25 | } 26 | 27 | public override neovimCapable(): boolean { 28 | return true; 29 | } 30 | 31 | public async execute(vimState: VimState): Promise { 32 | const ctx = new EvaluationContext(); 33 | const values = this.expressions.map((x) => ctx.evaluate(x)); 34 | const message = values.map((v) => displayValue(v)).join(this.sep); 35 | StatusBar.setText(vimState, message, this.error); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/cmd_line/commands/eval.ts: -------------------------------------------------------------------------------- 1 | import { optWhitespace, Parser } from 'parsimmon'; 2 | import { VimState } from '../../state/vimState'; 3 | import { ExCommand } from '../../vimscript/exCommand'; 4 | import { expressionParser, functionCallParser } from '../../vimscript/expression/parser'; 5 | import { Expression } from '../../vimscript/expression/types'; 6 | import { EvaluationContext } from '../../vimscript/expression/evaluate'; 7 | 8 | export class EvalCommand extends ExCommand { 9 | public static argParser: Parser = optWhitespace 10 | .then(expressionParser) 11 | .map((expression) => new EvalCommand(expression)); 12 | 13 | private expression: Expression; 14 | private constructor(expression: Expression) { 15 | super(); 16 | this.expression = expression; 17 | } 18 | 19 | public async execute(vimState: VimState): Promise { 20 | const ctx = new EvaluationContext(); 21 | ctx.evaluate(this.expression); 22 | } 23 | } 24 | 25 | export class CallCommand extends ExCommand { 26 | public static argParser: Parser = optWhitespace 27 | .then(functionCallParser) 28 | .map((call) => new CallCommand(call)); 29 | 30 | private expression: Expression; 31 | private constructor(funcCall: Expression) { 32 | super(); 33 | this.expression = funcCall; 34 | } 35 | 36 | public async execute(vimState: VimState): Promise { 37 | const ctx = new EvaluationContext(); 38 | ctx.evaluate(this.expression); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/cmd_line/commands/explore.ts: -------------------------------------------------------------------------------- 1 | import { commands } from 'vscode'; 2 | import { VimState } from '../../state/vimState'; 3 | import { ExCommand } from '../../vimscript/exCommand'; 4 | 5 | export class ExploreCommand extends ExCommand { 6 | async execute(vimState: VimState): Promise { 7 | await commands.executeCommand('workbench.view.explorer'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/cmd_line/commands/fileInfo.ts: -------------------------------------------------------------------------------- 1 | import { all, optWhitespace, Parser, seq } from 'parsimmon'; 2 | import { VimState } from '../../state/vimState'; 3 | import { reportFileInfo } from '../../util/statusBarTextUtils'; 4 | import { ExCommand } from '../../vimscript/exCommand'; 5 | import { bangParser } from '../../vimscript/parserUtils'; 6 | 7 | export class FileInfoCommand extends ExCommand { 8 | public static readonly argParser: Parser = seq( 9 | bangParser, 10 | optWhitespace.then(all), 11 | ).map(([bang, fileName]) => new FileInfoCommand({ bang, fileName })); 12 | 13 | private args: { 14 | bang: boolean; 15 | fileName?: string; 16 | }; 17 | private constructor(args: { bang: boolean; fileName?: string }) { 18 | super(); 19 | this.args = args; 20 | } 21 | 22 | async execute(vimState: VimState): Promise { 23 | // TODO: Use `this.args` 24 | reportFileInfo(vimState.cursors[0].start, vimState); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/cmd_line/commands/goto.ts: -------------------------------------------------------------------------------- 1 | import { optWhitespace, Parser } from 'parsimmon'; 2 | import { VimState } from '../../state/vimState'; 3 | import { ExCommand } from '../../vimscript/exCommand'; 4 | import { LineRange } from '../../vimscript/lineRange'; 5 | import { numberParser } from '../../vimscript/parserUtils'; 6 | 7 | export class GotoCommand extends ExCommand { 8 | public static readonly argParser: Parser = optWhitespace 9 | .then(numberParser.fallback(undefined)) 10 | .map((count) => new GotoCommand(count)); 11 | 12 | private offset?: number; 13 | constructor(offset?: number) { 14 | super(); 15 | this.offset = offset; 16 | } 17 | 18 | private gotoOffset(vimState: VimState, offset: number) { 19 | vimState.cursorStopPosition = vimState.document.positionAt(offset); 20 | } 21 | 22 | public async execute(vimState: VimState): Promise { 23 | this.gotoOffset(vimState, this.offset ?? 0); 24 | } 25 | 26 | public override async executeWithRange(vimState: VimState, range: LineRange): Promise { 27 | if (this.offset === undefined) { 28 | this.offset = range.resolve(vimState)?.end ?? 0; 29 | } 30 | this.gotoOffset(vimState, this.offset); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/cmd_line/commands/gotoLine.ts: -------------------------------------------------------------------------------- 1 | import { VimState } from '../../state/vimState'; 2 | import { ExCommand } from '../../vimscript/exCommand'; 3 | import { LineRange } from '../../vimscript/lineRange'; 4 | 5 | export class GotoLineCommand extends ExCommand { 6 | public async execute(vimState: VimState): Promise { 7 | return; 8 | } 9 | 10 | public override async executeWithRange(vimState: VimState, range: LineRange): Promise { 11 | vimState.cursorStartPosition = vimState.cursorStopPosition = vimState.cursorStopPosition 12 | .with({ line: range.resolve(vimState).end }) 13 | .obeyStartOfLine(vimState.document); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/cmd_line/commands/grep.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import * as error from '../../error'; 4 | import { VimState } from '../../state/vimState'; 5 | import { Pattern, SearchDirection } from '../../vimscript/pattern'; 6 | import { ExCommand } from '../../vimscript/exCommand'; 7 | import { Parser, seq, optWhitespace, whitespace } from 'parsimmon'; 8 | import { fileNameParser } from '../../vimscript/parserUtils'; 9 | 10 | // Still missing: 11 | // When a number is put before the command this is used 12 | // as the maximum number of matches to find. Use 13 | // ":1vimgrep pattern file" to find only the first. 14 | // Useful if you only want to check if there is a match 15 | // and quit quickly when it's found. 16 | 17 | // Without the 'j' flag Vim jumps to the first match. 18 | // With 'j' only the quickfix list is updated. 19 | // With the [!] any changes in the current buffer are 20 | // abandoned. 21 | interface IGrepCommandArguments { 22 | pattern: Pattern; 23 | files: string[]; 24 | } 25 | 26 | // Implements :grep 27 | // https://vimdoc.sourceforge.net/htmldoc/quickfix.html#:vimgrep 28 | export class GrepCommand extends ExCommand { 29 | // TODO: parse the pattern for flags to notify the user that they are not supported yet 30 | public static readonly argParser: Parser = optWhitespace.then( 31 | seq( 32 | Pattern.parser({ direction: SearchDirection.Backward, delimiter: ' ' }), 33 | fileNameParser.sepBy(whitespace), 34 | ).map(([pattern, files]) => new GrepCommand({ pattern, files })), 35 | ); 36 | 37 | public readonly arguments: IGrepCommandArguments; 38 | constructor(args: IGrepCommandArguments) { 39 | super(); 40 | this.arguments = args; 41 | } 42 | 43 | async execute(): Promise { 44 | const { pattern, files } = this.arguments; 45 | if (files.length === 0) { 46 | throw error.VimError.fromCode(error.ErrorCode.NoFileName); 47 | } 48 | // There are other arguments that can be passed, but probably need to dig into the VSCode source code, since they are not listed in the API reference 49 | // https://code.visualstudio.com/api/references/commands 50 | // This link on the other hand has the commands and I used this as a reference 51 | // https://stackoverflow.com/questions/62251045/search-find-in-files-keybinding-can-take-arguments-workbench-view-search-can 52 | await vscode.commands.executeCommand('workbench.action.findInFiles', { 53 | query: pattern.patternString, 54 | filesToInclude: files.join(','), 55 | triggerSearch: true, 56 | isRegex: true, 57 | }); 58 | await vscode.commands.executeCommand('search.action.focusSearchList'); 59 | // TODO: Only if there's no [j] flag 60 | await vscode.commands.executeCommand('search.action.focusNextSearchResult'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/cmd_line/commands/history.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line id-denylist 2 | import { Parser, alt, optWhitespace, string } from 'parsimmon'; 3 | import { 4 | CommandShowCommandHistory, 5 | CommandShowSearchHistory, 6 | } from '../../actions/commands/actions'; 7 | import { VimState } from '../../state/vimState'; 8 | import { ExCommand } from '../../vimscript/exCommand'; 9 | import { nameAbbrevParser } from '../../vimscript/parserUtils'; 10 | import { SearchDirection } from '../../vimscript/pattern'; 11 | 12 | export enum HistoryCommandType { 13 | Cmd, 14 | Search, 15 | Expr, 16 | Input, 17 | Debug, 18 | All, 19 | } 20 | 21 | const historyTypeParser: Parser = alt( 22 | alt(nameAbbrevParser('c', 'md'), string(':')).result(HistoryCommandType.Cmd), 23 | alt(nameAbbrevParser('s', 'earch'), string('/')).result(HistoryCommandType.Search), 24 | alt(nameAbbrevParser('e', 'xpr'), string('=')).result(HistoryCommandType.Expr), 25 | alt(nameAbbrevParser('i', 'nput'), string('@')).result(HistoryCommandType.Input), 26 | alt(nameAbbrevParser('d', 'ebug'), string('>')).result(HistoryCommandType.Debug), 27 | nameAbbrevParser('a', 'll').result(HistoryCommandType.All), 28 | ); 29 | 30 | export interface IHistoryCommandArguments { 31 | type: HistoryCommandType; 32 | // TODO: :history can also accept a range 33 | } 34 | 35 | // http://vimdoc.sourceforge.net/htmldoc/cmdline.html#:history 36 | export class HistoryCommand extends ExCommand { 37 | public static readonly argParser: Parser = optWhitespace 38 | .then(historyTypeParser.fallback(HistoryCommandType.Cmd)) 39 | .map((type) => new HistoryCommand({ type })); 40 | 41 | private readonly arguments: IHistoryCommandArguments; 42 | constructor(args: IHistoryCommandArguments) { 43 | super(); 44 | this.arguments = args; 45 | } 46 | 47 | async execute(vimState: VimState): Promise { 48 | switch (this.arguments.type) { 49 | case HistoryCommandType.Cmd: 50 | await new CommandShowCommandHistory().exec(vimState.cursorStopPosition, vimState); 51 | break; 52 | case HistoryCommandType.Search: 53 | await new CommandShowSearchHistory(SearchDirection.Forward).exec( 54 | vimState.cursorStopPosition, 55 | vimState, 56 | ); 57 | break; 58 | // TODO: Implement these 59 | case HistoryCommandType.Expr: 60 | throw new Error('Not implemented'); 61 | case HistoryCommandType.Input: 62 | throw new Error('Not implemented'); 63 | case HistoryCommandType.Debug: 64 | throw new Error('Not implemented'); 65 | case HistoryCommandType.All: 66 | throw new Error('Not implemented'); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/cmd_line/commands/jumps.ts: -------------------------------------------------------------------------------- 1 | import { QuickPickItem, Range, window } from 'vscode'; 2 | 3 | import { Jump } from '../../jumps/jump'; 4 | import { globalState } from '../../state/globalState'; 5 | import { VimState } from '../../state/vimState'; 6 | import { ExCommand } from '../../vimscript/exCommand'; 7 | 8 | class JumpPickItem implements QuickPickItem { 9 | jump: Jump; 10 | 11 | label: string; 12 | description?: string; 13 | detail?: string; 14 | picked?: boolean; 15 | alwaysShow?: boolean; 16 | 17 | constructor(jump: Jump, idx: number) { 18 | this.jump = jump; 19 | this.label = jump.fileName; 20 | this.detail = `jump ${idx} line ${jump.position.line + 1} col ${jump.position.character}`; 21 | try { 22 | this.description = jump.document.lineAt(jump.position).text; 23 | } catch (e) { 24 | this.description = undefined; 25 | } 26 | } 27 | } 28 | 29 | export class JumpsCommand extends ExCommand { 30 | async execute(vimState: VimState): Promise { 31 | const jumpTracker = globalState.jumpTracker; 32 | if (jumpTracker.hasJumps) { 33 | const quickPickItems = jumpTracker.jumps.map((jump, idx) => new JumpPickItem(jump, idx)); 34 | const item = await window.showQuickPick(quickPickItems, { 35 | canPickMany: false, 36 | }); 37 | if (item && item.jump.document !== undefined) { 38 | void window.showTextDocument(item.jump.document, { 39 | selection: new Range(item.jump.position, item.jump.position), 40 | }); 41 | } 42 | } else { 43 | void window.showInformationMessage('No jumps available'); 44 | } 45 | } 46 | } 47 | 48 | export class ClearJumpsCommand extends ExCommand { 49 | async execute(vimState: VimState): Promise { 50 | const jumpTracker = globalState.jumpTracker; 51 | jumpTracker.clearJumps(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/cmd_line/commands/nohl.ts: -------------------------------------------------------------------------------- 1 | import { VimState } from '../../state/vimState'; 2 | import { globalState } from '../../state/globalState'; 3 | import { StatusBar } from '../../statusBar'; 4 | import { ExCommand } from '../../vimscript/exCommand'; 5 | 6 | export class NohlCommand extends ExCommand { 7 | async execute(vimState: VimState): Promise { 8 | globalState.hl = false; 9 | 10 | // Clear the `match x of y` message from status bar 11 | StatusBar.clear(vimState); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/cmd_line/commands/normal.ts: -------------------------------------------------------------------------------- 1 | import { Parser, all, whitespace } from 'parsimmon'; 2 | import { VimState } from '../../state/vimState'; 3 | import { ExCommand } from '../../vimscript/exCommand'; 4 | import { LineRange } from '../../vimscript/lineRange'; 5 | 6 | export class NormalCommand extends ExCommand { 7 | // TODO: support to parse `:normal!` 8 | public static readonly argParser: Parser = whitespace 9 | .then(all) 10 | .map((keystrokes) => new NormalCommand(keystrokes)); 11 | 12 | private readonly keystrokes: string; 13 | constructor(argument: string) { 14 | super(); 15 | this.keystrokes = argument; 16 | } 17 | 18 | override async execute(vimState: VimState): Promise { 19 | vimState.recordedState.transformer.addTransformation({ 20 | type: 'executeNormal', 21 | keystrokes: this.keystrokes, 22 | }); 23 | } 24 | 25 | override async executeWithRange(vimState: VimState, lineRange: LineRange): Promise { 26 | vimState.recordedState.transformer.addTransformation({ 27 | type: 'executeNormal', 28 | keystrokes: this.keystrokes, 29 | range: lineRange, 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/cmd_line/commands/only.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { VimState } from '../../state/vimState'; 3 | import { ExCommand } from '../../vimscript/exCommand'; 4 | 5 | export class OnlyCommand extends ExCommand { 6 | async execute(vimState: VimState): Promise { 7 | await Promise.allSettled([ 8 | vscode.commands.executeCommand('workbench.action.joinAllGroups'), 9 | vscode.commands.executeCommand('workbench.action.maximizeEditor'), 10 | vscode.commands.executeCommand('workbench.action.closePanel'), 11 | ]); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/cmd_line/commands/print.ts: -------------------------------------------------------------------------------- 1 | import { Parser, succeed } from 'parsimmon'; 2 | import { VimState } from '../../state/vimState'; 3 | import { StatusBar } from '../../statusBar'; 4 | import { ExCommand } from '../../vimscript/exCommand'; 5 | import { Address, LineRange } from '../../vimscript/lineRange'; 6 | 7 | type PrintArgs = { 8 | printNumbers: boolean; 9 | printText: boolean; 10 | }; 11 | 12 | // TODO: `:l[ist]` is more than an alias 13 | // TODO: `:z` 14 | export class PrintCommand extends ExCommand { 15 | // TODO: Print {count} and [flags] 16 | public static readonly argParser = (args: { 17 | printNumbers: boolean; 18 | printText: boolean; 19 | }): Parser => succeed(new PrintCommand(args)); 20 | 21 | private args: PrintArgs; 22 | constructor(args: PrintArgs) { 23 | super(); 24 | this.args = args; 25 | } 26 | 27 | async execute(vimState: VimState): Promise { 28 | // TODO: Wrong default for `:=` 29 | void this.executeWithRange(vimState, new LineRange(new Address({ type: 'current_line' }))); 30 | } 31 | 32 | override async executeWithRange(vimState: VimState, range: LineRange): Promise { 33 | const { end } = range.resolve(vimState); 34 | 35 | // For now, we just print the last line. 36 | // TODO: Create a dynamic document if there's more than one line? 37 | const line = vimState.document.lineAt(end); 38 | let output: string; 39 | if (this.args.printNumbers) { 40 | if (this.args.printText) { 41 | output = `${line.lineNumber + 1} ${line.text}`; 42 | } else { 43 | output = `${line.lineNumber + 1}`; 44 | } 45 | } else { 46 | if (this.args.printText) { 47 | output = `${line.text}`; 48 | } else { 49 | output = ''; 50 | } 51 | } 52 | StatusBar.setText(vimState, output); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/cmd_line/commands/pwd.ts: -------------------------------------------------------------------------------- 1 | import { ExCommand } from '../../vimscript/exCommand'; 2 | import { VimState } from '../../state/vimState'; 3 | import { StatusBar } from '../../statusBar'; 4 | import * as vscode from 'vscode'; 5 | 6 | /** 7 | * Implements the :pwd command, which prints the current working directory. 8 | */ 9 | export class PwdCommand extends ExCommand { 10 | async execute(vimState: VimState): Promise { 11 | const workspaceFolders = vscode.workspace.workspaceFolders; 12 | if (workspaceFolders && workspaceFolders.length > 0) { 13 | const currentDir = workspaceFolders[0].uri.fsPath; 14 | StatusBar.setText(vimState, `Current Directory: ${currentDir}`); 15 | } else { 16 | StatusBar.setText(vimState, 'No workspace folder is open.'); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/cmd_line/commands/quit.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from 'parsimmon'; 2 | import * as vscode from 'vscode'; 3 | 4 | import * as error from '../../error'; 5 | import { VimState } from '../../state/vimState'; 6 | import { ExCommand } from '../../vimscript/exCommand'; 7 | import { bangParser } from '../../vimscript/parserUtils'; 8 | 9 | export interface IQuitCommandArguments { 10 | bang?: boolean; 11 | quitAll?: boolean; 12 | } 13 | 14 | // 15 | // Implements :quit 16 | // http://vimdoc.sourceforge.net/htmldoc/editing.html#:quit 17 | // 18 | export class QuitCommand extends ExCommand { 19 | public static readonly argParser: (quitAll: boolean) => Parser = ( 20 | quitAll: boolean, 21 | ) => 22 | bangParser.map( 23 | (bang) => 24 | new QuitCommand({ 25 | bang, 26 | quitAll, 27 | }), 28 | ); 29 | 30 | public override isRepeatableWithDot = false; 31 | 32 | public arguments: IQuitCommandArguments; 33 | constructor(args: IQuitCommandArguments) { 34 | super(); 35 | this.arguments = args; 36 | } 37 | 38 | async execute(vimState: VimState): Promise { 39 | // NOTE: We can't currently get all open text editors, so this isn't perfect. See #3809 40 | const duplicatedInSplit = 41 | vscode.window.visibleTextEditors.filter((editor) => editor.document === vimState.document) 42 | .length > 1; 43 | if ( 44 | vimState.document.isDirty && 45 | !this.arguments.bang && 46 | (!duplicatedInSplit || this.arguments.quitAll) 47 | ) { 48 | throw error.VimError.fromCode(error.ErrorCode.NoWriteSinceLastChange); 49 | } 50 | 51 | if (this.arguments.quitAll) { 52 | await vscode.commands.executeCommand('workbench.action.closeAllEditors'); 53 | } else { 54 | if (!this.arguments.bang) { 55 | await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); 56 | } else { 57 | await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor'); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/cmd_line/commands/read.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line id-denylist 2 | import { all, alt, optWhitespace, Parser, seq, string, whitespace } from 'parsimmon'; 3 | import { SUPPORT_READ_COMMAND } from 'platform/constants'; 4 | import { readFileAsync } from 'platform/fs'; 5 | import { VimState } from '../../state/vimState'; 6 | import { ExCommand } from '../../vimscript/exCommand'; 7 | import { fileNameParser, FileOpt, fileOptParser } from '../../vimscript/parserUtils'; 8 | 9 | export type IReadCommandArguments = { 10 | opt: FileOpt; 11 | } & ({ cmd: string } | { file: string } | object); 12 | 13 | // 14 | // Implements :read and :read! 15 | // http://vimdoc.sourceforge.net/htmldoc/insert.html#:read 16 | // http://vimdoc.sourceforge.net/htmldoc/insert.html#:read! 17 | // 18 | export class ReadCommand extends ExCommand { 19 | public static readonly argParser: Parser = seq( 20 | whitespace.then(fileOptParser).fallback([]), 21 | optWhitespace 22 | .then( 23 | alt<{ cmd: string } | { file: string }>( 24 | string('!') 25 | .then(all) 26 | .map((cmd) => { 27 | return { cmd }; 28 | }), 29 | fileNameParser.map((file) => { 30 | return { file }; 31 | }), 32 | ), 33 | ) 34 | .fallback(undefined), 35 | ).map(([opt, other]) => new ReadCommand({ opt, ...other })); 36 | 37 | private readonly arguments: IReadCommandArguments; 38 | constructor(args: IReadCommandArguments) { 39 | super(); 40 | this.arguments = args; 41 | } 42 | 43 | public override neovimCapable(): boolean { 44 | return true; 45 | } 46 | 47 | async execute(vimState: VimState): Promise { 48 | const textToInsert = await this.getTextToInsert(vimState); 49 | if (textToInsert) { 50 | vimState.recordedState.transformer.insert( 51 | vimState.cursorStopPosition.getLineEnd(), 52 | '\n' + textToInsert, 53 | ); 54 | } 55 | } 56 | 57 | // TODO: executeWithRange() 58 | 59 | async getTextToInsert(vimState: VimState): Promise { 60 | if ('file' in this.arguments) { 61 | return readFileAsync(this.arguments.file, 'utf8'); 62 | } else if ('cmd' in this.arguments) { 63 | if (this.arguments.cmd.length > 0) { 64 | if (SUPPORT_READ_COMMAND) { 65 | const cmd = this.arguments.cmd; 66 | return new Promise(async (resolve, reject) => { 67 | const { exec } = await import('child_process'); 68 | exec(cmd, (err, stdout, stderr) => { 69 | if (err) { 70 | reject(err); 71 | } else { 72 | resolve(stdout); 73 | } 74 | }); 75 | }); 76 | } else { 77 | return ''; 78 | } 79 | } else { 80 | // TODO: error message? 81 | return ''; 82 | } 83 | } else { 84 | return vimState.document.getText(); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/cmd_line/commands/redo.ts: -------------------------------------------------------------------------------- 1 | import { VimState } from '../../state/vimState'; 2 | import { CommandRedo } from '../../actions/commands/actions'; 3 | import { Position } from 'vscode'; 4 | import { ExCommand } from '../../vimscript/exCommand'; 5 | import { optWhitespace, Parser } from 'parsimmon'; 6 | import { numberParser } from '../../vimscript/parserUtils'; 7 | 8 | // 9 | // Implements :red[o] 10 | // http://vimdoc.sourceforge.net/htmldoc/undo.html#redo 11 | // 12 | export class RedoCommand extends ExCommand { 13 | public static readonly argParser: Parser = optWhitespace 14 | .then(numberParser) 15 | .fallback(undefined) 16 | .map((count) => new RedoCommand(count)); 17 | 18 | private count?: number; 19 | private constructor(count?: number) { 20 | super(); 21 | this.count = count; 22 | } 23 | 24 | async execute(vimState: VimState): Promise { 25 | // TODO: Use `this.count` 26 | await new CommandRedo().exec(new Position(0, 0), vimState); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/cmd_line/commands/sh.ts: -------------------------------------------------------------------------------- 1 | import { window } from 'vscode'; 2 | import { VimState } from '../../state/vimState'; 3 | import { ExCommand } from '../../vimscript/exCommand'; 4 | 5 | export class ShCommand extends ExCommand { 6 | async execute(vimState: VimState): Promise { 7 | window.createTerminal().show(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/cmd_line/commands/shift.ts: -------------------------------------------------------------------------------- 1 | import { Position, Selection } from 'vscode'; 2 | 3 | // eslint-disable-next-line id-denylist 4 | import { optWhitespace, Parser, seq, string } from 'parsimmon'; 5 | import { PositionDiff } from '../../common/motion/position'; 6 | import { VimState } from '../../state/vimState'; 7 | import { ExCommand } from '../../vimscript/exCommand'; 8 | import { Address, LineRange } from '../../vimscript/lineRange'; 9 | import { numberParser } from '../../vimscript/parserUtils'; 10 | 11 | export type ShiftDirection = '>' | '<'; 12 | export type ShiftArgs = { 13 | dir: '>' | '<'; 14 | depth: number; 15 | numLines: number | undefined; 16 | }; 17 | 18 | export class ShiftCommand extends ExCommand { 19 | public static readonly argParser = (dir: '>' | '<'): Parser => 20 | optWhitespace 21 | .then( 22 | seq( 23 | // `:>>>` indents 3 times `shiftwidth` 24 | string(dir) 25 | .many() 26 | .map((shifts) => shifts.length + 1) 27 | .skip(optWhitespace), 28 | // `:> 2` indents 2 lines 29 | numberParser.fallback(undefined), 30 | ), 31 | ) 32 | .map(([depth, numLines]) => new ShiftCommand({ dir, depth, numLines })); 33 | 34 | private args: ShiftArgs; 35 | constructor(args: ShiftArgs) { 36 | super(); 37 | this.args = args; 38 | } 39 | 40 | public async execute(vimState: VimState): Promise { 41 | void this.executeWithRange(vimState, new LineRange(new Address({ type: 'current_line' }))); 42 | } 43 | 44 | public override async executeWithRange(vimState: VimState, range: LineRange): Promise { 45 | let { start, end } = range.resolve(vimState); 46 | if (this.args.numLines !== undefined) { 47 | start = end; 48 | end = start + this.args.numLines; 49 | } 50 | 51 | vimState.editor.selection = new Selection(new Position(start, 0), new Position(end, 0)); 52 | for (let i = 0; i < this.args.depth; i++) { 53 | if (this.args.dir === '>') { 54 | vimState.recordedState.transformer.vscodeCommand('editor.action.indentLines'); 55 | } else if (this.args.dir === '<') { 56 | vimState.recordedState.transformer.vscodeCommand('editor.action.outdentLines'); 57 | } 58 | } 59 | 60 | vimState.recordedState.transformer.moveCursor(PositionDiff.startOfLine()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/cmd_line/commands/smile.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { VimState } from '../../state/vimState'; 3 | import { TextEditor } from '../../textEditor'; 4 | import { ExCommand } from '../../vimscript/exCommand'; 5 | 6 | export class SmileCommand extends ExCommand { 7 | static readonly smileText: string = ` 8 | oooo$$$$$$$$$$$$oooo 9 | oo$$$$$$$$$$$$$$$$$$$$$$$$o 10 | oo$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o o$ $$ o$ 11 | o $ oo o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o $$ $$ $$o$ 12 | oo $ $ "$ o$$$$$$$$$ $$$$$$$$$$$$$ $$$$$$$$$o $$$o$$o$ 13 | "$$$$$$o$ o$$$$$$$$$ $$$$$$$$$$$ $$$$$$$$$$o $$$$$$$$ 14 | $$$$$$$ $$$$$$$$$$$ $$$$$$$$$$$ $$$$$$$$$$$$$$$$$$$$$$$ 15 | $$$$$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$ $$$$$$$$$$$$$$ """$$$ 16 | "$$$""""$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$ 17 | $$$ o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ "$$$o 18 | o$$" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$o 19 | $$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" "$$$$$$ooooo$$$$o 20 | o$$$oooo$$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ o$$$$$$$$$$$$$$$$$ 21 | $$$$$$$$"$$$$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $$$$"""""""" 22 | """" $$$$ "$$$$$$$$$$$$$$$$$$$$$$$$$$$$" o$$$ 23 | "$$$o """$$$$$$$$$$$$$$$$$$"$$" $$$ 24 | $$$o "$$""$$$$$$"""" o$$$ 25 | $$$$o o$$$" 26 | "$$$$o o$$$$$$o"$$$$o o$$$$ 27 | "$$$$$oo ""$$$$o$$$$$o o$$$$"" 28 | ""$$$$$oooo "$$$o$$$$$$$$$""" 29 | ""$$$$$$$oo $$$$$$$$$$ 30 | """"$$$$$$$$$$$ 31 | $$$$$$$$$$$$ 32 | $$$$$$$$$$" 33 | "$$$""""`; 34 | 35 | constructor() { 36 | super(); 37 | } 38 | 39 | async execute(vimState: VimState): Promise { 40 | await vscode.commands.executeCommand('workbench.action.files.newUntitledFile'); 41 | await TextEditor.insert(vscode.window.activeTextEditor!, SmileCommand.smileText); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/cmd_line/commands/terminal.ts: -------------------------------------------------------------------------------- 1 | import { Parser, succeed } from 'parsimmon'; 2 | import * as vscode from 'vscode'; 3 | import { VimState } from '../../state/vimState'; 4 | import { ExCommand } from '../../vimscript/exCommand'; 5 | 6 | export class TerminalCommand extends ExCommand { 7 | public static readonly argParser: Parser = succeed(new TerminalCommand()); 8 | 9 | async execute(vimState: VimState): Promise { 10 | await vscode.commands.executeCommand('workbench.action.createTerminalEditor'); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/cmd_line/commands/undo.ts: -------------------------------------------------------------------------------- 1 | import { VimState } from '../../state/vimState'; 2 | import { CommandUndo } from '../../actions/commands/actions'; 3 | import { Position } from 'vscode'; 4 | import { ExCommand } from '../../vimscript/exCommand'; 5 | import { optWhitespace, Parser } from 'parsimmon'; 6 | import { numberParser } from '../../vimscript/parserUtils'; 7 | 8 | // 9 | // Implements :u[ndo] 10 | // http://vimdoc.sourceforge.net/htmldoc/undo.html 11 | // 12 | export class UndoCommand extends ExCommand { 13 | public static readonly argParser: Parser = optWhitespace 14 | .then(numberParser) 15 | .fallback(undefined) 16 | .map((count) => new UndoCommand(count)); 17 | 18 | private count?: number; 19 | private constructor(count?: number) { 20 | super(); 21 | this.count = count; 22 | } 23 | 24 | async execute(vimState: VimState): Promise { 25 | // TODO: Use `this.count` 26 | await new CommandUndo().exec(new Position(0, 0), vimState); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/cmd_line/commands/vscode.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCode, VimError } from '../../error'; 2 | import { StatusBar } from '../../statusBar'; 3 | import * as vscode from 'vscode'; 4 | import { VimState } from '../../state/vimState'; 5 | import { ExCommand } from '../../vimscript/exCommand'; 6 | import { all, Parser, whitespace } from 'parsimmon'; 7 | 8 | export class VsCodeCommand extends ExCommand { 9 | public static readonly argParser: Parser = whitespace 10 | .then(all) 11 | .map((command) => new VsCodeCommand(command)); 12 | 13 | private command?: string; 14 | 15 | public constructor(command?: string) { 16 | super(); 17 | this.command = command; 18 | } 19 | 20 | async execute(vimState: VimState): Promise { 21 | if (!this.command) { 22 | StatusBar.displayError(vimState, VimError.fromCode(ErrorCode.ArgumentRequired)); 23 | return; 24 | } 25 | await vscode.commands.executeCommand(this.command); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/cmd_line/commands/wall.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from 'parsimmon'; 2 | import * as vscode from 'vscode'; 3 | import { VimState } from '../../state/vimState'; 4 | import { ExCommand } from '../../vimscript/exCommand'; 5 | import { bangParser } from '../../vimscript/parserUtils'; 6 | 7 | // 8 | // Implements :wall (write all) 9 | // http://vimdoc.sourceforge.net/htmldoc/editing.html#:wall 10 | // 11 | export class WallCommand extends ExCommand { 12 | public static readonly argParser: Parser = bangParser.map( 13 | (bang) => new WallCommand(bang), 14 | ); 15 | 16 | private readonly bang: boolean; 17 | constructor(bang?: boolean) { 18 | super(); 19 | this.bang = bang ?? false; 20 | } 21 | 22 | async execute(vimState: VimState): Promise { 23 | // TODO : overwrite readonly files when bang? == true 24 | await vscode.workspace.saveAll(false); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/cmd_line/commands/writequit.ts: -------------------------------------------------------------------------------- 1 | import { optWhitespace, Parser, regexp, seq } from 'parsimmon'; 2 | import { VimState } from '../../state/vimState'; 3 | import { ExCommand } from '../../vimscript/exCommand'; 4 | import { bangParser, fileNameParser, FileOpt, fileOptParser } from '../../vimscript/parserUtils'; 5 | import { QuitCommand } from './quit'; 6 | import { WriteCommand } from './write'; 7 | 8 | // 9 | // Implements :writequit 10 | // http://vimdoc.sourceforge.net/htmldoc/editing.html#write-quit 11 | // 12 | export interface IWriteQuitCommandArguments { 13 | bang: boolean; 14 | opt: FileOpt; 15 | file?: string; 16 | } 17 | 18 | export class WriteQuitCommand extends ExCommand { 19 | public static readonly argParser: Parser = seq( 20 | bangParser.skip(optWhitespace), 21 | fileOptParser.skip(optWhitespace), 22 | fileNameParser.fallback(undefined), 23 | ).map(([bang, opt, file]) => new WriteQuitCommand(file ? { bang, opt, file } : { bang, opt })); 24 | 25 | public override isRepeatableWithDot = false; 26 | 27 | private readonly args: IWriteQuitCommandArguments; 28 | constructor(args: IWriteQuitCommandArguments) { 29 | super(); 30 | this.args = args; 31 | } 32 | 33 | // Writing command. Taken as a basis from the "write.ts" file. 34 | async execute(vimState: VimState): Promise { 35 | await new WriteCommand({ bgWrite: false, ...this.args }).execute(vimState); 36 | 37 | await new QuitCommand({ 38 | // wq! fails when no file name is provided 39 | bang: false, 40 | }).execute(vimState); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/cmd_line/commands/writequitall.ts: -------------------------------------------------------------------------------- 1 | import { Parser, seq, whitespace } from 'parsimmon'; 2 | import { VimState } from '../../state/vimState'; 3 | import { ExCommand } from '../../vimscript/exCommand'; 4 | import { bangParser, FileOpt, fileOptParser } from '../../vimscript/parserUtils'; 5 | import * as wall from '../commands/wall'; 6 | import * as quit from './quit'; 7 | 8 | // 9 | // Implements :writequitall 10 | // http://vimdoc.sourceforge.net/htmldoc/editing.html#:wqall 11 | // 12 | export interface IWriteQuitAllCommandArguments { 13 | bang: boolean; 14 | fileOpt: FileOpt; 15 | } 16 | 17 | export class WriteQuitAllCommand extends ExCommand { 18 | public static readonly argParser: Parser = seq( 19 | bangParser, 20 | whitespace.then(fileOptParser).fallback([]), 21 | ).map(([bang, fileOpt]) => new WriteQuitAllCommand({ bang, fileOpt })); 22 | 23 | public override isRepeatableWithDot = false; 24 | 25 | private readonly arguments: IWriteQuitAllCommandArguments; 26 | constructor(args: IWriteQuitAllCommandArguments) { 27 | super(); 28 | this.arguments = args; 29 | } 30 | 31 | // Writing command. Taken as a basis from the "write.ts" file. 32 | async execute(vimState: VimState): Promise { 33 | const quitArgs: quit.IQuitCommandArguments = { 34 | // wq! fails when no file name is provided 35 | bang: false, 36 | }; 37 | 38 | const wallCmd = new wall.WallCommand(this.arguments.bang); 39 | await wallCmd.execute(vimState); 40 | 41 | // TODO: fileOpt is not used 42 | 43 | quitArgs.quitAll = true; 44 | const quitCmd = new quit.QuitCommand(quitArgs); 45 | await quitCmd.execute(vimState); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/cmd_line/commands/yank.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line id-denylist 2 | import { alt, any, optWhitespace, Parser, seq, whitespace } from 'parsimmon'; 3 | import { Position } from 'vscode'; 4 | import { YankOperator } from '../../actions/operator'; 5 | import { RegisterMode } from '../../register/register'; 6 | import { VimState } from '../../state/vimState'; 7 | import { ExCommand } from '../../vimscript/exCommand'; 8 | import { LineRange } from '../../vimscript/lineRange'; 9 | import { numberParser } from '../../vimscript/parserUtils'; 10 | 11 | export interface YankCommandArguments { 12 | register?: string; 13 | count?: number; 14 | } 15 | 16 | export class YankCommand extends ExCommand { 17 | public static readonly argParser: Parser = optWhitespace.then( 18 | alt( 19 | numberParser.map((count) => { 20 | return { register: undefined, count }; 21 | }), 22 | // eslint-disable-next-line id-denylist 23 | seq(any.fallback(undefined), whitespace.then(numberParser).fallback(undefined)).map( 24 | ([register, count]) => { 25 | return { register, count }; 26 | }, 27 | ), 28 | ).map( 29 | ({ register, count }) => 30 | new YankCommand({ 31 | register, 32 | count, 33 | }), 34 | ), 35 | ); 36 | 37 | private readonly arguments: YankCommandArguments; 38 | constructor(args: YankCommandArguments) { 39 | super(); 40 | this.arguments = args; 41 | } 42 | 43 | private async yank(vimState: VimState, start: Position, end: Position) { 44 | vimState.currentRegisterMode = RegisterMode.LineWise; 45 | if (this.arguments.register) { 46 | vimState.recordedState.registerName = this.arguments.register; 47 | } 48 | 49 | const cursorPosition = vimState.cursorStopPosition; 50 | 51 | await new YankOperator().run(vimState, start.getLineBegin(), end.getLineEnd()); 52 | 53 | // YankOperator moves the cursor - undo that 54 | vimState.cursorStopPosition = cursorPosition; 55 | } 56 | 57 | async execute(vimState: VimState): Promise { 58 | const linesToYank = this.arguments.count ?? 1; 59 | const startPosition = vimState.cursorStartPosition; 60 | const endPosition = linesToYank 61 | ? startPosition.getDown(linesToYank - 1).getLineEnd() 62 | : vimState.cursorStopPosition; 63 | await this.yank(vimState, startPosition, endPosition); 64 | } 65 | 66 | override async executeWithRange(vimState: VimState, range: LineRange): Promise { 67 | /** 68 | * If a [cnt] and [range] is specified (e.g. :.+2y3), :yank [cnt] is called from 69 | * the end of the [range]. 70 | * Ex. if two lines are VisualLine highlighted, :<,>y3 will :y3 71 | * from the end of the selected lines. 72 | */ 73 | const { start, end } = range.resolve(vimState); 74 | if (this.arguments.count) { 75 | vimState.cursorStartPosition = new Position(end, 0); 76 | await this.execute(vimState); 77 | return; 78 | } 79 | 80 | await this.yank(vimState, new Position(start, 0), new Position(end, 0)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/common/matching/quoteMatcher.ts: -------------------------------------------------------------------------------- 1 | enum QuoteMatch { 2 | Opening, 3 | Closing, 4 | } 5 | 6 | /** 7 | * QuoteMatcher matches quoted strings, respecting escaped quotes (\") and friends 8 | */ 9 | export class QuoteMatcher { 10 | static readonly escapeChar = '\\'; 11 | 12 | private readonly quoteMap: QuoteMatch[] = []; 13 | 14 | constructor(quote: '"' | "'" | '`', corpus: string) { 15 | let openingQuote = true; 16 | // Loop over corpus, marking quotes and respecting escape characters. 17 | for (let i = 0; i < corpus.length; i++) { 18 | if (corpus[i] === QuoteMatcher.escapeChar) { 19 | i += 1; 20 | continue; 21 | } 22 | if (corpus[i] === quote) { 23 | this.quoteMap[i] = openingQuote ? QuoteMatch.Opening : QuoteMatch.Closing; 24 | openingQuote = !openingQuote; 25 | } 26 | } 27 | } 28 | 29 | public surroundingQuotes(cursorIndex: number): [number, number] | undefined { 30 | const cursorQuoteType = this.quoteMap[cursorIndex]; 31 | if (cursorQuoteType === QuoteMatch.Opening) { 32 | const closing = this.getNextQuote(cursorIndex); 33 | return closing !== undefined ? [cursorIndex, closing] : undefined; 34 | } else if (cursorQuoteType === QuoteMatch.Closing) { 35 | return [this.getPrevQuote(cursorIndex)!, cursorIndex]; 36 | } else { 37 | const opening = this.getPrevQuote(cursorIndex) ?? this.getNextQuote(cursorIndex); 38 | 39 | if (opening !== undefined) { 40 | const closing = this.getNextQuote(opening); 41 | if (closing !== undefined) { 42 | return [opening, closing]; 43 | } 44 | } 45 | } 46 | 47 | return undefined; 48 | } 49 | 50 | private getNextQuote(start: number): number | undefined { 51 | for (let i = start + 1; i < this.quoteMap.length; i++) { 52 | if (this.quoteMap[i] !== undefined) { 53 | return i; 54 | } 55 | } 56 | 57 | return undefined; 58 | } 59 | 60 | private getPrevQuote(start: number): number | undefined { 61 | for (let i = start - 1; i >= 0; i--) { 62 | if (this.quoteMap[i] !== undefined) { 63 | return i; 64 | } 65 | } 66 | 67 | return undefined; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/common/motion/cursor.ts: -------------------------------------------------------------------------------- 1 | import { Position, Selection, TextEditor } from 'vscode'; 2 | 3 | export class Cursor { 4 | public readonly start: Position; 5 | public readonly stop: Position; 6 | 7 | constructor(start: Position, stop: Position) { 8 | this.start = start; 9 | this.stop = stop; 10 | } 11 | 12 | public isValid(textEditor: TextEditor) { 13 | return this.start.isValid(textEditor) && this.stop.isValid(textEditor); 14 | } 15 | 16 | /** 17 | * Create a Cursor from a VSCode selection. 18 | */ 19 | public static FromVSCodeSelection(sel: Selection): Cursor { 20 | return new Cursor(sel.start, sel.end); 21 | } 22 | 23 | public equals(other: Cursor): boolean { 24 | return this.start.isEqual(other.start) && this.stop.isEqual(other.stop); 25 | } 26 | 27 | /** 28 | * Returns a new Cursor which is the same as this Cursor, but with the provided stop value. 29 | */ 30 | public withNewStop(stop: Position): Cursor { 31 | return new Cursor(this.start, stop); 32 | } 33 | 34 | /** 35 | * Returns a new Cursor which is the same as this Cursor, but with the provided start value. 36 | */ 37 | public withNewStart(start: Position): Cursor { 38 | return new Cursor(start, this.stop); 39 | } 40 | 41 | public toString(): string { 42 | return `[${this.start.toString()} | ${this.stop.toString()}]`; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/configuration/configurationValidator.ts: -------------------------------------------------------------------------------- 1 | import { IConfiguration } from './iconfiguration'; 2 | import { IConfigurationValidator, ValidatorResults } from './iconfigurationValidator'; 3 | 4 | class ConfigurationValidator { 5 | private readonly validators: IConfigurationValidator[]; 6 | 7 | constructor() { 8 | this.validators = []; 9 | } 10 | 11 | public registerValidator(validator: IConfigurationValidator) { 12 | this.validators.push(validator); 13 | } 14 | 15 | public async validate(config: IConfiguration): Promise { 16 | const results = new ValidatorResults(); 17 | 18 | for (const validator of this.validators) { 19 | const validatorResults = await validator.validate(config); 20 | if (validatorResults.hasError) { 21 | // errors found in configuration, disable feature 22 | validator.disable(config); 23 | } 24 | 25 | results.concat(validatorResults); 26 | } 27 | 28 | return results; 29 | } 30 | } 31 | 32 | export const configurationValidator = new ConfigurationValidator(); 33 | -------------------------------------------------------------------------------- /src/configuration/iconfigurationValidator.ts: -------------------------------------------------------------------------------- 1 | import { IConfiguration } from './iconfiguration'; 2 | 3 | interface IValidatorResult { 4 | level: 'error' | 'warning'; 5 | message: string; 6 | } 7 | 8 | export class ValidatorResults { 9 | errors = new Array(); 10 | 11 | public append(validationResult: IValidatorResult) { 12 | this.errors.push(validationResult); 13 | } 14 | 15 | public concat(validationResults: ValidatorResults) { 16 | this.errors = this.errors.concat(validationResults.get()); 17 | } 18 | 19 | public get(): readonly IValidatorResult[] { 20 | return this.errors; 21 | } 22 | 23 | public get numErrors(): number { 24 | return this.errors.filter((e) => e.level === 'error').length; 25 | } 26 | 27 | public get hasError(): boolean { 28 | return this.numErrors > 0; 29 | } 30 | 31 | public get numWarnings(): number { 32 | return this.errors.filter((e) => e.level === 'warning').length; 33 | } 34 | 35 | public get hasWarning(): boolean { 36 | return this.numWarnings > 0; 37 | } 38 | } 39 | 40 | export interface IConfigurationValidator { 41 | validate(config: IConfiguration): Promise; 42 | disable(config: IConfiguration): void; 43 | } 44 | -------------------------------------------------------------------------------- /src/configuration/notation.ts: -------------------------------------------------------------------------------- 1 | export class Notation { 2 | // Mapping from a regex to the normalized string that it should be converted to. 3 | private static readonly notationMap: ReadonlyArray<[RegExp, string]> = [ 4 | [/ctrl\+|c\-/gi, 'C-'], 5 | [/cmd\+|d\-/gi, 'D-'], 6 | [/shift\+|s\-/gi, 'S-'], 7 | [/escape|esc/gi, 'Esc'], 8 | [/backspace|bs/gi, 'BS'], 9 | [/delete|del/gi, 'Del'], 10 | [/home/gi, 'Home'], 11 | [/end/gi, 'End'], 12 | [/insert/gi, 'Insert'], 13 | [//gi, ' '], 14 | [/||/gi, '\n'], 15 | ]; 16 | 17 | private static shiftedLetterRegex = //; 18 | 19 | /** 20 | * Converts keystroke like to a single control character like \t 21 | */ 22 | public static ToControlCharacter(key: string) { 23 | if (key === '') { 24 | return '\t'; 25 | } 26 | 27 | return key; 28 | } 29 | 30 | public static IsControlKey(key: string): boolean { 31 | key = key.toLocaleUpperCase(); 32 | return ( 33 | this.isSurroundedByAngleBrackets(key) && key !== '' && key !== '' && key !== '' 34 | ); 35 | } 36 | 37 | /** 38 | * Normalizes key to AngleBracketNotation 39 | * (e.g. , Ctrl+x, normalized to ) 40 | * and converts the characters to their literals 41 | * (e.g. , , ) 42 | */ 43 | public static NormalizeKey(key: string, leaderKey: string): string { 44 | if (typeof key !== 'string') { 45 | return key; 46 | } 47 | 48 | if (key.length === 1) { 49 | return key; 50 | } 51 | 52 | key = key.toLocaleLowerCase(); 53 | 54 | if (!this.isSurroundedByAngleBrackets(key)) { 55 | key = `<${key}>`; 56 | } 57 | 58 | if (key === '') { 59 | return leaderKey; 60 | } 61 | 62 | if (['', '', '', ''].includes(key)) { 63 | return key; 64 | } 65 | 66 | for (const [regex, standardNotation] of this.notationMap) { 67 | key = key.replace(regex, standardNotation); 68 | } 69 | 70 | if (this.shiftedLetterRegex.test(key)) { 71 | key = key[3].toUpperCase(); 72 | } 73 | 74 | return key; 75 | } 76 | 77 | /** 78 | * Converts a key to a form which will look nice when logged, etc. 79 | */ 80 | public static printableKey(key: string, leaderKey: string) { 81 | const normalized = this.NormalizeKey(key, leaderKey); 82 | return normalized === ' ' ? '' : normalized === '\n' ? '' : normalized; 83 | } 84 | 85 | private static isSurroundedByAngleBrackets(key: string): boolean { 86 | return key.startsWith('<') && key.endsWith('>'); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/configuration/validators/inputMethodSwitcherValidator.ts: -------------------------------------------------------------------------------- 1 | import { IConfigurationValidator, ValidatorResults } from '../iconfigurationValidator'; 2 | import { IConfiguration } from '../iconfiguration'; 3 | import { existsAsync } from 'platform/fs'; 4 | import { Globals } from '../../globals'; 5 | import { configurationValidator } from '../configurationValidator'; 6 | 7 | export class InputMethodSwitcherConfigurationValidator implements IConfigurationValidator { 8 | async validate(config: IConfiguration): Promise { 9 | const result = new ValidatorResults(); 10 | 11 | const inputMethodConfig = config.autoSwitchInputMethod; 12 | 13 | if (!inputMethodConfig.enable || Globals.isTesting) { 14 | return Promise.resolve(result); 15 | } 16 | 17 | if (!inputMethodConfig.switchIMCmd.includes('{im}')) { 18 | result.append({ 19 | level: 'error', 20 | message: 21 | 'vim.autoSwitchInputMethod.switchIMCmd is incorrect, it should contain the placeholder {im}.', 22 | }); 23 | } 24 | 25 | if (inputMethodConfig.obtainIMCmd === undefined || inputMethodConfig.obtainIMCmd === '') { 26 | result.append({ 27 | level: 'error', 28 | message: 'vim.autoSwitchInputMethod.obtainIMCmd is empty.', 29 | }); 30 | } else if (!(await existsAsync(this.getRawCmd(inputMethodConfig.obtainIMCmd)))) { 31 | result.append({ 32 | level: 'error', 33 | message: `Unable to find ${inputMethodConfig.obtainIMCmd}. Check your 'vim.autoSwitchInputMethod.obtainIMCmd' in VSCode setting.`, 34 | }); 35 | } 36 | 37 | if (inputMethodConfig.defaultIM === undefined || inputMethodConfig.defaultIM === '') { 38 | result.append({ 39 | level: 'error', 40 | message: 'vim.autoSwitchInputMethod.defaultIM is empty.', 41 | }); 42 | } else if (!(await existsAsync(this.getRawCmd(inputMethodConfig.switchIMCmd)))) { 43 | result.append({ 44 | level: 'error', 45 | message: `Unable to find ${inputMethodConfig.switchIMCmd}. Check your 'vim.autoSwitchInputMethod.switchIMCmd' in VSCode setting.`, 46 | }); 47 | } 48 | 49 | return Promise.resolve(result); 50 | } 51 | 52 | disable(config: IConfiguration) { 53 | config.autoSwitchInputMethod.enable = false; 54 | } 55 | 56 | private getRawCmd(cmd: string): string { 57 | return cmd.split(' ')[0]; 58 | } 59 | } 60 | 61 | configurationValidator.registerValidator(new InputMethodSwitcherConfigurationValidator()); 62 | -------------------------------------------------------------------------------- /src/configuration/validators/neovimValidator.ts: -------------------------------------------------------------------------------- 1 | import { execFileSync } from 'child_process'; 2 | import { existsSync } from 'fs'; 3 | import * as path from 'path'; 4 | import * as process from 'process'; 5 | import { configurationValidator } from '../configurationValidator'; 6 | import { IConfiguration } from '../iconfiguration'; 7 | import { IConfigurationValidator, ValidatorResults } from '../iconfigurationValidator'; 8 | 9 | export class NeovimValidator implements IConfigurationValidator { 10 | validate(config: IConfiguration): Promise { 11 | const result = new ValidatorResults(); 12 | 13 | if (config.enableNeovim) { 14 | let triedToParsePath = false; 15 | try { 16 | // Try to find nvim in path if it is not defined 17 | if (config.neovimPath === '') { 18 | const pathVar = process.env.PATH; 19 | if (pathVar) { 20 | pathVar.split(path.delimiter).forEach((element) => { 21 | let neovimExecutable = 'nvim'; 22 | if (process.platform === 'win32') { 23 | neovimExecutable += '.exe'; 24 | } 25 | const testPath = path.join(element, neovimExecutable); 26 | if (existsSync(testPath)) { 27 | config.neovimPath = testPath; 28 | triedToParsePath = true; 29 | return; 30 | } 31 | }); 32 | } 33 | } 34 | execFileSync(config.neovimPath, ['--version']); 35 | } catch (e) { 36 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 37 | let errorMessage = `Invalid neovimPath. ${e.message}.`; 38 | if (triedToParsePath) { 39 | errorMessage += `Tried to parse PATH ${config.neovimPath}.`; 40 | } 41 | result.append({ 42 | level: 'error', 43 | message: errorMessage, 44 | }); 45 | } 46 | // If Neovim config path doesn't exist, default to empty config path. 47 | if (config.neovimUseConfigFile && config.neovimConfigPath !== '') { 48 | if (!existsSync(config.neovimConfigPath)) { 49 | const warningMessage = `No config file found in neovimConfigPath. Neovim will search its default config path.`; 50 | config.neovimConfigPath = ''; 51 | result.append({ 52 | level: 'warning', 53 | message: warningMessage, 54 | }); 55 | } 56 | } 57 | } 58 | 59 | return Promise.resolve(result); 60 | } 61 | 62 | disable(config: IConfiguration) { 63 | config.enableNeovim = false; 64 | } 65 | } 66 | 67 | configurationValidator.registerValidator(new NeovimValidator()); 68 | -------------------------------------------------------------------------------- /src/configuration/validators/vimrcValidator.ts: -------------------------------------------------------------------------------- 1 | import { IConfiguration } from '../iconfiguration'; 2 | import { IConfigurationValidator, ValidatorResults } from '../iconfigurationValidator'; 3 | import { configurationValidator } from '../configurationValidator'; 4 | 5 | export class VimrcValidator implements IConfigurationValidator { 6 | async validate(config: IConfiguration): Promise { 7 | const result = new ValidatorResults(); 8 | 9 | // if (config.vimrc.enable && !fs.existsSync(vimrc.vimrcPath)) { 10 | // result.append({ 11 | // level: 'error', 12 | // message: `.vimrc not found at ${config.vimrc.path}`, 13 | // }); 14 | // } 15 | 16 | return result; 17 | } 18 | 19 | disable(config: IConfiguration): void { 20 | // no-op 21 | } 22 | } 23 | 24 | configurationValidator.registerValidator(new VimrcValidator()); 25 | -------------------------------------------------------------------------------- /src/globals.ts: -------------------------------------------------------------------------------- 1 | import { IConfiguration } from './configuration/iconfiguration'; 2 | 3 | /** 4 | * Global variables shared throughout extension 5 | */ 6 | export class Globals { 7 | /** 8 | * This is where we put files like HistoryFile. The path is given to us by VSCode. 9 | */ 10 | static extensionStoragePath: string; 11 | 12 | /** 13 | * Used for testing. 14 | */ 15 | static isTesting = false; 16 | static mockConfiguration: IConfiguration; 17 | } 18 | -------------------------------------------------------------------------------- /src/history/historyFile.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext } from 'vscode'; 2 | import { configuration } from '../configuration/configuration'; 3 | import { Globals } from '../globals'; 4 | import { HistoryBase } from 'platform/history'; 5 | 6 | // TODO(jfields): What's going on here? Just combine HistoryFile and HistoryBase... 7 | export class HistoryFile { 8 | private base: HistoryBase; 9 | 10 | get historyFilePath(): string { 11 | return this.base.historyKey; 12 | } 13 | 14 | constructor(context: ExtensionContext, historyFileName: string) { 15 | this.base = new HistoryBase(context, historyFileName, Globals.extensionStoragePath); 16 | } 17 | 18 | public async add(value: string | undefined): Promise { 19 | return this.base.add(value, configuration.history); 20 | } 21 | 22 | public get(): string[] { 23 | return this.base.get(configuration.history); 24 | } 25 | 26 | public clear() { 27 | this.base.clear(); 28 | } 29 | 30 | public async load(): Promise { 31 | await this.base.load(); 32 | } 33 | } 34 | 35 | export class SearchHistory extends HistoryFile { 36 | constructor(context: ExtensionContext) { 37 | super(context, '.search_history'); 38 | } 39 | } 40 | 41 | export class CommandLineHistory extends HistoryFile { 42 | constructor(context: ExtensionContext) { 43 | super(context, '.cmdline_history'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/jumps/jump.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Position } from 'vscode'; 3 | 4 | import { VimState } from '../state/vimState'; 5 | 6 | /** 7 | * Represents a Jump in the JumpTracker. 8 | * Includes information necessary to determine jump actions, 9 | * and to be able to open the related file. 10 | */ 11 | export class Jump { 12 | public readonly document: vscode.TextDocument; 13 | public readonly position: Position; 14 | 15 | /** 16 | * 17 | * @param options 18 | * @param options.editor - The editor associated with the jump. 19 | * @param options.position - The line and column number information. 20 | */ 21 | constructor({ document, position }: { document: vscode.TextDocument; position: Position }) { 22 | this.document = document; 23 | this.position = position; 24 | } 25 | 26 | public get fileName() { 27 | return this.document.fileName; 28 | } 29 | 30 | /** 31 | * Factory method for creating a Jump from a VimState's current cursor position. 32 | * @param vimState - State that contains the fileName and position for the jump 33 | */ 34 | public static fromStateNow(vimState: VimState) { 35 | return new Jump({ 36 | document: vimState.document, 37 | position: vimState.cursorStopPosition, 38 | }); 39 | } 40 | 41 | /** 42 | * Factory method for creating a Jump from a VimState's cursor position, 43 | * before any actions or commands were performed. 44 | * @param vimState - State that contains the fileName and prior position for the jump 45 | */ 46 | public static fromStateBefore(vimState: VimState) { 47 | return new Jump({ 48 | document: vimState.document, 49 | position: vimState.cursorsInitialState[0].stop, 50 | }); 51 | } 52 | 53 | /** 54 | * Determine whether another jump matches the same file path, line number, and character column. 55 | * @param other - Another Jump to compare against 56 | */ 57 | public isSamePosition(other: Jump): boolean { 58 | return this.fileName === other.fileName && this.position.isEqual(other.position); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/mode/mode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Position } from 'vscode'; 3 | 4 | export enum Mode { 5 | Normal, 6 | Insert, 7 | Visual, 8 | VisualBlock, 9 | VisualLine, 10 | SearchInProgressMode, 11 | CommandlineInProgress, 12 | Replace, 13 | EasyMotionMode, 14 | EasyMotionInputMode, 15 | SurroundInputMode, 16 | OperatorPendingMode, // Pseudo-Mode, used only when remapping. DON'T SET TO THIS MODE 17 | Disabled, 18 | } 19 | 20 | export enum VSCodeVimCursorType { 21 | Block, 22 | Line, 23 | LineThin, 24 | Underline, 25 | TextDecoration, 26 | Native, 27 | UnderlineThin, 28 | } 29 | 30 | export enum NormalCommandState { 31 | Waiting, 32 | Executing, 33 | Finished, 34 | } 35 | 36 | export enum DotCommandStatus { 37 | Waiting, 38 | Executing, 39 | Finished, 40 | } 41 | 42 | export enum ReplayMode { 43 | Insert, 44 | Replace, 45 | } 46 | 47 | /** 48 | * Is the given mode visual, visual line, or visual block? 49 | */ 50 | export function isVisualMode(mode: Mode): mode is Mode.Visual | Mode.VisualLine | Mode.VisualBlock { 51 | return [Mode.Visual, Mode.VisualLine, Mode.VisualBlock].includes(mode); 52 | } 53 | 54 | /** 55 | * Is the given mode one where the cursor is on the status bar? 56 | * This means SearchInProgess and CommandlineInProgress modes. 57 | */ 58 | export function isStatusBarMode( 59 | mode: Mode, 60 | ): mode is Mode.CommandlineInProgress | Mode.SearchInProgressMode { 61 | return [Mode.SearchInProgressMode, Mode.CommandlineInProgress].includes(mode); 62 | } 63 | 64 | export function getCursorStyle(cursorType: VSCodeVimCursorType) { 65 | switch (cursorType) { 66 | case VSCodeVimCursorType.Block: 67 | return vscode.TextEditorCursorStyle.Block; 68 | case VSCodeVimCursorType.Line: 69 | return vscode.TextEditorCursorStyle.Line; 70 | case VSCodeVimCursorType.LineThin: 71 | return vscode.TextEditorCursorStyle.LineThin; 72 | case VSCodeVimCursorType.Underline: 73 | return vscode.TextEditorCursorStyle.Underline; 74 | case VSCodeVimCursorType.UnderlineThin: 75 | return vscode.TextEditorCursorStyle.UnderlineThin; 76 | case VSCodeVimCursorType.TextDecoration: 77 | return vscode.TextEditorCursorStyle.LineThin; 78 | case VSCodeVimCursorType.Native: 79 | default: 80 | return vscode.TextEditorCursorStyle.Block; 81 | } 82 | } 83 | 84 | export function visualBlockGetTopLeftPosition(start: Position, stop: Position): Position { 85 | return new Position(Math.min(start.line, stop.line), Math.min(start.character, stop.character)); 86 | } 87 | 88 | export function visualBlockGetBottomRightPosition(start: Position, stop: Position): Position { 89 | return new Position(Math.max(start.line, stop.line), Math.max(start.character, stop.character)); 90 | } 91 | -------------------------------------------------------------------------------- /src/mode/modeData.ts: -------------------------------------------------------------------------------- 1 | import { ExCommandLine, SearchCommandLine } from '../cmd_line/commandLine'; 2 | import { ReplaceState } from '../state/replaceState'; 3 | import { Mode } from './mode'; 4 | 5 | /** Modes which have no extra associated data. */ 6 | export type SimpleMode = Exclude< 7 | Mode, 8 | Mode.Replace | Mode.SearchInProgressMode | Mode.CommandlineInProgress | Mode.Insert 9 | >; 10 | 11 | /** State associated with the current mode. */ 12 | export type ModeData = 13 | | { 14 | mode: Mode.Replace; 15 | replaceState: ReplaceState; 16 | } 17 | | { 18 | mode: Mode.CommandlineInProgress; 19 | commandLine: ExCommandLine; 20 | } 21 | | { 22 | mode: Mode.SearchInProgressMode; 23 | commandLine: SearchCommandLine; 24 | /** The first line number that was visible when SearchInProgressMode began */ 25 | firstVisibleLineBeforeSearch: number; 26 | } 27 | | { 28 | mode: Mode.Insert; 29 | /** The high surrogate of an incomplete pair */ 30 | highSurrogate: string | undefined; 31 | } 32 | | { 33 | mode: SimpleMode; 34 | }; 35 | 36 | export type ModeDataFor = { mode: T } & ModeData; 37 | -------------------------------------------------------------------------------- /src/mode/modeHandlerMap.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor, Uri } from 'vscode'; 2 | import { ModeHandler } from './modeHandler'; 3 | 4 | /** 5 | * Stores one ModeHandler (and therefore VimState) per TextDocument. 6 | */ 7 | class ModeHandlerMapImpl { 8 | private modeHandlerMap = new Map(); 9 | 10 | public async getOrCreate(editor: TextEditor): Promise<[ModeHandler, boolean]> { 11 | const editorId = editor.document.uri; 12 | 13 | let isNew = false; 14 | let modeHandler: ModeHandler | undefined = this.get(editorId); 15 | 16 | if (!modeHandler) { 17 | isNew = true; 18 | modeHandler = await ModeHandler.create(this, editor); 19 | this.modeHandlerMap.set(editorId, modeHandler); 20 | } 21 | return [modeHandler, isNew]; 22 | } 23 | 24 | public get(uri: Uri): ModeHandler | undefined { 25 | return this.modeHandlerMap.get(uri); 26 | } 27 | 28 | public entries(): IterableIterator<[Uri, ModeHandler]> { 29 | return this.modeHandlerMap.entries(); 30 | } 31 | 32 | public delete(editorId: Uri) { 33 | const modeHandler = this.modeHandlerMap.get(editorId); 34 | if (modeHandler) { 35 | modeHandler.dispose(); 36 | this.modeHandlerMap.delete(editorId); 37 | } 38 | } 39 | 40 | public clear() { 41 | for (const key of this.modeHandlerMap.keys()) { 42 | this.delete(key); 43 | } 44 | } 45 | } 46 | 47 | export const ModeHandlerMap = new ModeHandlerMapImpl(); 48 | -------------------------------------------------------------------------------- /src/platform/browser/constants.ts: -------------------------------------------------------------------------------- 1 | export const SUPPORT_VIMRC = false; 2 | export const SUPPORT_NVIM = false; 3 | export const SUPPORT_IME_SWITCHER = false; 4 | export const SUPPORT_READ_COMMAND = false; 5 | -------------------------------------------------------------------------------- /src/platform/browser/fs.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export const constants = { 4 | UV_FS_SYMLINK_DIR: 1, 5 | UV_FS_SYMLINK_JUNCTION: 2, 6 | O_RDONLY: 0, 7 | O_WRONLY: 1, 8 | O_RDWR: 2, 9 | UV_DIRENT_UNKNOWN: 0, 10 | UV_DIRENT_FILE: 1, 11 | UV_DIRENT_DIR: 2, 12 | UV_DIRENT_LINK: 3, 13 | UV_DIRENT_FIFO: 4, 14 | UV_DIRENT_SOCKET: 5, 15 | UV_DIRENT_CHAR: 6, 16 | UV_DIRENT_BLOCK: 7, 17 | S_IFMT: 61440, 18 | S_IFREG: 32768, 19 | S_IFDIR: 16384, 20 | S_IFCHR: 8192, 21 | S_IFBLK: 24576, 22 | S_IFIFO: 4096, 23 | S_IFLNK: 40960, 24 | S_IFSOCK: 49152, 25 | O_CREAT: 512, 26 | O_EXCL: 2048, 27 | UV_FS_O_FILEMAP: 0, 28 | O_NOCTTY: 131072, 29 | O_TRUNC: 1024, 30 | O_APPEND: 8, 31 | O_DIRECTORY: 1048576, 32 | O_NOFOLLOW: 256, 33 | O_SYNC: 128, 34 | O_DSYNC: 4194304, 35 | O_SYMLINK: 2097152, 36 | O_NONBLOCK: 4, 37 | S_IRWXU: 448, 38 | S_IRUSR: 256, 39 | S_IWUSR: 128, 40 | S_IXUSR: 64, 41 | S_IRWXG: 56, 42 | S_IRGRP: 32, 43 | S_IWGRP: 16, 44 | S_IXGRP: 8, 45 | S_IRWXO: 7, 46 | S_IROTH: 4, 47 | S_IWOTH: 2, 48 | S_IXOTH: 1, 49 | F_OK: 0, 50 | R_OK: 4, 51 | W_OK: 2, 52 | X_OK: 1, 53 | UV_FS_COPYFILE_EXCL: 1, 54 | COPYFILE_EXCL: 1, 55 | UV_FS_COPYFILE_FICLONE: 2, 56 | COPYFILE_FICLONE: 2, 57 | UV_FS_COPYFILE_FICLONE_FORCE: 4, 58 | COPYFILE_FICLONE_FORCE: 4, 59 | }; 60 | 61 | export async function doesFileExist(fileUri: vscode.Uri) { 62 | try { 63 | await vscode.workspace.fs.stat(fileUri); 64 | return true; 65 | } catch { 66 | return false; 67 | } 68 | } 69 | 70 | export async function existsAsync(path: string): Promise { 71 | try { 72 | await vscode.workspace.fs.stat(vscode.Uri.parse(path)); 73 | return true; 74 | } catch (_e) { 75 | return false; 76 | } 77 | } 78 | 79 | export async function unlink(path: string): Promise { 80 | await vscode.workspace.fs.delete(vscode.Uri.parse(path)); 81 | } 82 | 83 | export async function readFileAsync(path: string, encoding: BufferEncoding): Promise { 84 | const ret = await vscode.workspace.fs.readFile(vscode.Uri.parse(path)); 85 | return ret.toString(); 86 | } 87 | 88 | export async function mkdirAsync(path: string, options: any): Promise { 89 | return vscode.workspace.fs.createDirectory(vscode.Uri.parse(path)); 90 | } 91 | 92 | export async function writeFileAsync( 93 | path: string, 94 | content: string, 95 | encoding: BufferEncoding, 96 | ): Promise { 97 | return vscode.workspace.fs.writeFile(vscode.Uri.parse(path), Buffer.from(content)); 98 | } 99 | 100 | export async function accessAsync(path: string, mode: number) { 101 | // no op in nodeless 102 | } 103 | 104 | export async function chmodAsync(path: string, mode: string | number) { 105 | // no op in nodeless 106 | } 107 | 108 | export function unlinkSync(path: string) { 109 | // no op in nodeless 110 | } 111 | -------------------------------------------------------------------------------- /src/platform/browser/history.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export class HistoryBase { 4 | private readonly context: vscode.ExtensionContext; 5 | private readonly historyFileName: string; 6 | private history: string[] = []; 7 | 8 | get historyKey(): string { 9 | return `vim.${this.historyFileName}`; 10 | } 11 | 12 | constructor( 13 | context: vscode.ExtensionContext, 14 | historyFileName: string, 15 | extensionStoragePath: string, 16 | ) { 17 | this.context = context; 18 | this.historyFileName = historyFileName; 19 | } 20 | 21 | public async add(value: string | undefined, history: number): Promise { 22 | if (!value || value.length === 0) { 23 | return; 24 | } 25 | 26 | // remove duplicates 27 | const index: number = this.history.indexOf(value); 28 | if (index !== -1) { 29 | this.history.splice(index, 1); 30 | } 31 | 32 | // append to the end 33 | this.history.push(value); 34 | 35 | // resize array if necessary 36 | if (this.history.length > history) { 37 | this.history = this.history.slice(this.history.length - history); 38 | } 39 | 40 | return this.save(); 41 | } 42 | 43 | public get(history: number): string[] { 44 | // resize array if necessary 45 | if (this.history.length > history) { 46 | this.history = this.history.slice(this.history.length - history); 47 | } 48 | 49 | return this.history; 50 | } 51 | 52 | public async clear() { 53 | void this.context.workspaceState.update(this.historyKey, undefined); 54 | this.history = []; 55 | } 56 | 57 | public async load(): Promise { 58 | const data = this.context.workspaceState.get(this.historyKey) || ''; 59 | if (data.length === 0) { 60 | return; 61 | } 62 | 63 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 64 | const parsedData = JSON.parse(data); 65 | if (!Array.isArray(parsedData)) { 66 | throw Error('Unexpected format in history. Expected JSON.'); 67 | } 68 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 69 | this.history = parsedData; 70 | } 71 | 72 | async save(): Promise { 73 | void this.context.workspaceState.update(this.historyKey, JSON.stringify(this.history)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/platform/node/constants.ts: -------------------------------------------------------------------------------- 1 | export const SUPPORT_VIMRC = true; 2 | export const SUPPORT_NVIM = true; 3 | export const SUPPORT_IME_SWITCHER = true; 4 | export const SUPPORT_READ_COMMAND = true; 5 | -------------------------------------------------------------------------------- /src/state/compositionState.ts: -------------------------------------------------------------------------------- 1 | export class CompositionState { 2 | isInComposition: boolean = false; 3 | insertedText: boolean = false; 4 | composingText: string = ''; 5 | 6 | reset() { 7 | this.isInComposition = false; 8 | this.insertedText = false; 9 | this.composingText = ''; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/state/globalState.ts: -------------------------------------------------------------------------------- 1 | import { JumpTracker } from '../jumps/jumpTracker'; 2 | import { RecordedState } from './../state/recordedState'; 3 | import { SearchState } from './searchState'; 4 | import { SubstituteState } from './substituteState'; 5 | 6 | /** 7 | * State which stores global state (across editors) 8 | */ 9 | class GlobalState { 10 | /** 11 | * Track jumps, and traverse jump history 12 | */ 13 | public readonly jumpTracker: JumpTracker = new JumpTracker(); 14 | 15 | /** 16 | * The keystroke sequence that made up our last complete action (that can be 17 | * repeated with '.'). 18 | */ 19 | public previousFullAction: RecordedState | undefined = undefined; 20 | 21 | public lastInvokedMacro: RecordedState | undefined = undefined; 22 | 23 | /** 24 | * Last substitute state for running :s by itself 25 | */ 26 | public substituteState: SubstituteState | undefined = undefined; 27 | 28 | /** 29 | * The most recently active SearchState 30 | * This is used for things like `n` and `hlsearch` 31 | */ 32 | public searchState: SearchState | undefined = undefined; 33 | 34 | /** 35 | * Used internally for nohl. 36 | */ 37 | public hl = true; 38 | } 39 | 40 | export const globalState = new GlobalState(); 41 | -------------------------------------------------------------------------------- /src/state/replaceState.ts: -------------------------------------------------------------------------------- 1 | import { Position } from 'vscode'; 2 | 3 | type ReplaceModeChange = { 4 | before: string; 5 | after: string; 6 | }; 7 | 8 | /** 9 | * State involved with entering Replace mode (R). 10 | */ 11 | export class ReplaceState { 12 | /** 13 | * Number of times we're going to repeat this replace action. 14 | * Comes from the count applied to the `R` command. 15 | */ 16 | public readonly timesToRepeat: number; 17 | 18 | private _changes: ReplaceModeChange[][]; 19 | public getChanges(cursorIdx: number): ReplaceModeChange[] { 20 | if (this._changes[cursorIdx] === undefined) { 21 | this._changes[cursorIdx] = []; 22 | } 23 | return this._changes[cursorIdx]; 24 | } 25 | public resetChanges(cursorIdx: number) { 26 | this._changes[cursorIdx] = []; 27 | } 28 | 29 | constructor(startPositions: Position[], timesToRepeat: number = 1) { 30 | this.timesToRepeat = timesToRepeat; 31 | this._changes = startPositions.map((pos) => []); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/state/substituteState.ts: -------------------------------------------------------------------------------- 1 | import { ReplaceString } from '../cmd_line/commands/substitute'; 2 | import { Pattern } from '../vimscript/pattern'; 3 | 4 | /** 5 | * State involved with Substitution commands (:s). 6 | */ 7 | export class SubstituteState { 8 | /** 9 | * The last pattern searched for in the substitution 10 | */ 11 | public searchPattern: Pattern | undefined; 12 | 13 | /** 14 | * The last replacement string in the substitution 15 | */ 16 | public replaceString: ReplaceString; 17 | 18 | constructor(searchPattern: Pattern | undefined, replaceString: ReplaceString) { 19 | this.searchPattern = searchPattern; 20 | this.replaceString = replaceString; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/taskQueue.ts: -------------------------------------------------------------------------------- 1 | import Queue from 'queue'; 2 | import { Logger } from './util/logger'; 3 | 4 | class TaskQueue { 5 | private readonly taskQueue = new Queue({ autostart: true, concurrency: 1 }); 6 | 7 | constructor() { 8 | this.taskQueue.addListener('error', (err, task) => { 9 | // TODO: Report via telemetry API? 10 | Logger.error(`Error running task: ${err}`); 11 | }); 12 | } 13 | 14 | /** 15 | * Adds a task to the task queue. 16 | */ 17 | public enqueueTask(task: () => Promise): void { 18 | this.taskQueue.push(task); 19 | } 20 | } 21 | 22 | export const taskQueue = new TaskQueue(); 23 | -------------------------------------------------------------------------------- /src/textobject/paragraph.ts: -------------------------------------------------------------------------------- 1 | import { Position } from 'vscode'; 2 | import { TextEditor } from '../textEditor'; 3 | 4 | /** 5 | * Get the end of the current paragraph. 6 | */ 7 | export function getCurrentParagraphEnd(pos: Position, trimWhite: boolean = false): Position { 8 | const lastLine = TextEditor.getLineCount() - 1; 9 | 10 | let line = pos.line; 11 | 12 | // If we're not in a paragraph yet, go down until we are. 13 | while (line < lastLine && isLineBlank(line, trimWhite)) { 14 | line++; 15 | } 16 | 17 | // Go until we're outside of the paragraph, or at the end of the document. 18 | while (line < lastLine && !isLineBlank(line, trimWhite)) { 19 | line++; 20 | } 21 | 22 | return pos.with({ line }).getLineEnd(); 23 | } 24 | 25 | /** 26 | * Get the beginning of the current paragraph. 27 | */ 28 | export function getCurrentParagraphBeginning(pos: Position, trimWhite: boolean = false): Position { 29 | let line = pos.line; 30 | 31 | // If we're not in a paragraph yet, go up until we are. 32 | while (line > 0 && isLineBlank(line, trimWhite)) { 33 | line--; 34 | } 35 | 36 | // Go until we're outside of the paragraph, or at the beginning of the document. 37 | while (line > 0 && !isLineBlank(line, trimWhite)) { 38 | line--; 39 | } 40 | 41 | return new Position(line, 0); 42 | } 43 | 44 | function isLineBlank(line: number, trimWhite: boolean = false): boolean { 45 | const text = TextEditor.getLine(line).text; 46 | return (trimWhite ? text.trim() : text) === ''; 47 | } 48 | -------------------------------------------------------------------------------- /src/textobject/util.ts: -------------------------------------------------------------------------------- 1 | export function getAllPositions(line: string, regex: RegExp): number[] { 2 | const positions: number[] = []; 3 | let result = regex.exec(line); 4 | 5 | while (result) { 6 | positions.push(result.index); 7 | 8 | // Handles the case where an empty string match causes lastIndex not to advance, 9 | // which gets us in an infinite loop. 10 | if (result.index === regex.lastIndex) { 11 | regex.lastIndex++; 12 | } 13 | result = regex.exec(line); 14 | } 15 | 16 | return positions; 17 | } 18 | 19 | export function getAllEndPositions(line: string, regex: RegExp): number[] { 20 | const positions: number[] = []; 21 | let result = regex.exec(line); 22 | 23 | while (result) { 24 | if (result[0].length) { 25 | positions.push(result.index + result[0].length - 1); 26 | } 27 | 28 | // Handles the case where an empty string match causes lastIndex not to advance, 29 | // which gets us in an infinite loop. 30 | if (result.index === regex.lastIndex) { 31 | regex.lastIndex++; 32 | } 33 | result = regex.exec(line); 34 | } 35 | 36 | return positions; 37 | } 38 | -------------------------------------------------------------------------------- /src/transformations/transformer.ts: -------------------------------------------------------------------------------- 1 | import { Position, Range } from 'vscode'; 2 | import { PositionDiff } from '../common/motion/position'; 3 | import { Logger } from '../util/logger'; 4 | import { stringify, Transformation } from './transformations'; 5 | 6 | /** 7 | * This class is (ideally) responsible for managing all changes made to document state, via @see Transformation. 8 | * Currently, changes are queued up within Actions and then executed (more or less) all at once. 9 | * 10 | * NOTE: This whole system is heavily WIP as I work through a large piecemeal refactor. 11 | */ 12 | export class Transformer { 13 | public readonly transformations: Transformation[] = []; 14 | 15 | public addTransformation(transformation: Transformation): void { 16 | Logger.debug(`Adding Transformation ${stringify(transformation)}`); 17 | this.transformations.push(transformation); 18 | } 19 | 20 | public insert(position: Position, text: string, diff?: PositionDiff): void { 21 | this.addTransformation({ type: 'insertText', position, text, diff }); 22 | } 23 | 24 | public delete(range: Range, diff?: PositionDiff): void { 25 | this.addTransformation({ type: 'deleteRange', range, diff }); 26 | } 27 | 28 | public replace(range: Range, text: string, diff?: PositionDiff): void { 29 | this.addTransformation({ type: 'replaceText', range, text, diff }); 30 | } 31 | 32 | public moveCursor(diff: PositionDiff, cursorIndex?: number): void { 33 | this.addTransformation({ type: 'moveCursor', diff, cursorIndex }); 34 | } 35 | 36 | public vscodeCommand(command: string, ...args: any[]): void { 37 | this.addTransformation({ type: 'vscodeCommand', command, args }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/util/child_process.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from 'child_process'; 2 | import { promisify } from 'util'; 3 | 4 | export function exec( 5 | command: string, 6 | options?: child_process.ExecOptions, 7 | ): child_process.PromiseWithChild<{ stdout: string | Buffer; stderr: string | Buffer }> { 8 | return promisify(child_process.exec)(command, options); 9 | } 10 | -------------------------------------------------------------------------------- /src/util/clipboard.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Logger } from './logger'; 3 | 4 | /** 5 | * A thin wrapper around `vscode.env.clipboard` 6 | */ 7 | export class Clipboard { 8 | public static async Copy(text: string): Promise { 9 | try { 10 | await vscode.env.clipboard.writeText(text); 11 | } catch (e) { 12 | Logger.error(`Error copying to clipboard. err=${e}`); 13 | } 14 | } 15 | 16 | public static async Paste(): Promise { 17 | return vscode.env.clipboard.readText(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/util/decorationUtils.ts: -------------------------------------------------------------------------------- 1 | import { DecorationOptions, Range, TextDocument, window } from 'vscode'; 2 | 3 | /** 4 | * Alias for the types of arrays that can be passed to a TextEditor's setDecorations method 5 | */ 6 | export type EditorDecorationArray = Range[] | DecorationOptions[]; 7 | 8 | /** 9 | * Decorations associated with search/substitute operations 10 | */ 11 | export type SearchDecorations = { 12 | searchHighlight?: EditorDecorationArray; 13 | searchMatch?: EditorDecorationArray; 14 | substitutionAppend?: EditorDecorationArray; 15 | substitutionReplace?: EditorDecorationArray; 16 | }; 17 | 18 | /** 19 | * @returns a DecorationOptions object representing the given range. If the 20 | * given range is empty, the range of the returned object will be extended one 21 | * character to the right. If the given range cannot be extended right, or 22 | * represents the end of a line (possibly containing EOL characters), the 23 | * returned object will specify an after element with the width of a single 24 | * character. 25 | */ 26 | export function ensureVisible(range: Range, document: TextDocument): DecorationOptions { 27 | return (range.isEmpty || range.end.isLineBeginning()) && range.start.isLineEnd(document) 28 | ? { 29 | // range is at EOL, possibly containing EOL char(s). 30 | range: range.with(undefined, range.start), 31 | renderOptions: { 32 | after: { 33 | color: 'transparent', 34 | contentText: '$', // non-whitespace character to set width. 35 | }, 36 | }, 37 | } 38 | : range.isEmpty 39 | ? { range: range.with(undefined, range.end.translate(0, 1)) } // extend range one character right 40 | : { range }; 41 | } 42 | 43 | /** 44 | * @returns a version of the input string suitable for use as the contentText of a decoration's before or after element 45 | */ 46 | export function formatDecorationText( 47 | text: string, 48 | tabsize: number, 49 | newlineReplacement: string | ((substring: string, ...args: any[]) => string) = '\u23ce', // "⏎" RETURN SYMBOL 50 | ) { 51 | // surround with zero-width space to prevent trimming 52 | return `\u200b${text 53 | // vscode collapses whitespace in decorations; modify text to prevent this. 54 | .replace(/ /g, '\u00a0') // " " NO-BREAK SPACE 55 | .replace(/\t/g, '\u00a0'.repeat(tabsize)) 56 | // Decorations can't change the apparent # of lines in the editor, so we must settle for a single-line version of our text 57 | .replace(/\r\n|[\r\n]/g, newlineReplacement as string)}\u200b`; 58 | } 59 | 60 | /** 61 | * @returns search decorations for the given ranges, taking into account the current match 62 | */ 63 | export function getDecorationsForSearchMatchRanges( 64 | ranges: Range[], 65 | document: TextDocument, 66 | currentMatchIndex?: number, 67 | ): SearchDecorations { 68 | const searchHighlight: DecorationOptions[] = []; 69 | const searchMatch: DecorationOptions[] = []; 70 | 71 | for (let i = 0; i < ranges.length; i++) { 72 | (i === currentMatchIndex ? searchMatch : searchHighlight).push( 73 | ensureVisible(ranges[i], document), 74 | ); 75 | } 76 | 77 | return { searchHighlight, searchMatch }; 78 | } 79 | -------------------------------------------------------------------------------- /src/util/logger.ts: -------------------------------------------------------------------------------- 1 | import { LogOutputChannel, window } from 'vscode'; 2 | 3 | export class Logger { 4 | private static output: LogOutputChannel; 5 | 6 | public static init(): void { 7 | Logger.output = window.createOutputChannel('Vim', { log: true }); 8 | } 9 | 10 | public static error(msg: string): void { 11 | Logger.output.error(msg); 12 | } 13 | public static warn(msg: string): void { 14 | Logger.output.warn(msg); 15 | } 16 | public static info(msg: string): void { 17 | Logger.output.info(msg); 18 | } 19 | public static debug(msg: string): void { 20 | Logger.output.debug(msg); 21 | } 22 | public static trace(msg: string): void { 23 | Logger.output.trace(msg); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/util/os.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | 3 | export function tmpdir(): string { 4 | return os.tmpdir(); 5 | } 6 | -------------------------------------------------------------------------------- /src/util/selections.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | /** 4 | * Hashes the given selections array into a string, based on the anchor and active positions of each 5 | * selection. 6 | */ 7 | export function hashSelectionsArray(selections: readonly vscode.Selection[]): string { 8 | return selections.reduce((hash, s) => hash + hashSelection(s), ''); 9 | } 10 | 11 | /** 12 | * Hashes the given selection into a string, based on its anchor and active positions. 13 | */ 14 | function hashSelection(selection: vscode.Selection): string { 15 | const { anchor, active } = selection; 16 | return `[${anchor.line}, ${anchor.character}; ${active.line}, ${active.character}]`; 17 | } 18 | 19 | /** 20 | * Returns whether the two arrays of selections are equal (i.e. have the same number of selections, 21 | * and the same anchor and active positions at each index). 22 | */ 23 | export function areSelectionArraysEqual( 24 | selectionsA: readonly vscode.Selection[], 25 | selectionsB: readonly vscode.Selection[], 26 | ): boolean { 27 | return ( 28 | selectionsA.length === selectionsB.length && 29 | selectionsA.every((sA, i) => areSelectionsEqual(sA, selectionsB[i])) 30 | ); 31 | } 32 | 33 | /** 34 | * Returns whether two selections are equal (i.e. have the same anchor and active positions). 35 | * 36 | * Note that `{@link vscode.Selection.isEqual}` is not used here because it's derived from 37 | * `Range.isEqual`, and only checks if the `start` and `end` positions are equal, without 38 | * considering `anchor` and `active` (i.e. which end of the range the cursor is on). 39 | */ 40 | function areSelectionsEqual(selectionA: vscode.Selection, selectionB: vscode.Selection): boolean { 41 | return ( 42 | selectionA.anchor.isEqual(selectionB.anchor) && selectionA.active.isEqual(selectionB.active) 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/util/specialKeys.ts: -------------------------------------------------------------------------------- 1 | export enum SpecialKeys { 2 | ExtensionEnable = '', 3 | ExtensionDisable = '', 4 | TimeoutFinished = '', 5 | } 6 | -------------------------------------------------------------------------------- /src/util/util.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Cursor } from '../common/motion/cursor'; 3 | import { VimState } from '../state/vimState'; 4 | 5 | /** 6 | * We used to have an issue where we would do something like execute a VSCode 7 | * command, and would encounter race conditions because the cursor positions 8 | * wouldn't yet be updated. So we waited for a selection change event, but 9 | * this doesn't seem to be necessary any more. 10 | * 11 | * @deprecated Calls to this should probably be replaced with calls to `ModeHandler::syncCursors()` or something... 12 | */ 13 | export function getCursorsAfterSync(editor: vscode.TextEditor): Cursor[] { 14 | return editor.selections.map((x) => Cursor.FromVSCodeSelection(x)); 15 | } 16 | 17 | export function clamp(num: number, min: number, max: number) { 18 | return Math.min(Math.max(num, min), max); 19 | } 20 | 21 | export function scrollView(vimState: VimState, offset: number) { 22 | if (offset !== 0) { 23 | vimState.postponedCodeViewChanges.push({ 24 | command: 'editorScroll', 25 | args: { 26 | to: offset > 0 ? 'up' : 'down', 27 | by: 'line', 28 | value: Math.abs(offset), 29 | revealCursor: false, 30 | select: false, 31 | }, 32 | }); 33 | } 34 | } 35 | 36 | export function assertDefined(x: X | undefined, err: string): asserts x { 37 | if (x === undefined) { 38 | throw new Error(err); 39 | } 40 | } 41 | 42 | export function isHighSurrogate(charCode: number): boolean { 43 | return 0xd800 <= charCode && charCode <= 0xdbff; 44 | } 45 | 46 | export function isLowSurrogate(charCode: number): boolean { 47 | return 0xdc00 <= charCode && charCode <= 0xdfff; 48 | } 49 | -------------------------------------------------------------------------------- /src/util/vscodeContext.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Logger } from './logger'; 3 | 4 | type ContextValue = boolean | string; 5 | 6 | /** 7 | * Wrapper around VS Code's `setContext`. 8 | * The API call takes several milliseconds to seconds to complete, 9 | * so let's cache the values and only call the API when necessary. 10 | */ 11 | export abstract class VSCodeContext { 12 | private static readonly cache: Map = new Map(); 13 | 14 | public static async set(key: string, value: ContextValue): Promise { 15 | const prev = this.get(key); 16 | if (prev !== value) { 17 | Logger.trace(`Setting key='${key}' to value='${value}'`); 18 | this.cache.set(key, value); 19 | await vscode.commands.executeCommand('setContext', key, value); 20 | } 21 | } 22 | 23 | public static get(key: string): ContextValue | undefined { 24 | return this.cache.get(key); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/vimscript/exCommand.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCode, VimError } from '../error'; 2 | import { VimState } from '../state/vimState'; 3 | import { LineRange } from './lineRange'; 4 | 5 | export abstract class ExCommand { 6 | /** 7 | * If this returns true and Neovim integration is enabled, we'll send this command to Neovim. 8 | */ 9 | public neovimCapable(): boolean { 10 | return false; 11 | } 12 | 13 | public readonly isRepeatableWithDot: boolean = true; 14 | 15 | abstract execute(vimState: VimState): Promise; 16 | 17 | async executeWithRange(vimState: VimState, range: LineRange): Promise { 18 | // By default, throw E481 ("No range allowed") 19 | throw VimError.fromCode(ErrorCode.NoRangeAllowed); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/vimscript/expression/displayValue.ts: -------------------------------------------------------------------------------- 1 | import { Value } from './types'; 2 | 3 | export function displayValue(value: Value, topLevel = true): string { 4 | switch (value.type) { 5 | case 'number': 6 | return value.value.toString(); 7 | case 'float': { 8 | // TODO: this is incorrect for float with exponent 9 | const result = value.value.toFixed(6).replace(/0*$/, ''); 10 | if (result.endsWith('.')) { 11 | return result + '0'; 12 | } 13 | return result; 14 | } 15 | case 'string': 16 | return topLevel ? value.value : `'${value.value.replace("'", "''")}'`; 17 | case 'list': 18 | return `[${value.items.map((v) => displayValue(v, false)).join(', ')}]`; 19 | case 'dict_val': 20 | return `{${[...value.items] 21 | .map(([k, v]) => `'${k}': ${displayValue(v, false)}`) 22 | .join(', ')}}`; 23 | case 'funcref': 24 | if (!value.arglist?.items.length) { 25 | if (value.dict) { 26 | return `function('${value.name}', ${displayValue(value.dict)})`; 27 | } 28 | return value.name; 29 | } else { 30 | if (value.dict) { 31 | return `function('${value.name}', ${displayValue(value.arglist)}, ${displayValue( 32 | value.dict, 33 | )})`; 34 | } 35 | return `function('${value.name}', ${displayValue(value.arglist)})`; 36 | } 37 | case 'blob': 38 | return ( 39 | '0z' + 40 | [...new Uint8Array(value.data)] 41 | .map((byte) => byte.toString(16).padStart(2, '0')) 42 | .join('') 43 | .toUpperCase() 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /syntaxes/vimscript.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "Vimscript", 4 | "fileTypes": [".vim", ".vimrc"], 5 | "scopeName": "source.vimscript", 6 | "patterns": [ 7 | { 8 | "name": "comment.line", 9 | "match": "(^| |\t)\".*$" 10 | }, 11 | { 12 | "name": "entity.name.function", 13 | "match": "^( |\t)*(let|const|eval|call)" 14 | }, 15 | { 16 | "name": "entity.name.function", 17 | "match": "^( |\t)*(map|nmap|vmap|smap|xmap|omap|map!|imap|lmap|cmap)" 18 | }, 19 | { 20 | "name": "entity.name.function", 21 | "match": "^( |\t)*(noremap|nnoremap|vnoremap|snoremap|xnoremap|onoremap|noremap!|inoremap|lnoremap|cnoremap)" 22 | }, 23 | { 24 | "name": "entity.name.function", 25 | "match": "^( |\t)*(unmap|nunmap|vunmap|sunmap|xunmap|ounmap|unmap!|iunmap|lunmap|cunmap)" 26 | }, 27 | { 28 | "name": "entity.name.function", 29 | "match": "^( |\t)*set" 30 | }, 31 | { 32 | "name": "constant.numeric", 33 | "match": "\\d+(\\.\\d+)?" 34 | }, 35 | { 36 | "name": "constant", 37 | "match": "(?i)" 38 | }, 39 | { 40 | "name": "constant", 41 | "match": "(?i)<(CR|Enter|Return)>" 42 | }, 43 | { 44 | "name": "constant", 45 | "match": "(?i)<(BS|Del|Space)>" 46 | }, 47 | { 48 | "name": "constant", 49 | "match": "(?i)" 50 | }, 51 | { 52 | "name": "constant", 53 | "match": "(?i)<(Up|Down|Left|Right)>" 54 | }, 55 | { 56 | "name": "constant", 57 | "match": "(?i)<[CD]-.>" 58 | }, 59 | { 60 | "name": "string.unquoted", 61 | "match": "(?i)[\\w-]+(\\.[\\w-]+)+" 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /test/actions/baseAction.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { BaseAction } from '../../src/actions/base'; 5 | import { EasyMotion } from '../../src/actions/plugins/easymotion/easymotion'; 6 | import { Mode } from '../../src/mode/mode'; 7 | import { VimState } from '../../src/state/vimState'; 8 | import { cleanUpWorkspace, setupWorkspace } from './../testUtils'; 9 | 10 | class TestAction1D extends BaseAction { 11 | keys = ['a', 'b']; 12 | actionType = 'command' as const; 13 | modes = [Mode.Normal]; 14 | } 15 | 16 | class TestAction2D extends BaseAction { 17 | keys = [ 18 | ['a', 'b'], 19 | ['c', 'd'], 20 | ]; 21 | actionType = 'command' as const; 22 | modes = [Mode.Normal]; 23 | } 24 | 25 | suite('base action', () => { 26 | const action1D = new TestAction1D(); 27 | const action2D = new TestAction2D(); 28 | let vimState: VimState; 29 | 30 | suiteSetup(async () => { 31 | await setupWorkspace(); 32 | vimState = new VimState(vscode.window.activeTextEditor!, new EasyMotion()); 33 | await vimState.load(); 34 | }); 35 | 36 | suiteTeardown(cleanUpWorkspace); 37 | 38 | test('compare key presses', () => { 39 | const testCases: Array<[string[] | string[][], string[], boolean]> = [ 40 | [['a'], ['a'], true], 41 | [[['a']], ['a'], true], 42 | [[['a'], ['b']], ['b'], true], 43 | [[['a'], ['b']], ['c'], false], 44 | [['a', 'b'], ['a', 'b'], true], 45 | [['a', 'b'], ['a', 'c'], false], 46 | [ 47 | [ 48 | ['a', 'b'], 49 | ['c', 'd'], 50 | ], 51 | ['c', 'd'], 52 | true, 53 | ], 54 | [[''], ['a'], false], 55 | [[''], [''], true], 56 | ]; 57 | 58 | for (const test of testCases) { 59 | const [left, right, expected] = test; 60 | 61 | const actual = BaseAction.CompareKeypressSequence(left, right); 62 | assert.strictEqual(actual, expected, `${left}. ${right}.`); 63 | } 64 | }); 65 | 66 | test('couldActionApply 1D keys positive', () => { 67 | assert.strictEqual(action1D.couldActionApply(vimState, ['a']), true); 68 | }); 69 | 70 | test('couldActionApply 1D keys negative', () => { 71 | assert.strictEqual(action1D.couldActionApply(vimState, ['b']), false); 72 | }); 73 | 74 | test('couldActionApply 2D keys positive', () => { 75 | assert.strictEqual(action2D.couldActionApply(vimState, ['c']), true); 76 | }); 77 | 78 | test('couldActionApply 2D keys negative', () => { 79 | assert.strictEqual(action2D.couldActionApply(vimState, ['b']), false); 80 | }); 81 | 82 | test('doesActionApply 1D keys positive', () => { 83 | assert.strictEqual(action1D.doesActionApply(vimState, ['a', 'b']), true); 84 | }); 85 | 86 | test('doesActionApply 1D keys negative', () => { 87 | assert.strictEqual(action1D.doesActionApply(vimState, ['a', 'a']), false); 88 | }); 89 | 90 | test('doesActionApply 2D keys positive', () => { 91 | assert.strictEqual(action2D.doesActionApply(vimState, ['c', 'd']), true); 92 | }); 93 | 94 | test('doesActionApply 2D keys negative', () => { 95 | assert.strictEqual(action2D.doesActionApply(vimState, ['a', 'a']), false); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/cmd_line/breakpoints.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { getAndUpdateModeHandler } from '../../extension'; 5 | import { ExCommandLine } from '../../src/cmd_line/commandLine'; 6 | import { ModeHandler } from '../../src/mode/modeHandler'; 7 | import { exCommandParser } from '../../src/vimscript/exCommandParser'; 8 | import { cleanUpWorkspace, setupWorkspace } from '../testUtils'; 9 | 10 | function clearBreakpoints() { 11 | vscode.debug.removeBreakpoints(vscode.debug.breakpoints); 12 | } 13 | 14 | suite('Breakpoints command', () => { 15 | let modeHandler: ModeHandler; 16 | 17 | suiteSetup(async () => { 18 | await setupWorkspace(); 19 | modeHandler = (await getAndUpdateModeHandler())!; 20 | }); 21 | suiteTeardown(cleanUpWorkspace); 22 | 23 | test('`:breaka` adds breakpoint', async () => { 24 | clearBreakpoints(); 25 | await modeHandler.handleMultipleKeyEvents(['', 'o', '']); // make sure it's working not only for the first line 26 | await new ExCommandLine('breaka', modeHandler.vimState.currentMode).run(modeHandler.vimState); 27 | assert.strictEqual(vscode.debug.breakpoints.length, 1); 28 | const breakpoint = vscode.debug.breakpoints[0] as vscode.SourceBreakpoint; 29 | assert.strictEqual( 30 | breakpoint.location.uri.fsPath, 31 | modeHandler.vimState.editor.document.uri.fsPath, 32 | ); 33 | assert.strictEqual( 34 | breakpoint.location.range.start.line, 35 | modeHandler.vimState.cursorStartPosition.line, 36 | ); 37 | }); 38 | 39 | test('`:breakd` delete breakpoint', async () => { 40 | clearBreakpoints(); 41 | await new ExCommandLine('breaka', modeHandler.vimState.currentMode).run(modeHandler.vimState); 42 | assert.strictEqual(vscode.debug.breakpoints.length, 1); 43 | await new ExCommandLine('breakd', modeHandler.vimState.currentMode).run(modeHandler.vimState); 44 | assert.strictEqual(vscode.debug.breakpoints.length, 0); 45 | }); 46 | 47 | test('test "here" is redundant', async () => { 48 | assert.deepStrictEqual( 49 | exCommandParser.tryParse(':breaka here'), 50 | exCommandParser.tryParse(':breaka'), 51 | ); 52 | assert.deepStrictEqual( 53 | exCommandParser.tryParse(':breakd here'), 54 | exCommandParser.tryParse(':breakd'), 55 | ); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/cmd_line/bufferDelete.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { join } from 'path'; 5 | import { getAndUpdateModeHandler } from '../../extension'; 6 | import { ExCommandLine } from '../../src/cmd_line/commandLine'; 7 | import * as error from '../../src/error'; 8 | import { ModeHandler } from '../../src/mode/modeHandler'; 9 | import * as t from '../testUtils'; 10 | 11 | suite('Buffer delete', () => { 12 | let modeHandler: ModeHandler; 13 | 14 | setup(async () => { 15 | await t.setupWorkspace(); 16 | modeHandler = (await getAndUpdateModeHandler())!; 17 | }); 18 | 19 | for (const cmd of ['bdelete', 'bdel', 'bd']) { 20 | test(`${cmd} deletes the current buffer`, async () => { 21 | await new ExCommandLine(cmd, modeHandler.vimState.currentMode).run(modeHandler.vimState); 22 | await t.waitForEditorsToClose(); 23 | 24 | assert.strictEqual(vscode.window.visibleTextEditors.length, 0); 25 | }); 26 | } 27 | 28 | test('bd does not delete buffer when there are unsaved changes', async () => { 29 | await modeHandler.handleMultipleKeyEvents(['i', 'a', 'b', 'a', '']); 30 | try { 31 | await new ExCommandLine('bd', modeHandler.vimState.currentMode).run(modeHandler.vimState); 32 | } catch (e) { 33 | assert.strictEqual(e, error.VimError.fromCode(error.ErrorCode.NoWriteSinceLastChange)); 34 | } 35 | }); 36 | 37 | test('bd! deletes the current buffer regardless of unsaved changes', async () => { 38 | await modeHandler.handleMultipleKeyEvents(['i', 'a', 'b', 'a', '']); 39 | 40 | await new ExCommandLine('bd!', modeHandler.vimState.currentMode).run(modeHandler.vimState); 41 | await t.waitForEditorsToClose(); 42 | 43 | assert.strictEqual(vscode.window.visibleTextEditors.length, 0); 44 | }); 45 | 46 | test.skip("bd 'N' deletes the Nth buffer open", async () => { 47 | const dirPath = await t.createDir(); 48 | const filePaths: string[] = []; 49 | 50 | try { 51 | for (let i = 0; i < 3; i++) { 52 | const uri: vscode.Uri = vscode.Uri.parse(join(dirPath, `${i}`)); 53 | filePaths.push(uri.toString()); 54 | void vscode.workspace.openTextDocument(uri).then((doc: vscode.TextDocument) => { 55 | void doc.save(); 56 | }); 57 | } 58 | 59 | await new ExCommandLine('bd 2', modeHandler.vimState.currentMode).run(modeHandler.vimState); 60 | await vscode.commands.executeCommand('workbench.action.openEditorAtIndex2'); 61 | 62 | assert.strictEqual(vscode.window.visibleTextEditors.length, 2); 63 | assert.strictEqual(vscode.window.activeTextEditor?.document.uri.fsPath, filePaths[2]); 64 | } finally { 65 | await vscode.workspace.fs.delete(vscode.Uri.file(dirPath), { recursive: true }); 66 | } 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/cmd_line/cursorLocation.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { getAndUpdateModeHandler } from '../../extension'; 4 | import { ModeHandler } from '../../src/mode/modeHandler'; 5 | import { setupWorkspace, cleanUpWorkspace } from '../testUtils'; 6 | import { StatusBar } from '../../src/statusBar'; 7 | 8 | suite('cursor location', () => { 9 | let modeHandler: ModeHandler; 10 | 11 | suiteSetup(async () => { 12 | await setupWorkspace(); 13 | modeHandler = (await getAndUpdateModeHandler())!; 14 | }); 15 | 16 | suiteTeardown(cleanUpWorkspace); 17 | 18 | test('cursor location in command line', async () => { 19 | await modeHandler.handleMultipleKeyEvents([ 20 | ':', 21 | 't', 22 | 'e', 23 | 's', 24 | 't', 25 | '', 26 | '', 27 | '', 28 | '', 29 | ]); 30 | 31 | const statusBarAfterCursorMovement = StatusBar.getText(); 32 | await modeHandler.handleKeyEvent(''); 33 | 34 | const statusBarAfterEsc = StatusBar.getText(); 35 | assert.strictEqual( 36 | statusBarAfterCursorMovement.trim(), 37 | ':tes|t', 38 | 'Command Tab Completion Failed', 39 | ); 40 | }); 41 | 42 | test('cursor location in search', async () => { 43 | await modeHandler.handleMultipleKeyEvents([ 44 | '/', 45 | 't', 46 | 'e', 47 | 's', 48 | 't', 49 | '', 50 | '', 51 | '', 52 | '', 53 | ]); 54 | 55 | const statusBarAfterCursorMovement = StatusBar.getText(); 56 | 57 | await modeHandler.handleKeyEvent(''); 58 | const statusBarAfterEsc = StatusBar.getText(); 59 | assert.strictEqual( 60 | statusBarAfterCursorMovement.trim(), 61 | '/tes|t', 62 | 'Command Tab Completion Failed', 63 | ); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/cmd_line/move.test.ts: -------------------------------------------------------------------------------- 1 | import { newTest } from '../testSimplifier'; 2 | 3 | suite(':[range]m[ove] [address] command', () => { 4 | newTest({ 5 | title: ':move [address] will move the cursor line to below the line given by {address}', 6 | start: ['|one', 'two', 'three'], 7 | keysPressed: ':m2\n', 8 | end: ['two', '|one', 'three'], 9 | }); 10 | 11 | newTest({ 12 | title: 13 | ':[range]move [address] will move the lines given by [range] to below the line given by {address}', 14 | start: ['|one', 'two', 'three', 'four', 'five'], 15 | keysPressed: ':1,3m4\n', 16 | end: ['four', 'one', 'two', '|three', 'five'], 17 | }); 18 | 19 | newTest({ 20 | title: ':[range]move [address] will move the visual range', 21 | start: ['|one', 'two', 'three', 'four', 'five'], 22 | keysPressed: 'vjj:m4\n', 23 | end: ['four', 'one', 'two', '|three', 'five'], 24 | }); 25 | 26 | newTest({ 27 | title: ':[range]move [address], boundary test: move 3 lines to the buttom', 28 | start: ['|one', 'two', 'three', 'four', 'five'], 29 | keysPressed: ':1,3m5\n', 30 | end: ['four', 'five', 'one', 'two', '|three'], 31 | }); 32 | 33 | newTest({ 34 | title: ':[range]move [address], boundary test: move 3 lines to the top', 35 | start: ['|one', 'two', 'three', 'four', 'five'], 36 | keysPressed: ':3,4m0\n', 37 | end: ['three', '|four', 'one', 'two', 'five'], 38 | }); 39 | 40 | newTest({ 41 | title: ':[range]move [address] will do nothing when move a range of line into itself', 42 | start: ['|one', 'two', 'three', 'four', 'five'], 43 | keysPressed: ':1,3m2\n', 44 | end: ['|one', 'two', 'three', 'four', 'five'], 45 | }); 46 | 47 | newTest({ 48 | title: ':[range]move [address] will do nothing when move a range of line right below itself', 49 | start: ['|one', 'two', 'three', 'four', 'five'], 50 | keysPressed: ':1,3m3\n', 51 | end: ['|one', 'two', 'three', 'four', 'five'], 52 | }); 53 | 54 | newTest({ 55 | title: ':[range]move [address] will do nothing when move a range of line right above itself', 56 | start: ['|one', 'two', 'three', 'four', 'five'], 57 | keysPressed: ':1,3m1\n', 58 | end: ['|one', 'two', 'three', 'four', 'five'], 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/cmd_line/normal.test.ts: -------------------------------------------------------------------------------- 1 | import { newTest, newTestSkip } from '../testSimplifier'; 2 | 3 | suite('Execute normal command', () => { 4 | newTest({ 5 | title: 'One liner', 6 | start: ['foo =| bar = 1'], 7 | keysPressed: ':normal f=i!=\n', 8 | end: ['foo = bar !|== 1'], 9 | }); 10 | 11 | newTest({ 12 | title: 'One liner with selection', 13 | start: ['foo =| bar = 1'], 14 | keysPressed: 'V:normal f=i!=\n', 15 | end: ['foo !|== bar = 1'], 16 | }); 17 | 18 | newTest({ 19 | title: 'Multiple liner with selection', 20 | start: ['foo =| bar = 1', 'foo = bar = 2'], 21 | keysPressed: 'Vj:normal f=i!=\n', 22 | end: ['foo !== bar = 1', 'foo !|== bar = 2'], 23 | }); 24 | 25 | newTest({ 26 | title: 'Multiple liner with line range', 27 | start: ['foo =| bar = 1', 'foo = bar = 2', 'foo = bar = 3'], 28 | keysPressed: ':2,3normal f=i!=\n', 29 | end: ['foo = bar = 1', 'foo !== bar = 2', 'foo !|== bar = 3'], 30 | }); 31 | 32 | newTest({ 33 | title: 'One liner with dot', 34 | start: ['foo =| bar = 1', 'foo = bar = 2'], 35 | keysPressed: 'f=i!=j^f=:normal .\n', 36 | end: ['foo = bar !== 1', 'foo !|== bar = 2'], 37 | }); 38 | 39 | newTest({ 40 | title: 'One liner with multiple dot', 41 | start: ['foo =| bar = 1', 'foo = bar = 2'], 42 | keysPressed: 'f=i!=j^f=:normal 2.\n', 43 | end: ['foo = bar !== 1', 'foo !=!|== bar = 2'], 44 | }); 45 | 46 | newTest({ 47 | title: 'One liner with macro', 48 | start: ['|1. one, 2. two, 3. three, 4. four'], 49 | keysPressed: 'qaf.r)q:normal @a\n', 50 | end: ['1) one, 2|) two, 3. three, 4. four'], 51 | }); 52 | 53 | newTest({ 54 | title: 'One liner with multiple macro', 55 | start: ['|1. one, 2. two, 3. three, 4. four'], 56 | keysPressed: 'qaf.r)q:normal 3@a\n', 57 | end: ['1) one, 2) two, 3) three, 4|) four'], 58 | }); 59 | 60 | newTest({ 61 | title: 'Multiple liner with multiple macro', 62 | start: ['|0. zero', '1. one, 2. two, 3. three, 4. four', '5. five, 6. six, 7. seven, 8. eight'], 63 | keysPressed: 'qaf.r)qjVj:normal 4@a\n', 64 | end: ['0) zero', '1) one, 2) two, 3) three, 4) four', '5) five, 6) six, 7) seven, 8|) eight'], 65 | }); 66 | 67 | newTest({ 68 | title: 'Incomplete operation', 69 | start: ['foo =| bar = 1', 'foo = bar = 2'], 70 | keysPressed: ':normal ddd\n', 71 | end: ['|foo = bar = 2'], 72 | }); 73 | 74 | // TODO: implement to stop when operation fails 75 | newTestSkip({ 76 | title: 'Operation stops after command fails', 77 | start: ['foo =| bar = 1', 'foo = bar = 2'], 78 | keysPressed: ':normal llllllllllllllllllllllllllllll j\n', 79 | end: ['foo = bar = |1', 'foo = bar = 2'], 80 | }); 81 | 82 | newTest({ 83 | title: 'Multiple liner with selection and undo', 84 | start: ['foo =| bar = 1', 'foo = bar = 2'], 85 | keysPressed: 'Vj:normal f=i!=\nu', 86 | end: ['foo |= bar = 1', 'foo = bar = 2'], 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/cmd_line/only.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { getAndUpdateModeHandler } from '../../extension'; 5 | import { ExCommandLine } from '../../src/cmd_line/commandLine'; 6 | import { ModeHandler } from '../../src/mode/modeHandler'; 7 | import { cleanUpWorkspace, setupWorkspace, waitForEditorsToClose } from '../testUtils'; 8 | 9 | const isPanelVisible = async () => 10 | withinIsolatedEditor(async () => { 11 | // Insert 1000 lines (ie. beyond veritical viewport) 12 | await vscode.window.activeTextEditor!.edit((editBuilder) => { 13 | editBuilder.insert(new vscode.Position(0, 0), 'Line\n'.repeat(1000)); 14 | }); 15 | 16 | // Toggle the panel's visibility to see which has a larger vertical viewport 17 | const initialVisibleLineCount = await getNumberOfVisibleLines(); 18 | await vscode.commands.executeCommand('workbench.action.togglePanel'); 19 | const postToggleVisibleLineCount = await getNumberOfVisibleLines(); 20 | await vscode.commands.executeCommand('workbench.action.togglePanel'); 21 | 22 | return postToggleVisibleLineCount > initialVisibleLineCount; 23 | }); 24 | 25 | const withinIsolatedEditor = async (lambda: () => Thenable) => { 26 | await vscode.commands.executeCommand('workbench.action.files.newUntitledFile'); 27 | const result = await lambda(); 28 | await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); 29 | return result; 30 | }; 31 | 32 | const getNumberOfVisibleLines = async () => 33 | vscode.window.activeTextEditor!.visibleRanges[0].end.line; 34 | 35 | suite(':only command', () => { 36 | let modeHandler: ModeHandler; 37 | 38 | setup(async () => { 39 | await setupWorkspace(); 40 | modeHandler = (await getAndUpdateModeHandler())!; 41 | }); 42 | 43 | teardown(cleanUpWorkspace); 44 | 45 | test('Run :only', async () => { 46 | // Ensure we have multiple editors in a split 47 | await vscode.commands.executeCommand('workbench.action.splitEditorRight'); 48 | await waitForEditorsToClose(2); 49 | assert.strictEqual(vscode.window.visibleTextEditors.length, 2, 'Editor did not split into 2'); 50 | 51 | // Ensure panel is visible 52 | if ((await isPanelVisible()) !== true) { 53 | await vscode.commands.executeCommand('workbench.action.togglePanel'); 54 | } 55 | assert.strictEqual(await isPanelVisible(), true); 56 | 57 | // Run 'only' command 58 | await new ExCommandLine('only', modeHandler.vimState.currentMode).run(modeHandler.vimState); 59 | assert.strictEqual( 60 | vscode.window.visibleTextEditors.length, 61 | 1, 62 | 'Did not reduce to single editor', 63 | ); 64 | assert.strictEqual(await isPanelVisible(), false, 'Panel is still visible'); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/cmd_line/redo.test.ts: -------------------------------------------------------------------------------- 1 | import { getAndUpdateModeHandler } from '../../extension'; 2 | import { ExCommandLine } from '../../src/cmd_line/commandLine'; 3 | import { ModeHandler } from '../../src/mode/modeHandler'; 4 | import { assertEqualLines, setupWorkspace } from '../testUtils'; 5 | 6 | suite('Redo command', () => { 7 | let modeHandler: ModeHandler; 8 | 9 | setup(async () => { 10 | await setupWorkspace(); 11 | modeHandler = (await getAndUpdateModeHandler())!; 12 | }); 13 | 14 | test('redoes last undoed action after insert mode', async () => { 15 | await modeHandler.handleMultipleKeyEvents(['I', 'a', '']); 16 | await modeHandler.handleMultipleKeyEvents(['I', 'b', '']); 17 | await new ExCommandLine('undo', modeHandler.vimState.currentMode).run(modeHandler.vimState); 18 | await new ExCommandLine('redo', modeHandler.vimState.currentMode).run(modeHandler.vimState); 19 | assertEqualLines(['ba']); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/cmd_line/smile.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { getAndUpdateModeHandler } from '../../extension'; 5 | import { ExCommandLine } from '../../src/cmd_line/commandLine'; 6 | import { ModeHandler } from '../../src/mode/modeHandler'; 7 | import { assertEqualLines, setupWorkspace, waitForTabChange } from './../testUtils'; 8 | import { SmileCommand } from '../../src/cmd_line/commands/smile'; 9 | 10 | suite('Smile command', () => { 11 | let modeHandler: ModeHandler; 12 | 13 | suiteSetup(async () => { 14 | await setupWorkspace(); 15 | modeHandler = (await getAndUpdateModeHandler())!; 16 | }); 17 | 18 | test(':smile creates new tab', async () => { 19 | await new ExCommandLine('smile', modeHandler.vimState.currentMode).run(modeHandler.vimState); 20 | await waitForTabChange(); 21 | 22 | assert.strictEqual( 23 | vscode.window.visibleTextEditors.length, 24 | 1, 25 | ':smile did not create a new untitled file', 26 | ); 27 | }); 28 | 29 | test(':smile editor contains smile text', async () => { 30 | await new ExCommandLine('smile', modeHandler.vimState.currentMode).run(modeHandler.vimState); 31 | await waitForTabChange(); 32 | const textArray = SmileCommand.smileText.split('\n'); 33 | 34 | assertEqualLines(textArray); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/cmd_line/split.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { getAndUpdateModeHandler } from '../../extension'; 5 | import { ExCommandLine } from '../../src/cmd_line/commandLine'; 6 | import { ModeHandler } from '../../src/mode/modeHandler'; 7 | import { setupWorkspace, waitForEditorsToClose } from './../testUtils'; 8 | 9 | suite('Horizontal split', () => { 10 | let modeHandler: ModeHandler; 11 | 12 | setup(async () => { 13 | await setupWorkspace(); 14 | modeHandler = (await getAndUpdateModeHandler())!; 15 | }); 16 | 17 | for (const cmd of ['sp', 'split', 'new']) { 18 | test(`:${cmd} creates a second split`, async () => { 19 | await new ExCommandLine(cmd, modeHandler.vimState.currentMode).run(modeHandler.vimState); 20 | await waitForEditorsToClose(2); 21 | 22 | assert.strictEqual( 23 | vscode.window.visibleTextEditors.length, 24 | 2, 25 | 'Editor did not split in 1 sec', 26 | ); 27 | }); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /test/cmd_line/tab.test.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as assert from 'assert'; 3 | 4 | import { getAndUpdateModeHandler } from '../../extension'; 5 | import { ExCommandLine } from '../../src/cmd_line/commandLine'; 6 | import { ModeHandler } from '../../src/mode/modeHandler'; 7 | import { createFile, setupWorkspace, cleanUpWorkspace } from '../testUtils'; 8 | 9 | suite('cmd_line tab', () => { 10 | let modeHandler: ModeHandler; 11 | 12 | suiteSetup(async () => { 13 | await setupWorkspace(); 14 | modeHandler = (await getAndUpdateModeHandler())!; 15 | }); 16 | 17 | suiteTeardown(cleanUpWorkspace); 18 | 19 | test('tabe with no arguments when not in workspace opens an untitled file', async () => { 20 | const beforeEditor = vscode.window.activeTextEditor; 21 | await new ExCommandLine('tabe', modeHandler.vimState.currentMode).run(modeHandler.vimState); 22 | const afterEditor = vscode.window.activeTextEditor; 23 | 24 | assert.notStrictEqual(beforeEditor, afterEditor, 'Active editor did not change'); 25 | }); 26 | 27 | test('tabedit with no arguments when not in workspace opens an untitled file', async () => { 28 | const beforeEditor = vscode.window.activeTextEditor; 29 | await new ExCommandLine('tabedit', modeHandler.vimState.currentMode).run(modeHandler.vimState); 30 | const afterEditor = vscode.window.activeTextEditor; 31 | 32 | assert.notStrictEqual(beforeEditor, afterEditor, 'Active editor did not change'); 33 | }); 34 | 35 | test('tabe with absolute path when not in workspace opens file', async () => { 36 | const filePath = await createFile(); 37 | await new ExCommandLine(`tabe ${filePath}`, modeHandler.vimState.currentMode).run( 38 | modeHandler.vimState, 39 | ); 40 | const editor = vscode.window.activeTextEditor; 41 | 42 | if (editor === undefined) { 43 | assert.fail('File did not open'); 44 | } else { 45 | if (process.platform !== 'win32') { 46 | assert.strictEqual(editor.document.fileName, filePath, 'Opened wrong file'); 47 | } else { 48 | assert.strictEqual( 49 | editor.document.fileName.toLowerCase(), 50 | filePath.toLowerCase(), 51 | 'Opened wrong file', 52 | ); 53 | } 54 | } 55 | }); 56 | 57 | test('tabe with current file path does nothing', async () => { 58 | const filePath = await createFile(); 59 | await new ExCommandLine(`tabe ${filePath}`, modeHandler.vimState.currentMode).run( 60 | modeHandler.vimState, 61 | ); 62 | 63 | const beforeEditor = vscode.window.activeTextEditor; 64 | await new ExCommandLine(`tabe ${filePath}`, modeHandler.vimState.currentMode).run( 65 | modeHandler.vimState, 66 | ); 67 | const afterEditor = vscode.window.activeTextEditor; 68 | 69 | assert.strictEqual( 70 | beforeEditor, 71 | afterEditor, 72 | 'Active editor changed even though :tabe opened the same file', 73 | ); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/cmd_line/undo.test.ts: -------------------------------------------------------------------------------- 1 | import { getAndUpdateModeHandler } from '../../extension'; 2 | import { ExCommandLine } from '../../src/cmd_line/commandLine'; 3 | import { ModeHandler } from '../../src/mode/modeHandler'; 4 | import { assertEqualLines, setupWorkspace } from '../testUtils'; 5 | 6 | suite('Undo command', () => { 7 | let modeHandler: ModeHandler; 8 | 9 | setup(async () => { 10 | await setupWorkspace(); 11 | modeHandler = (await getAndUpdateModeHandler())!; 12 | }); 13 | 14 | test('undoes last action after insert mode', async () => { 15 | await modeHandler.handleMultipleKeyEvents(['i', 'a', '']); 16 | await modeHandler.handleMultipleKeyEvents(['i', 'b', '']); 17 | await new ExCommandLine('undo', modeHandler.vimState.currentMode).run(modeHandler.vimState); 18 | assertEqualLines(['a']); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/cmd_line/vsplit.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { getAndUpdateModeHandler } from '../../extension'; 5 | import { ExCommandLine } from '../../src/cmd_line/commandLine'; 6 | import { ModeHandler } from '../../src/mode/modeHandler'; 7 | import { setupWorkspace, waitForEditorsToClose } from './../testUtils'; 8 | 9 | suite('Vertical split', () => { 10 | let modeHandler: ModeHandler; 11 | 12 | setup(async () => { 13 | await setupWorkspace(); 14 | modeHandler = (await getAndUpdateModeHandler())!; 15 | }); 16 | 17 | for (const cmd of ['vs', 'vsp', 'vsplit', 'vnew', 'vne']) { 18 | test(`:${cmd} creates a second split`, async () => { 19 | await new ExCommandLine(cmd, modeHandler.vimState.currentMode).run(modeHandler.vimState); 20 | await waitForEditorsToClose(2); 21 | 22 | assert.strictEqual( 23 | vscode.window.visibleTextEditors.length, 24 | 2, 25 | 'Editor did not split in 1 sec', 26 | ); 27 | }); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /test/cmd_line/writequit.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { getAndUpdateModeHandler } from '../../extension'; 5 | import { ExCommandLine } from '../../src/cmd_line/commandLine'; 6 | import { ModeHandler } from '../../src/mode/modeHandler'; 7 | import { newTest } from '../testSimplifier'; 8 | import { cleanUpWorkspace, setupWorkspace, waitForEditorsToClose } from './../testUtils'; 9 | 10 | suite('Basic write-quit', () => { 11 | let modeHandler: ModeHandler; 12 | 13 | setup(async () => { 14 | await setupWorkspace(); 15 | modeHandler = (await getAndUpdateModeHandler())!; 16 | }); 17 | 18 | suiteTeardown(cleanUpWorkspace); 19 | 20 | test('Run write and quit', async () => { 21 | await modeHandler.handleMultipleKeyEvents(['i', 'a', 'b', 'a', '']); 22 | 23 | await new ExCommandLine('wq', modeHandler.vimState.currentMode).run(modeHandler.vimState); 24 | await waitForEditorsToClose(); 25 | 26 | assert.strictEqual(vscode.window.visibleTextEditors.length, 0, 'Window after 1sec still open'); 27 | }); 28 | 29 | newTest({ 30 | title: ':q[uit] cannot close dirty file', 31 | start: ['one', 't|wo', 'three'], 32 | keysPressed: 'x' + ':q\n', 33 | end: ['one', 't|o', 'three'], 34 | statusBar: 'E37: No write since last change (add ! to override)', 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/cmd_line/yank.test.ts: -------------------------------------------------------------------------------- 1 | import { newTest } from '../testSimplifier'; 2 | 3 | suite(':[range]y[ank] [count] command', () => { 4 | newTest({ 5 | title: ':yank will yank a single line', 6 | start: ['|one', 'two', 'three'], 7 | keysPressed: ':yank\n' + 'p', 8 | end: ['one', '|one', 'two', 'three'], 9 | }); 10 | 11 | newTest({ 12 | title: ':yank [cnt] will yank 3 lines', 13 | start: ['|one', 'two', 'three', 'four', 'five'], 14 | keysPressed: ':y3\n' + 'p', 15 | end: ['one', '|one', 'two', 'three', 'two', 'three', 'four', 'five'], 16 | }); 17 | 18 | newTest({ 19 | title: ':yank [x] [cnt] will yank [cnt] lines', 20 | start: ['|one', 'two', 'three', 'four', 'five'], 21 | keysPressed: ':ya3\n' + 'p', 22 | end: ['one', '|one', 'two', 'three', 'two', 'three', 'four', 'five'], 23 | }); 24 | 25 | newTest({ 26 | title: ':yank [register] [cnt] will yank [cnt] lines', 27 | start: ['|one', 'two', 'three', 'four', 'five'], 28 | keysPressed: ':yan3\n' + 'p', 29 | end: ['one', '|one', 'two', 'three', 'two', 'three', 'four', 'five'], 30 | }); 31 | 32 | newTest({ 33 | title: 34 | ':[range]yank [cnt] will yank from the end of the range, if range is VisualLine highlight', 35 | start: ['|one', 'two', 'three', 'four', 'five'], 36 | keysPressed: 'vjj:yan3\nG' + 'p', 37 | end: ['one', 'two', 'three', 'four', 'five', '|three', 'four', 'five'], 38 | }); 39 | 40 | newTest({ 41 | title: ':[range]yank [cnt] will yank [cnt] from the end of the range, if range a line number', 42 | start: ['|one', 'two', 'three', 'four', 'five'], 43 | keysPressed: ':.+3yan2\n' + 'p', 44 | end: ['one', '|four', 'five', 'two', 'three', 'four', 'five'], 45 | }); 46 | 47 | newTest({ 48 | title: ':[range]yank will yank from the end of the range, if range a line number', 49 | start: ['|one', 'two', 'three', 'four', 'five'], 50 | keysPressed: ':.+3yan\n' + 'p', 51 | end: ['one', '|four', 'two', 'three', 'four', 'five'], 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/completion/lineCompletion.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { getAndUpdateModeHandler } from '../../extension'; 4 | import { getCompletionsForCurrentLine } from '../../src/completion/lineCompletionProvider'; 5 | import { ModeHandler } from '../../src/mode/modeHandler'; 6 | import { cleanUpWorkspace, setupWorkspace } from '../testUtils'; 7 | import { VimState } from '../../src/state/vimState'; 8 | import { Position } from 'vscode'; 9 | 10 | suite('Provide line completions', () => { 11 | let modeHandler: ModeHandler; 12 | let vimState: VimState; 13 | 14 | suiteSetup(async () => { 15 | await setupWorkspace(); 16 | modeHandler = (await getAndUpdateModeHandler())!; 17 | vimState = modeHandler.vimState; 18 | }); 19 | suiteTeardown(cleanUpWorkspace); 20 | 21 | const setupTestWithLines = async (lines: string[]) => { 22 | vimState.cursorStopPosition = new Position(0, 0); 23 | 24 | await modeHandler.handleKeyEvent(''); 25 | await vimState.editor.edit((builder) => { 26 | builder.insert(new Position(0, 0), lines.join('\n')); 27 | }); 28 | await modeHandler.handleMultipleKeyEvents(['', 'g', 'g', 'j', 'j', 'A']); 29 | }; 30 | 31 | suite('Line Completion Provider unit tests', () => { 32 | // TODO(#4844): this fails on Windows 33 | test('Can complete lines in file, prioritizing above cursor, near cursor', async () => { 34 | if (process.platform === 'win32') { 35 | return; 36 | } 37 | const lines = ['a1', 'a2', 'a', 'a3', 'b1', 'a4']; 38 | await setupTestWithLines(lines); 39 | const expectedCompletions = ['a2', 'a1', 'a3', 'a4']; 40 | const topCompletions = getCompletionsForCurrentLine( 41 | vimState.cursorStopPosition, 42 | vimState.document, 43 | )!.slice(0, expectedCompletions.length); 44 | 45 | assert.deepStrictEqual(topCompletions, expectedCompletions, 'Unexpected completions found'); 46 | }); 47 | 48 | // TODO(#4844): this fails on Windows (and now linux too, for some reason?) 49 | test.skip('Can complete lines in file with different indentation', async () => { 50 | if (process.platform === 'win32') { 51 | return; 52 | } 53 | const lines = ['a1', ' a 2', 'a', 'a3 ', 'b1', 'a4']; 54 | await setupTestWithLines(lines); 55 | const expectedCompletions = ['a 2', 'a1', 'a3 ', 'a4']; 56 | const topCompletions = getCompletionsForCurrentLine( 57 | vimState.cursorStopPosition, 58 | vimState.document, 59 | )!.slice(0, expectedCompletions.length); 60 | 61 | assert.deepStrictEqual(topCompletions, expectedCompletions, 'Unexpected completions found'); 62 | }); 63 | 64 | test('Returns no completions for unmatched line', async () => { 65 | const lines = ['a1', ' a2', 'azzzzzzzzzzzzzzzzzzzzzzzz', 'a3 ', 'b1', 'a4']; 66 | await setupTestWithLines(lines); 67 | const expectedCompletions = []; 68 | const completions = getCompletionsForCurrentLine( 69 | vimState.cursorStopPosition, 70 | vimState.document, 71 | )!.slice(0, expectedCompletions.length); 72 | 73 | assert.strictEqual(completions.length, 0, 'Completions found, but none were expected'); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/configuration/configuration.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as srcConfiguration from '../../src/configuration/configuration'; 3 | import * as vscode from 'vscode'; 4 | import { setupWorkspace } from './../testUtils'; 5 | import { Mode } from '../../src/mode/mode'; 6 | import { newTest } from '../testSimplifier'; 7 | import { IConfiguration } from '../../src/configuration/iconfiguration'; 8 | 9 | const testConfig: Partial = { 10 | leader: '', 11 | normalModeKeyBindingsNonRecursive: [ 12 | { 13 | before: ['leader', 'o'], 14 | after: ['o', 'eSc', 'k'], 15 | }, 16 | { 17 | before: ['', 'f', 'e', 's'], 18 | after: ['v'], 19 | }, 20 | ], 21 | whichwrap: 'h,l', 22 | }; 23 | 24 | suite('Configuration', () => { 25 | setup(async () => { 26 | await setupWorkspace({ config: testConfig }); 27 | }); 28 | 29 | test('remappings are normalized', async () => { 30 | const normalizedKeybinds = srcConfiguration.configuration.normalModeKeyBindingsNonRecursive; 31 | const normalizedKeybindsMap = srcConfiguration.configuration.normalModeKeyBindingsMap; 32 | const testingKeybinds = testConfig.normalModeKeyBindingsNonRecursive; 33 | 34 | assert.strictEqual(normalizedKeybinds.length, testingKeybinds!.length); 35 | assert.strictEqual(normalizedKeybinds.length, normalizedKeybindsMap.size); 36 | assert.deepStrictEqual(normalizedKeybinds[0].before, [' ', 'o']); 37 | assert.deepStrictEqual(normalizedKeybinds[0].after, ['o', '', 'k']); 38 | }); 39 | 40 | test('textwidth is configurable per-language', async () => { 41 | const globalVimConfig = vscode.workspace.getConfiguration('vim'); 42 | const jsVimConfig = vscode.workspace.getConfiguration('vim', { languageId: 'javascript' }); 43 | 44 | try { 45 | assert.strictEqual(jsVimConfig.get('textwidth'), 80); 46 | await jsVimConfig.update('textwidth', 120, vscode.ConfigurationTarget.Global, true); 47 | 48 | const updatedGlobalVimConfig = vscode.workspace.getConfiguration('vim'); 49 | assert.strictEqual(updatedGlobalVimConfig.get('textwidth'), 80); 50 | 51 | const updatedJsVimConfig = vscode.workspace.getConfiguration('vim', { 52 | languageId: 'javascript', 53 | }); 54 | assert.strictEqual(updatedJsVimConfig.get('textwidth'), 120); 55 | } finally { 56 | await globalVimConfig.update('textwidth', undefined, vscode.ConfigurationTarget.Global); 57 | await jsVimConfig.update('textwidth', undefined, vscode.ConfigurationTarget.Global); 58 | } 59 | }); 60 | 61 | newTest({ 62 | title: 'Can handle long key chords', 63 | start: ['|'], 64 | // fes 65 | keysPressed: ' fes', 66 | end: ['|'], 67 | endMode: Mode.Visual, 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/configuration/langmap.test.ts: -------------------------------------------------------------------------------- 1 | import { Mode } from '../../src/mode/mode'; 2 | import { newTest } from '../testSimplifier'; 3 | import { cleanUpWorkspace, setupWorkspace } from '../testUtils'; 4 | 5 | const dvorakLangmap = 6 | '\'q,\\,w,.e,pr,yt,fy,gu,ci,ro,lp,/[,=],aa,os,ed,uf,ig,dh,hj,tk,nl,s\\;,-\',\\;z,qx,jc,kv,xb,bn,mm,w\\,,v.,z/,[-,]=,"Q,E,PR,YT,FY,GU,CI,RO,LP,?{,+},AA,OS,ED,UF,IG,DH,HJ,TK,NL,S:,_",:Z,QX,JC,KV,XB,BN,MM,W<,V>,Z?'; 7 | 8 | suite('Langmap', () => { 9 | suiteSetup(async () => { 10 | await setupWorkspace({ 11 | config: { 12 | langmap: dvorakLangmap, 13 | }, 14 | }); 15 | }); 16 | suiteTeardown(async () => { 17 | await setupWorkspace({ 18 | config: { 19 | langmap: '', 20 | }, 21 | }); 22 | await cleanUpWorkspace(); 23 | }); 24 | 25 | newTest({ 26 | title: 'Test example binding (ee → dd)', 27 | start: ['lorem ispum', 'dolor |sit amet', 'consectetur adipiscing elit'], 28 | keysPressed: 'ee', 29 | end: ['lorem ispum', '|consectetur adipiscing elit'], 30 | }); 31 | 32 | newTest({ 33 | title: "Remapped keys shouldn't behave like their original mappings. (dd → hh)", 34 | start: ['lorem ispum', 'dolor |sit amet', 'consectetur adipiscing elit'], 35 | keysPressed: 'dd', 36 | end: ['lorem ispum', 'dolo|r sit amet', 'consectetur adipiscing elit'], 37 | }); 38 | 39 | newTest({ 40 | title: "Test macros ('aee'@a → qaeeq@a)", 41 | start: ['|a', 'b', 'c'], 42 | keysPressed: "'aee'@a", 43 | end: ['|c'], 44 | }); 45 | 46 | newTest({ 47 | title: "Test macro register mapping (''ee'@' → qqeeq@q)", 48 | start: ['|a', 'b', 'c'], 49 | keysPressed: "''ee'@'", 50 | end: ['|c'], 51 | }); 52 | 53 | newTest({ 54 | title: "Test no double macro register mapping (',ee'@, → qweeq@w)", 55 | start: ['|a', 'b', 'c'], 56 | keysPressed: "',ee'@,", 57 | end: ['|c'], 58 | }); 59 | 60 | newTest({ 61 | title: "Test marks (mah-a, → maj'a)", 62 | start: ['|a', 'b'], 63 | keysPressed: 'mah-a', 64 | end: ['|a', 'b'], 65 | }); 66 | 67 | newTest({ 68 | title: "Test mark register remapping (m'h-' → mqj'q)", 69 | start: ['|a', 'b'], 70 | keysPressed: "m'h-'", 71 | end: ['|a', 'b'], 72 | }); 73 | 74 | newTest({ 75 | title: "Test no double mark register remapping (m,h-, → mwj'w)", 76 | start: ['|a', 'b'], 77 | keysPressed: 'm,h-,', 78 | end: ['|a', 'b'], 79 | }); 80 | 81 | newTest({ 82 | title: 'Test search (uu → fu)', 83 | start: ['|Hello, how are you?'], 84 | keysPressed: 'uu', 85 | end: ['Hello, how are yo|u?'], 86 | }); 87 | 88 | newTest({ 89 | title: 'Test replacement (pp → rp)', 90 | start: ['|r'], 91 | keysPressed: 'pp', 92 | end: ['|p'], 93 | }); 94 | 95 | newTest({ 96 | title: 'Test no ctrl remapping ()', 97 | start: ['one', 't|wo', 'three'], 98 | keysPressed: '' + 'h' + 'e', 99 | end: ['one', 't|o', 'tree'], 100 | endMode: Mode.Normal, 101 | }); 102 | 103 | newTest({ 104 | title: 'Test no insert mode remapping (cc → ic)', 105 | start: ['|'], 106 | keysPressed: 'c' + 'c', 107 | end: ['c|'], 108 | endMode: Mode.Insert, 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /test/configuration/notation.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { Notation } from '../../src/configuration/notation'; 4 | 5 | suite('Notation', () => { 6 | test('Normalize', () => { 7 | const leaderKey = '//'; 8 | const testCases: { [key: string]: string } = { 9 | '': '', 10 | 'cTrL+x': '', 11 | 'CtRl+y': '', 12 | 'c-z': '', 13 | '': '', 14 | eScapE: '', 15 | hOme: '', 16 | inSert: '', 17 | eNd: '', 18 | '': '//', 19 | LEaDer: '//', 20 | '': '\n', 21 | '': '\n', 22 | '': ' ', 23 | '': '', 24 | '': '', 25 | '': 'J', 26 | '': 'J', 27 | '': 'J', 28 | '': 'J', 29 | }; 30 | 31 | for (const test in testCases) { 32 | if (testCases.hasOwnProperty(test)) { 33 | const expected = testCases[test]; 34 | 35 | const actual = Notation.NormalizeKey(test, leaderKey); 36 | assert.strictEqual(actual, expected); 37 | } 38 | } 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/configuration/validators/neovimValidator.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as path from 'path'; 3 | import * as sinon from 'sinon'; 4 | import fs from 'fs'; 5 | import childProcess from 'child_process'; 6 | import { Configuration } from '../../testConfiguration'; 7 | import { NeovimValidator } from '../../../src/configuration/validators/neovimValidator'; 8 | 9 | suite('Neovim Validator', () => { 10 | let sandbox: sinon.SinonSandbox; 11 | 12 | setup(() => { 13 | sandbox = sinon.createSandbox(); 14 | }); 15 | 16 | teardown(() => { 17 | sandbox.restore(); 18 | }); 19 | 20 | test('neovim enabled without path', async () => { 21 | // setup 22 | const configuration = new Configuration(); 23 | configuration.enableNeovim = true; 24 | configuration.neovimPath = ''; 25 | 26 | const oldPath = process.env.PATH?.slice(); 27 | process.env.PATH = ''; 28 | 29 | // test 30 | const validator = new NeovimValidator(); 31 | const actual = await validator.validate(configuration); 32 | validator.disable(configuration); 33 | 34 | process.env.PATH = oldPath; 35 | 36 | // assert 37 | assert.strictEqual(actual.numErrors, 1); 38 | assert.strictEqual(actual.hasError, true); 39 | assert.strictEqual(configuration.enableNeovim, false); 40 | }); 41 | 42 | // TODO(#4844): this fails on Windows 43 | test('neovim enabled with nvim in path', async () => { 44 | if (process.platform === 'win32') { 45 | return; 46 | } 47 | 48 | // setup 49 | const configuration = new Configuration(); 50 | configuration.enableNeovim = true; 51 | configuration.neovimPath = ''; 52 | 53 | const oldPath = process.env.PATH?.slice(); 54 | process.env.PATH = `/usr/bin${path.delimiter}/some/other/path`; 55 | sandbox.stub(fs, 'existsSync').withArgs('/usr/bin/nvim').returns(true); 56 | sandbox.stub(childProcess, 'execFileSync').withArgs('/usr/bin/nvim', sinon.match.array); 57 | 58 | // test 59 | const validator = new NeovimValidator(); 60 | const actual = await validator.validate(configuration); 61 | 62 | process.env.PATH = oldPath; 63 | 64 | // assert 65 | assert.strictEqual(actual.numErrors, 0); 66 | assert.strictEqual(configuration.enableNeovim, true); 67 | assert.strictEqual(configuration.neovimPath, '/usr/bin/nvim'); 68 | }); 69 | 70 | test('neovim disabled', async () => { 71 | // setup 72 | const configuration = new Configuration(); 73 | configuration.enableNeovim = false; 74 | configuration.neovimPath = ''; 75 | 76 | // test 77 | const validator = new NeovimValidator(); 78 | const actual = await validator.validate(configuration); 79 | 80 | // assert 81 | assert.strictEqual(actual.numErrors, 0); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/configuration/vimrc.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as testConfiguration from '../testConfiguration'; 3 | import * as os from 'os'; 4 | import { vimrc } from '../../src/configuration/vimrc'; 5 | 6 | suite('Vimrc', () => { 7 | const configuration = new testConfiguration.Configuration(); 8 | const vimrcpath = os.homedir(); 9 | configuration.vimrc.enable = true; 10 | 11 | test("Can expand $HOME to user's home directory", async () => { 12 | configuration.vimrc.path = '$HOME'; 13 | 14 | await vimrc.load(configuration); 15 | assert.strictEqual(vimrc.vimrcPath, vimrcpath); 16 | }); 17 | 18 | test("Can expand ~ to user's home directory", async () => { 19 | configuration.vimrc.path = '~'; 20 | 21 | await vimrc.load(configuration); 22 | assert.strictEqual(vimrc.vimrcPath, vimrcpath); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/error.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { ErrorCode, ErrorMessage } from '../src/error'; 4 | 5 | suite('Error', () => { 6 | test('error code has message', () => { 7 | // eslint-disable-next-line guard-for-in 8 | for (const errorCodeString in ErrorCode) { 9 | const errorCode = Number(errorCodeString); 10 | if (!isNaN(errorCode)) { 11 | assert.notStrictEqual(ErrorMessage[errorCode], undefined, errorCodeString); 12 | } 13 | } 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | 4 | import * as srcConfiguration from '../src/configuration/configuration'; 5 | import * as testConfiguration from './testConfiguration'; 6 | 7 | import * as packagejson from '../package.json'; 8 | 9 | suite('package.json', () => { 10 | test('all keys have handlers', async () => { 11 | const registeredCommands = await vscode.commands.getCommands(); 12 | const keybindings = packagejson.contributes.keybindings; 13 | assert.ok(keybindings); 14 | 15 | for (const keybinding of keybindings) { 16 | const found = registeredCommands.includes(keybinding.command); 17 | assert.ok( 18 | found, 19 | 'Missing handler for key=' + keybinding.key + '. Expected handler=' + keybinding.command, 20 | ); 21 | } 22 | }); 23 | 24 | test('all defined configurations in package.json have handlers', async () => { 25 | // package.json 26 | const pkgConfigurations = packagejson.contributes.configuration.properties; 27 | assert.ok(pkgConfigurations); 28 | const keys = Object.keys(pkgConfigurations); 29 | assert.notStrictEqual(keys.length, 0); 30 | 31 | // configuration 32 | let handlers = Object.keys(srcConfiguration.configuration); 33 | let unhandled = keys.filter((k) => handlers.includes(k)); 34 | assert.strictEqual(unhandled.length, 0, 'Missing src handlers for ' + unhandled.join(',')); 35 | 36 | // test configuration 37 | handlers = Object.keys(new testConfiguration.Configuration()); 38 | unhandled = keys.filter((k) => handlers.includes(k)); 39 | assert.strictEqual(unhandled.length, 0, 'Missing test handlers for ' + unhandled.join(',')); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | import glob from 'glob'; 13 | import Mocha from 'mocha'; 14 | import * as path from 'path'; 15 | 16 | import { Globals } from '../src/globals'; 17 | import { Configuration } from './testConfiguration'; 18 | 19 | Globals.isTesting = true; 20 | Globals.mockConfiguration = new Configuration(); 21 | 22 | export function run(): Promise { 23 | const mochaGrep = new RegExp(process.env.MOCHA_GREP || ''); 24 | 25 | // Create the mocha test 26 | const mocha = new Mocha({ 27 | ui: 'tdd', 28 | color: true, 29 | timeout: 10000, 30 | grep: mochaGrep, 31 | }); 32 | 33 | const testsRoot = path.resolve(__dirname, '.'); 34 | 35 | return new Promise((c, e) => { 36 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 37 | if (err) { 38 | return e(err); 39 | } 40 | 41 | // Add files to the test suite 42 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 43 | 44 | try { 45 | // Run the mocha test 46 | mocha.run((failures) => { 47 | if (failures > 0) { 48 | e(new Error(`${failures} tests failed.`)); 49 | } else { 50 | c(); 51 | } 52 | }); 53 | } catch (error) { 54 | console.error(error); 55 | e(error as Error); 56 | } 57 | }); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /test/mode/modeHandler.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { window, workspace } from 'vscode'; 3 | import { ModeHandler } from '../../src/mode/modeHandler'; 4 | import { ModeHandlerMap } from '../../src/mode/modeHandlerMap'; 5 | 6 | suite('ModeHandler', () => { 7 | setup(() => { 8 | ModeHandlerMap.clear(); 9 | }); 10 | 11 | teardown(() => { 12 | ModeHandlerMap.clear(); 13 | }); 14 | 15 | test('ModeHandlerMap', async () => { 16 | let isNew: boolean; 17 | 18 | assert.deepStrictEqual([...ModeHandlerMap.entries()], []); 19 | 20 | const document1 = await workspace.openTextDocument({ content: 'document1' }); 21 | const editor1 = await window.showTextDocument(document1); 22 | 23 | let modeHandler1: ModeHandler; 24 | { 25 | [modeHandler1, isNew] = await ModeHandlerMap.getOrCreate(editor1); 26 | assert.strictEqual(isNew, true); 27 | assert.notStrictEqual(modeHandler1, undefined); 28 | assert.deepStrictEqual( 29 | new Set(ModeHandlerMap.entries()), 30 | new Set([[document1.uri, modeHandler1]]), 31 | ); 32 | } 33 | 34 | const document2 = await workspace.openTextDocument({ content: 'document2' }); 35 | const editor2 = await window.showTextDocument(document2); 36 | 37 | let modeHandler2: ModeHandler; 38 | { 39 | [modeHandler2, isNew] = await ModeHandlerMap.getOrCreate(editor2); 40 | assert.strictEqual(isNew, true); 41 | assert.notStrictEqual(modeHandler2, undefined); 42 | assert.notStrictEqual(modeHandler1, modeHandler2); 43 | assert.deepStrictEqual( 44 | new Set(ModeHandlerMap.entries()), 45 | new Set([ 46 | [document1.uri, modeHandler1], 47 | [document2.uri, modeHandler2], 48 | ]), 49 | ); 50 | } 51 | 52 | // TODO: test closing editor, opening another for same document 53 | // TODO: test delete 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/mode/normalModeTests/matchingBracket.test.ts: -------------------------------------------------------------------------------- 1 | import { newTest } from '../../testSimplifier'; 2 | import { setupWorkspace } from './../../testUtils'; 3 | 4 | suite('Matching Bracket (%)', () => { 5 | suiteSetup(setupWorkspace); 6 | 7 | newTest({ 8 | title: 'before opening parenthesis', 9 | start: ['|one (two)'], 10 | keysPressed: '%', 11 | end: ['one (two|)'], 12 | }); 13 | 14 | newTest({ 15 | title: 'inside parenthesis', 16 | start: ['(|one { two })'], 17 | keysPressed: '%', 18 | end: ['(one { two |})'], 19 | }); 20 | 21 | newTest({ 22 | title: 'nested parenthesis beginning', 23 | start: ['|((( )))'], 24 | keysPressed: '%', 25 | end: ['((( ))|)'], 26 | }); 27 | 28 | newTest({ 29 | title: 'nested parenthesis end', 30 | start: ['((( ))|)'], 31 | keysPressed: '%', 32 | end: ['|((( )))'], 33 | }); 34 | 35 | newTest({ 36 | title: 'nested bracket and parenthesis beginning', 37 | start: ['|[(( ))]'], 38 | keysPressed: '%', 39 | end: ['[(( ))|]'], 40 | }); 41 | 42 | newTest({ 43 | title: 'nested bracket, parenthesis, braces beginning', 44 | start: ['|[(( }}} ))]'], 45 | keysPressed: '%', 46 | end: ['[(( }}} ))|]'], 47 | }); 48 | 49 | newTest({ 50 | title: 'nested bracket, parenthesis, braces end', 51 | start: ['[(( }}} ))|]'], 52 | keysPressed: '%', 53 | end: ['|[(( }}} ))]'], 54 | }); 55 | 56 | newTest({ 57 | title: 'parentheses after >', 58 | start: ['|foo->bar(baz);'], 59 | keysPressed: '%', 60 | end: ['foo->bar(baz|);'], 61 | }); 62 | 63 | newTest({ 64 | title: 'parentheses after "', 65 | start: ['|test "in quotes" [(in brackets)]'], 66 | keysPressed: '%', 67 | end: ['test "in quotes" [(in brackets)|]'], 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/mode/normalModeTests/motionMatchpairs.test.ts: -------------------------------------------------------------------------------- 1 | import { newTest } from '../../testSimplifier'; 2 | import { setupWorkspace } from './../../testUtils'; 3 | 4 | suite('matchpair empty', () => { 5 | setup(async () => { 6 | await setupWorkspace({ config: { matchpairs: '' } }); 7 | }); 8 | 9 | newTest({ 10 | title: "basic motion doesn't work", 11 | start: ['bla |< blubb >'], 12 | keysPressed: '%', 13 | end: ['bla |< blubb >'], 14 | }); 15 | }); 16 | 17 | suite('matchpairs enabled', () => { 18 | suiteSetup(async () => { 19 | await setupWorkspace({ config: { matchpairs: '<:>' } }); 20 | }); 21 | 22 | suite('Tests for % with matchpairs', () => { 23 | newTest({ 24 | title: 'basic jump with %', 25 | start: ['for |< bar > baz'], 26 | keysPressed: '%', 27 | end: ['for < bar |> baz'], 28 | }); 29 | 30 | newTest({ 31 | title: 'basic jump with %. cursor before pair', 32 | start: ['|for < bar > baz'], 33 | keysPressed: '%', 34 | end: ['for < bar |> baz'], 35 | }); 36 | 37 | newTest({ 38 | title: 'backwards jump with %', 39 | start: ['for < bar |> baz'], 40 | keysPressed: '%', 41 | end: ['for |< bar > baz'], 42 | }); 43 | 44 | newTest({ 45 | title: 'nested jump with %', 46 | start: ['for |< < bar > > baz'], 47 | keysPressed: '%', 48 | end: ['for < < bar > |> baz'], 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/number/numericString.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { NumericString, NumericStringRadix } from '../../src/common/number/numericString'; 4 | 5 | suite('numeric string', () => { 6 | test('fails on non-string', () => { 7 | assert.strictEqual(undefined, NumericString.parse('hi')); 8 | }); 9 | 10 | test('handles hex round trip', () => { 11 | const input = '0xa1'; 12 | assert.strictEqual(input, NumericString.parse(input)?.num.toString()); 13 | // run each assertion twice to make sure that regex state doesn't cause failures 14 | assert.strictEqual(input, NumericString.parse(input)?.num.toString()); 15 | }); 16 | 17 | test('handles hex with capitals round trip', () => { 18 | const input = '0xAb1'; 19 | assert.strictEqual('0xab1', NumericString.parse(input)?.num.toString()); 20 | }); 21 | 22 | test('handles decimal round trip', () => { 23 | const input = '9'; 24 | assert.strictEqual(input, NumericString.parse(input)?.num.toString()); 25 | assert.strictEqual(input, NumericString.parse(input)?.num.toString()); 26 | }); 27 | 28 | test('handles octal trip', () => { 29 | const input = '07'; 30 | assert.strictEqual(input, NumericString.parse(input)?.num.toString()); 31 | assert.strictEqual(input, NumericString.parse(input)?.num.toString()); 32 | }); 33 | 34 | test('handles octal trip', () => { 35 | const input = '07'; 36 | assert.strictEqual(input, NumericString.parse(input)?.num.toString()); 37 | assert.strictEqual(input, NumericString.parse(input)?.num.toString()); 38 | }); 39 | 40 | test('handles decimal radix', () => { 41 | assert.strictEqual(NumericString.parse('07', NumericStringRadix.Dec)?.num.value, 7); 42 | assert.strictEqual(NumericString.parse('hi-07hello', NumericStringRadix.Dec)?.num.value, -7); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/operator/comment.test.ts: -------------------------------------------------------------------------------- 1 | import { Mode } from '../../src/mode/mode'; 2 | import { newTest } from '../testSimplifier'; 3 | import { setupWorkspace } from './../testUtils'; 4 | 5 | suite('comment operator', () => { 6 | suiteSetup(async () => { 7 | await setupWorkspace({ fileExtension: '.js' }); 8 | }); 9 | 10 | newTest({ 11 | title: 'gcc comments out current line', 12 | start: ['first line', '|second line'], 13 | keysPressed: 'gcc', 14 | end: ['first line', '|// second line'], 15 | }); 16 | 17 | newTest({ 18 | title: 'gcj comments in current and next line', 19 | start: ['// first| line', '// second line', 'third line'], 20 | keysPressed: 'gcj', 21 | end: ['|first line', 'second line', 'third line'], 22 | }); 23 | 24 | newTest({ 25 | title: 'block comment with motion', 26 | start: ['function test(arg|1, arg2, arg3) {'], 27 | keysPressed: 'gCi)', 28 | end: ['function test(|/* arg1, arg2, arg3 */) {'], 29 | }); 30 | 31 | newTest({ 32 | title: 'block comment in Visual Mode', 33 | start: ['blah |blah blah'], 34 | keysPressed: 'vlllgC', 35 | end: ['blah |/* blah */ blah'], 36 | endMode: Mode.Normal, 37 | }); 38 | 39 | newTest({ 40 | title: 'comment in visual line mode', 41 | start: ['one', '|two', 'three', 'four'], 42 | keysPressed: 'Vjgc', 43 | end: ['one', '|// two', '// three', 'four'], 44 | endMode: Mode.Normal, 45 | }); 46 | 47 | newTest({ 48 | title: 'comment in visual block mode', 49 | start: ['one', '|two', 'three', 'four'], 50 | keysPressed: 'lljgc', 51 | end: ['one', '|// two', '// three', 'four'], 52 | endMode: Mode.Normal, 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/operator/filter.test.ts: -------------------------------------------------------------------------------- 1 | import { newTest } from '../testSimplifier'; 2 | 3 | // TODO(#4844): this fails on Windows 4 | suite('filter operator', () => { 5 | if (process.platform === 'win32') { 6 | return; 7 | } 8 | 9 | newTest({ 10 | title: '!! with no count', 11 | start: ['|'], 12 | keysPressed: '!!echo hello world\n', 13 | end: ['|hello world'], 14 | }); 15 | 16 | newTest({ 17 | title: '!! with whitespace moves cursor to first non-whitespace character', 18 | start: ['|'], 19 | keysPressed: '!!echo " hello world"\n', 20 | end: [' |hello world'], 21 | }); 22 | 23 | newTest({ 24 | title: '!! with count', 25 | start: ['|abc', 'def'], 26 | keysPressed: '2!!echo hello world\n', 27 | end: ['|hello world'], 28 | }); 29 | 30 | newTest({ 31 | title: '!{forwards motion}{filter}', 32 | start: ['|abc', 'def', 'ghi'], 33 | keysPressed: '!jecho hello world\n', 34 | end: ['|hello world', 'ghi'], 35 | }); 36 | 37 | newTest({ 38 | title: '!{backwards motion}{filter}', 39 | start: ['abc', 'def', '|ghi'], 40 | keysPressed: '!{echo hello world\n', 41 | end: ['|hello world', 'ghi'], 42 | }); 43 | 44 | newTest({ 45 | title: 'v!{filter}', 46 | start: ['|abc', 'def', 'ghi'], 47 | keysPressed: 'vjj!echo hello world\n', 48 | end: ['|hello world'], 49 | }); 50 | 51 | newTest({ 52 | title: 'V!{filter}', 53 | start: ['|abc', 'def', 'ghi'], 54 | keysPressed: 'Vjj!echo hello world\n', 55 | end: ['|hello world'], 56 | }); 57 | 58 | newTest({ 59 | title: '!{filter}', 60 | start: ['|abc', 'def', 'ghi'], 61 | keysPressed: 'jj!echo hello world\n', 62 | end: ['|hello world'], 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/operator/format.test.ts: -------------------------------------------------------------------------------- 1 | import { newTest } from '../testSimplifier'; 2 | import { setupWorkspace } from './../testUtils'; 3 | 4 | suite('format operator', () => { 5 | suiteSetup(async () => { 6 | await setupWorkspace({ fileExtension: '.ts' }); 7 | }); 8 | 9 | newTest({ 10 | title: '== formats current line', 11 | start: [' |let a;', ' let b;'], 12 | keysPressed: '==', 13 | end: ['|let a;', ' let b;'], 14 | }); 15 | 16 | newTest({ 17 | title: '=$ formats entire line', 18 | start: [' function f() {|let a;', 'let b;', '}'], 19 | keysPressed: '=$', 20 | end: ['|function f() {', ' let a;', 'let b;', '}'], 21 | }); 22 | 23 | newTest({ 24 | title: '=j formats two lines', 25 | start: [' |let a;', ' let b;', ' let c;'], 26 | keysPressed: '=j', 27 | end: ['|let a;', 'let b;', ' let c;'], 28 | }); 29 | 30 | newTest({ 31 | title: '3=k formats three lines', 32 | start: [' let a;', ' let b;', '| let c;'], 33 | keysPressed: '3=k', 34 | end: ['|let a;', 'let b;', 'let c;'], 35 | }); 36 | 37 | newTest({ 38 | title: '=gg formats to top of file', 39 | start: [' let a;', ' let b;', '| let c;'], 40 | keysPressed: '=gg', 41 | end: ['|let a;', 'let b;', 'let c;'], 42 | }); 43 | 44 | newTest({ 45 | title: '=G formats to bottom of file', 46 | start: ['| let a;', ' let b;', ' let c;'], 47 | keysPressed: '=G', 48 | end: ['|let a;', 'let b;', 'let c;'], 49 | }); 50 | 51 | newTest({ 52 | title: '=ip formats paragraph', 53 | start: [' function f() {', '|let a;', ' }', '', ' let b;'], 54 | keysPressed: '=ip', 55 | end: ['|function f() {', ' let a;', '}', '', ' let b;'], 56 | }); 57 | 58 | newTest({ 59 | title: 'format in visual mode', 60 | start: [' function f() {', 'let a;', '| }', '', ' let b;'], 61 | keysPressed: 'vkk=', 62 | end: ['|function f() {', ' let a;', '}', '', ' let b;'], 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/operator/rot13.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { ROT13Operator } from '../../src/actions/operator'; 4 | import { newTest } from '../testSimplifier'; 5 | 6 | suite('rot13 operator', () => { 7 | test('rot13() unit test', () => { 8 | const testCases = [ 9 | ['abcdefghijklmnopqrstuvwxyz', 'nopqrstuvwxyzabcdefghijklm'], 10 | ['ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'NOPQRSTUVWXYZABCDEFGHIJKLM'], 11 | ['!@#$%^&*()', '!@#$%^&*()'], 12 | ['âéü', 'âéü'], 13 | ]; 14 | for (const [input, output] of testCases) { 15 | assert.strictEqual(ROT13Operator.rot13(input), output); 16 | } 17 | }); 18 | 19 | newTest({ 20 | title: 'g?j works', 21 | start: ['a|bc', 'def', 'ghi'], 22 | keysPressed: 'g?j', 23 | end: ['n|op', 'qrs', 'ghi'], 24 | }); 25 | 26 | newTest({ 27 | title: 'g? in visual mode works', 28 | start: ['a|bc', 'def', 'ghi'], 29 | keysPressed: 'vj$g?', 30 | end: ['a|op', 'qrs', 'ghi'], 31 | }); 32 | 33 | newTest({ 34 | title: 'g? in visual line mode works', 35 | start: ['a|bc', 'def', 'ghi'], 36 | keysPressed: 'Vj$g?', 37 | end: ['|nop', 'qrs', 'ghi'], 38 | }); 39 | 40 | newTest({ 41 | title: 'g? in visual block mode works', 42 | start: ['a|bc', 'def', 'ghi'], 43 | keysPressed: 'j$g?', 44 | end: ['a|op', 'drs', 'ghi'], 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/operator/shift.test.ts: -------------------------------------------------------------------------------- 1 | import { newTest } from '../testSimplifier'; 2 | import { setupWorkspace } from '../testUtils'; 3 | 4 | suite('shift operator', () => { 5 | suiteSetup(setupWorkspace); 6 | 7 | newTest({ 8 | title: 'basic shift left test', 9 | start: [' |zxcv', ' zxcv', ' zxcv'], 10 | keysPressed: '<<', 11 | end: ['|zxcv', ' zxcv', ' zxcv'], 12 | }); 13 | 14 | newTest({ 15 | title: 'shift left goto end test', 16 | start: [' |zxcv', ' zxcv', ' zxcv'], 17 | keysPressed: 'G', 32 | end: [' |zxcv', ' zxcv', ' zxcv'], 33 | }); 34 | 35 | newTest({ 36 | title: 'shift right goto line test', 37 | start: ['|zxcv', 'zxcv', 'zxcv'], 38 | keysPressed: '>2G', 39 | end: [' |zxcv', ' zxcv', 'zxcv'], 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/operator/surrogate.test.ts: -------------------------------------------------------------------------------- 1 | import { newTest } from '../testSimplifier'; 2 | 3 | suite('surrogate-pair', () => { 4 | newTest({ 5 | title: 'yank single hokke', 6 | start: ['|𩸽'], 7 | keysPressed: 'vyp', 8 | end: ['𩸽|𩸽'], 9 | }); 10 | 11 | newTest({ 12 | title: 'move across hokke', 13 | start: ['|𩸽𩸽𩸽𩸽𩸽'], 14 | keysPressed: 'lll', 15 | end: ['𩸽𩸽𩸽|𩸽𩸽'], 16 | }); 17 | 18 | newTest({ 19 | title: 'move and yank triple hokke', 20 | start: ['|𩸽𩸽𩸽'], 21 | keysPressed: 'vllyp', 22 | end: ['𩸽𩸽𩸽|𩸽𩸽𩸽'], 23 | }); 24 | 25 | newTest({ 26 | title: 'yank cute dog and hokke across lines', 27 | start: ['|𩸽𩸽𩸽🐕🐕🐕', '🐕🐕🐕𩸽𩸽𩸽'], 28 | keysPressed: 'vjllyP', 29 | end: ['|𩸽𩸽𩸽🐕🐕🐕', '🐕🐕🐕𩸽𩸽𩸽🐕🐕🐕', '🐕🐕🐕𩸽𩸽𩸽'], 30 | }); 31 | 32 | newTest({ 33 | title: 'insert a cute dog', 34 | start: ['|'], 35 | keysPressed: 'i🐕weee', 36 | end: ['🐕weee|'], 37 | }); 38 | 39 | newTest({ 40 | title: 'insert some more cute dogs', 41 | start: ['|'], 42 | keysPressed: 'i🐕🐕', 43 | end: ['🐕🐕|'], 44 | }); 45 | 46 | newTest({ 47 | title: 'move left over cute dog', 48 | start: ['|𩸽🐕', 'text'], 49 | keysPressed: 'jlllkh', 50 | end: ['|𩸽🐕', 'text'], 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/plugins/imswitcher.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { InputMethodSwitcher } from '../../src/actions/plugins/imswitcher'; 3 | import { Mode } from '../../src/mode/mode'; 4 | import { setupWorkspace } from '../testUtils'; 5 | 6 | suite('Input method plugin', () => { 7 | let savedCmd = ''; 8 | 9 | const fakeExecuteChinese = (cmd: string): Promise => { 10 | return new Promise((resolve, reject) => { 11 | if (cmd === 'im-select') { 12 | resolve('chinese'); 13 | } else { 14 | savedCmd = cmd; 15 | resolve(''); 16 | } 17 | }); 18 | }; 19 | 20 | const fakeExecuteDefault = (cmd: string): Promise => { 21 | return new Promise((resolve, reject) => { 22 | if (cmd === 'im-select') { 23 | resolve('default'); 24 | } else { 25 | savedCmd = cmd; 26 | resolve(''); 27 | } 28 | }); 29 | }; 30 | 31 | setup(async () => { 32 | await setupWorkspace({ 33 | config: { 34 | autoSwitchInputMethod: { 35 | enable: true, 36 | defaultIM: 'default', 37 | obtainIMCmd: 'im-select', 38 | switchIMCmd: 'im-select {im}', 39 | }, 40 | }, 41 | }); 42 | }); 43 | 44 | test('use default im in insert mode', async () => { 45 | savedCmd = ''; 46 | const inputMethodSwitcher = new InputMethodSwitcher(fakeExecuteDefault); 47 | await inputMethodSwitcher.switchInputMethod(Mode.Normal, Mode.Insert); 48 | assert.strictEqual('', savedCmd); 49 | await inputMethodSwitcher.switchInputMethod(Mode.Insert, Mode.Normal); 50 | assert.strictEqual('', savedCmd); 51 | await inputMethodSwitcher.switchInputMethod(Mode.Normal, Mode.Insert); 52 | assert.strictEqual('', savedCmd); 53 | await inputMethodSwitcher.switchInputMethod(Mode.Insert, Mode.Normal); 54 | assert.strictEqual('', savedCmd); 55 | }); 56 | 57 | test('use other im in insert mode', async () => { 58 | savedCmd = ''; 59 | const inputMethodSwitcher = new InputMethodSwitcher(fakeExecuteChinese); 60 | await inputMethodSwitcher.switchInputMethod(Mode.Normal, Mode.Insert); 61 | assert.strictEqual('', savedCmd); 62 | await inputMethodSwitcher.switchInputMethod(Mode.Insert, Mode.Normal); 63 | assert.strictEqual('im-select default', savedCmd); 64 | await inputMethodSwitcher.switchInputMethod(Mode.Normal, Mode.Insert); 65 | assert.strictEqual('im-select chinese', savedCmd); 66 | await inputMethodSwitcher.switchInputMethod(Mode.Insert, Mode.Normal); 67 | assert.strictEqual('im-select default', savedCmd); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/register/repeatableMovement.test.ts: -------------------------------------------------------------------------------- 1 | import { newTest } from '../testSimplifier'; 2 | 3 | suite('Repeatable movements with f and t', () => { 4 | newTest({ 5 | title: 'Can repeat f', 6 | start: ['|abc abc abc'], 7 | keysPressed: 'fa;', 8 | end: ['abc abc |abc'], 9 | }); 10 | 11 | newTest({ 12 | title: 'Can repeat reversed F', 13 | start: ['|abc abc abc'], 14 | keysPressed: 'fa$,', 15 | end: ['abc abc |abc'], 16 | }); 17 | 18 | newTest({ 19 | title: 'Can repeat t', 20 | start: ['|abc abc abc'], 21 | keysPressed: 'tc;', 22 | end: ['abc a|bc abc'], 23 | }); 24 | 25 | newTest({ 26 | title: 'Can repeat N times reversed t', 27 | start: ['|abc abc abc abc'], 28 | keysPressed: 'tc$3,', 29 | end: ['abc| abc abc abc'], 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /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 the extension test runner script 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 | // Disable other extensions while running tests for avoiding unexpected side-effect 20 | launchArgs: ['--disable-extensions'], 21 | }); 22 | } catch (err) { 23 | console.error(err); 24 | console.error('Failed to run tests'); 25 | process.exit(1); 26 | } 27 | } 28 | 29 | void main(); 30 | -------------------------------------------------------------------------------- /test/state/vimState.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | import { EasyMotion } from '../../src/actions/plugins/easymotion/easymotion'; 4 | import { Position } from 'vscode'; 5 | import { Cursor } from '../../src/common/motion/cursor'; 6 | import { VimState } from '../../src/state/vimState'; 7 | 8 | suite('VimState', () => { 9 | test('de-dupes cursors', async () => { 10 | // setup 11 | const vimState = new VimState(vscode.window.activeTextEditor!, new EasyMotion()); 12 | await vimState.load(); 13 | const cursorStart = new Position(0, 0); 14 | const cursorStop = new Position(0, 1); 15 | const initialCursors = [ 16 | new Cursor(cursorStart, cursorStop), 17 | new Cursor(cursorStart, cursorStop), 18 | ]; 19 | 20 | // test 21 | vimState.cursors = initialCursors; 22 | 23 | // assert 24 | assert.strictEqual(vimState.cursors.length, 1); 25 | }); 26 | 27 | test('cursorStart/cursorStop should be first cursor in cursors', async () => { 28 | // setup 29 | const vimState = new VimState(vscode.window.activeTextEditor!, new EasyMotion()); 30 | await vimState.load(); 31 | const cursorStart = new Position(0, 0); 32 | const cursorStop = new Position(0, 1); 33 | const initialCursors = [ 34 | new Cursor(cursorStart, cursorStop), 35 | new Cursor(new Position(1, 0), new Position(1, 1)), 36 | ]; 37 | 38 | // test 39 | vimState.cursors = initialCursors; 40 | 41 | // assert 42 | assert.strictEqual(vimState.cursors.length, 2); 43 | assert.strictEqual(vimState.isMultiCursor, true); 44 | vimState.cursorStartPosition = cursorStart; 45 | vimState.cursorStopPosition = cursorStop; 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/vimscript/searchOffset.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'assert'; 2 | import { SearchOffset } from '../../src/vimscript/pattern'; 3 | 4 | function parseTest(name: string, input: string, output: SearchOffset) { 5 | test(name, () => { 6 | assert.deepStrictEqual(SearchOffset.parser.tryParse(input), output); 7 | }); 8 | } 9 | 10 | suite('SearchOffset parsing', () => { 11 | parseTest('+', '+', new SearchOffset({ type: 'lines', delta: 1 })); 12 | parseTest('-', '-', new SearchOffset({ type: 'lines', delta: -1 })); 13 | 14 | parseTest('[num]', '123', new SearchOffset({ type: 'lines', delta: 123 })); 15 | parseTest('+[num]', '+123', new SearchOffset({ type: 'lines', delta: 123 })); 16 | parseTest('-[num]', '-123', new SearchOffset({ type: 'lines', delta: -123 })); 17 | 18 | parseTest('e', 'e', new SearchOffset({ type: 'chars_from_end', delta: 0 })); 19 | parseTest('e+', 'e+', new SearchOffset({ type: 'chars_from_end', delta: 1 })); 20 | parseTest('e-', 'e-', new SearchOffset({ type: 'chars_from_end', delta: -1 })); 21 | parseTest('e[num]', 'e123', new SearchOffset({ type: 'chars_from_end', delta: 123 })); 22 | parseTest('e+[num]', 'e+123', new SearchOffset({ type: 'chars_from_end', delta: 123 })); 23 | parseTest('e-[num]', 'e-123', new SearchOffset({ type: 'chars_from_end', delta: -123 })); 24 | 25 | parseTest('s', 's', new SearchOffset({ type: 'chars_from_start', delta: 0 })); 26 | parseTest('s+', 's+', new SearchOffset({ type: 'chars_from_start', delta: 1 })); 27 | parseTest('s-', 's-', new SearchOffset({ type: 'chars_from_start', delta: -1 })); 28 | parseTest('s[num]', 's123', new SearchOffset({ type: 'chars_from_start', delta: 123 })); 29 | parseTest('s+[num]', 's+123', new SearchOffset({ type: 'chars_from_start', delta: 123 })); 30 | parseTest('s-[num]', 's-123', new SearchOffset({ type: 'chars_from_start', delta: -123 })); 31 | 32 | parseTest('b', 'b', new SearchOffset({ type: 'chars_from_start', delta: 0 })); 33 | parseTest('b+', 'b+', new SearchOffset({ type: 'chars_from_start', delta: 1 })); 34 | parseTest('b-', 'b-', new SearchOffset({ type: 'chars_from_start', delta: -1 })); 35 | parseTest('b[num]', 'b123', new SearchOffset({ type: 'chars_from_start', delta: 123 })); 36 | parseTest('b+[num]', 'b+123', new SearchOffset({ type: 'chars_from_start', delta: 123 })); 37 | parseTest('b-[num]', 'b-123', new SearchOffset({ type: 'chars_from_start', delta: -123 })); 38 | 39 | // TODO: ;{pattern} 40 | }); 41 | 42 | // TODO: Write these tests 43 | // suite('SearchOffset application', () => {}); 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2021"], 4 | "module": "commonjs", 5 | "target": "ES2021", 6 | "outDir": "out", 7 | "noImplicitOverride": true, 8 | "noImplicitReturns": true, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "sourceMap": true, 12 | "strict": true, 13 | "useUnknownInCatchVariables": false, 14 | "experimentalDecorators": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "platform/*": ["src/platform/node/*"] 18 | }, 19 | "resolveJsonModule": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "esModuleInterop": true 22 | // "isolatedModules": true, 23 | }, 24 | "exclude": ["node_modules", "!node_modules/@types"] 25 | } 26 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const prod_configs = require('./webpack.config.js'); 3 | 4 | module.exports = [ 5 | merge.merge(prod_configs[0], { 6 | mode: 'development', 7 | devtool: 'source-map', 8 | optimization: { 9 | minimize: false, 10 | removeAvailableModules: false, 11 | removeEmptyChunks: false, 12 | splitChunks: false, 13 | }, 14 | }), 15 | ]; 16 | --------------------------------------------------------------------------------