1
0
Fork 0
mirror of synced 2024-12-25 00:03:20 -05:00

Update Coc.nvim

This commit is contained in:
Kurtis Moxley 2022-07-05 14:25:26 +08:00
parent 9c0283f83b
commit 45613661bc
376 changed files with 93665 additions and 505 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,22 @@
root = true
[*]
end_of_line = lf
charset = utf-8
[*.{js,ts}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
max_line_length = 120
[*.json]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.vim]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
max_line_length = 120

View file

@ -0,0 +1,5 @@
node_modules
coverage
build
lib
typings

View file

@ -0,0 +1,339 @@
module.exports = {
"root": true,
"env": {
"es6": true,
"node": true,
"jest/globals": true
},
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json",
"sourceType": "module"
},
"plugins": [
"jsdoc",
"jest",
"@typescript-eslint"
],
"rules": {
"comma-dangle": [
0
],
"guard-for-in": [
0
],
"no-dupe-class-members": [
0
],
"prefer-spread": [
0
],
"prefer-rest-params": [
0
],
"func-names": [
0
],
"require-atomic-updates": [
0
],
"no-empty": "off",
"no-console": "off",
"linebreak-style": [
1,
"unix"
],
"no-prototype-builtins": [
0
],
"no-unused-vars": [
0
],
"no-async-promise-executor": [
0
],
"constructor-super": "error",
"for-direction": [
"error"
],
"getter-return": [
"error"
],
"no-case-declarations": [
"error"
],
"no-class-assign": [
"error"
],
"no-compare-neg-zero": [
"error"
],
"no-cond-assign": "error",
"no-const-assign": [
"error"
],
"no-constant-condition": [
"error"
],
"no-control-regex": [
"error"
],
"no-debugger": "error",
"no-delete-var": [
"error"
],
"no-dupe-args": [
"error"
],
"no-dupe-keys": [
"error"
],
"no-duplicate-case": [
"error"
],
"no-empty-character-class": [
"error"
],
"no-empty-pattern": [
"error"
],
"no-ex-assign": [
"error"
],
"no-extra-boolean-cast": [
"error"
],
"no-extra-semi": [
"error"
],
"no-fallthrough": "off",
"no-func-assign": [
"error"
],
"no-global-assign": [
"error"
],
"no-inner-declarations": [
"error"
],
"no-invalid-regexp": [
"error"
],
"no-irregular-whitespace": "error",
"no-misleading-character-class": [
"error"
],
"no-mixed-spaces-and-tabs": [
"error"
],
"no-new-symbol": [
"error"
],
"no-obj-calls": [
"error"
],
"no-octal": [
"error"
],
"no-redeclare": "error",
"no-regex-spaces": [
"error"
],
"no-self-assign": [
"error"
],
"no-shadow-restricted-names": [
"error"
],
"no-sparse-arrays": "error",
"no-this-before-super": [
"error"
],
"no-undef": [
"off"
],
"no-unexpected-multiline": [
"error"
],
"no-unreachable": [
"warn"
],
"no-unsafe-finally": "error",
"no-unsafe-negation": [
"error"
],
"no-unused-labels": "error",
"no-useless-catch": [
"error"
],
"no-useless-escape": [
"error"
],
"no-with": [
"error"
],
"require-yield": [
"error"
],
"use-isnan": "error",
"valid-typeof": "off",
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off",
"@typescript-eslint/prefer-string-starts-ends-with": "off",
"@typescript-eslint/prefer-regexp-exec": "off",
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": "off",
"@typescript-eslint/require-await": "off",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/restrict-plus-operands": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
"accessibility": "explicit",
"overrides": {
"accessors": "explicit",
"constructors": "off"
}
}
],
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/member-delimiter-style": "off",
"@typescript-eslint/camelcase": "off",
"@typescript-eslint/member-ordering": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-parameter-properties": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/prefer-for-of": "off",
"@typescript-eslint/prefer-function-type": "off",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/quotes": "off",
"@typescript-eslint/semi": [
"error",
"never"
],
"@typescript-eslint/triple-slash-reference": [
"error",
{
"path": "always",
"types": "prefer-import",
"lib": "always"
}
],
"@typescript-eslint/unbound-method": "off",
"@typescript-eslint/unified-signatures": "error",
"arrow-body-style": "off",
"arrow-parens": [
"error",
"as-needed"
],
"camelcase": "off",
"complexity": "off",
"curly": "off",
"dot-notation": "off",
"eol-last": "off",
"eqeqeq": [
"off",
"always"
],
"id-blacklist": [
"error",
"any",
"Number",
"number",
"String",
"string",
"Boolean",
"boolean",
"Undefined"
],
"id-match": "error",
"jsdoc/check-alignment": "error",
"jsdoc/check-indentation": "error",
"jsdoc/newline-after-description": "error",
"max-classes-per-file": "off",
"new-parens": "error",
"no-bitwise": "off",
"no-caller": "error",
"no-eval": "error",
"no-invalid-this": "off",
"no-magic-numbers": "off",
"no-multiple-empty-lines": [
"error",
{
"max": 1
}
],
"no-new-wrappers": "error",
"no-shadow": [
"off",
{
"hoist": "all"
}
],
"no-template-curly-in-string": "off",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef-init": "error",
"no-underscore-dangle": "off",
"no-unused-expressions": "off",
"no-var": "error",
"no-void": "off",
"object-shorthand": "error",
"one-var": [
"error",
"never"
],
"prefer-const": "off",
"prefer-template": "off",
"quote-props": [
"error",
"as-needed"
],
"radix": "error",
"space-before-function-paren": [
"error",
{
"anonymous": "never",
"asyncArrow": "always",
"named": "never"
}
],
"spaced-comment": [
"error",
"always",
{
"markers": [
"/"
]
}
]
},
"settings": {}
}

View file

@ -0,0 +1,3 @@
coverage:
status:
patch: off

View file

@ -0,0 +1,4 @@
# These are supported funding model platforms
open_collective: cocnvim
patreon: chemzqm

View file

@ -0,0 +1,45 @@
---
name: Bug report
about: Create a report to help us improve
---
<!--
**Warning: We will close the bug issue without the issue template and the reproduce ways.**
If you have question, please ask at https://gitter.im/neoclide/coc.nvim
If the problem related to specific language server, please checkout: https://git.io/fjCEM
If your have performance issue, checkout: https://git.io/fjCEX & https://git.io/Jfe00
-->
## Result from CocInfo
<!--Run `:CocInfo` command and paste the content below.-->
## Describe the bug
A clear and concise description of what the bug is.
## Reproduce the bug
**We will close your issue when you don't provide minimal vimrc and we can't
reproduce it**
- Create file `mini.vim` with
```vim
set nocompatible
set runtimepath^=/path/to/coc.nvim
filetype plugin indent on
syntax on
set hidden
```
- Start (neo)vim with command: `vim -u mini.vim`
- Operate vim.
## Screenshots (optional)
If applicable, add screenshots to help explain your problem.

View file

@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -0,0 +1,73 @@
name: Dev
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
test:
if: github.event.pull_request.draft == false
timeout-minutes: 60
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node:
- "16"
- "14"
nvim:
- "0.7.0"
include:
# only enable coverage on the fastest job
- node: "16"
ENABLE_CODE_COVERAGE: true
env:
NODE_ENV: test
steps:
- name: Checkout
uses: actions/checkout@v2.3.4
with:
fetch-depth: 2
- name: Setup Node.js ${{ matrix.node }}
uses: actions/setup-node@v2.4.0
with:
node-version: ${{ matrix.node }}
cache: "yarn"
- name: Setup python3
uses: actions/setup-python@v2
with:
python-version: '3.9'
cache: 'pip'
- run: pip install pynvim
- name: Install Dependencies
run: |
yarn global add typescript
yarn install --frozen-lockfile
- name: Setup nvim ${{ matrix.nvim }}
run: |
sudo apt-get install -y xclip ripgrep ctags
xclip -version
rg --version
ctags --version
curl -LO https://github.com/neovim/neovim/releases/download/v${{ matrix.nvim }}/nvim-linux64.tar.gz
tar xzf nvim-linux64.tar.gz
export PATH="${PATH}:node_modules/.bin:$(pwd)/nvim-linux64/bin"
nvim --version
yarn test-build --maxWorkers=2
- name: Codecov
uses: codecov/codecov-action@v1
if: ${{ matrix.ENABLE_CODE_COVERAGE }}
with:
fail_ci_if_error: true

View file

@ -0,0 +1,35 @@
name: Lint
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
lint:
if: github.event.pull_request.draft == false
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2.3.4
- name: Setup Node.js
uses: actions/setup-node@v2.4.0
with:
cache: "yarn"
- name: Install Dependencies
run: yarn install --frozen-lockfile
- name: Check Types by TSC
run: yarn lint:typecheck
- name: Lint ESLint
run: yarn lint
- name: Check Lock File Changes
run: yarn && echo "Listing changed files:" && git diff --name-only --exit-code && echo "No files changed during lint."

View file

@ -1,13 +1,16 @@
lib
.cache
*.map
coverage
__pycache__
.pyc
.log
src
publish.sh
build
doc/tags
doc/tags-cn
typings/package.json
node_modules
src/__tests__/tags
typings
publish.sh
release.sh
!src/__tests__/tags
src/__tests__/extensions/db.json
package-lock.json

View file

@ -0,0 +1 @@
lib

View file

@ -0,0 +1,16 @@
*.map
.cache
lib/extensions
lib/__tests__
plugin
autoload
rplugin
src
.github
build
coverage
data
tslint.json
tsconfig.json
.zip
.DS_Store

View file

@ -0,0 +1 @@
src/

View file

@ -0,0 +1,16 @@
{
"sourceMaps": false,
"module": {
"type": "es6"
},
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": false,
"dynamicImport": false,
"decorators": false
},
"loose": true,
"target": "es2016"
}
}

View file

@ -0,0 +1,7 @@
{
"eslint.validate": ["typescript"],
"eslint.lintTask.options": [".", "--ext", ".ts"],
"typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": false,
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true,
"typescript.suggestionActions.enabled": false
}

View file

@ -0,0 +1,207 @@
# Backers
❤️ coc.nvim? Help us keep it alive by [donating funds](https://www.bountysource.com/teams/coc-nvim)😘!
<a href="https://github.com/oblitum" target="_blank" title="oblitum">
<img src="https://github.com/oblitum.png?size=64" width="64" height="64" alt="oblitum">
</a>
<a href="https://github.com/free-easy" target="_blank" title="free-easy">
<img src="https://github.com/free-easy.png?size=64" width="64" height="64" alt="free-easy">
</a>
<a href="https://github.com/ruanyl" target="_blank" title="ruanyl">
<img src="https://github.com/ruanyl.png?size=64" width="64" height="64" alt="ruanyl">
</a>
<a href="https://github.com/robjuffermans" target="_blank" title="robjuffermans">
<img src="https://github.com/robjuffermans.png?size=64" width="64" height="64" alt="robjuffermans">
</a>
<a href="https://github.com/iamcco" target="_blank" title="iamcco">
<img src="https://github.com/iamcco.png?size=64" width="64" height="64" alt="iamcco">
</a>
<a href="https://github.com/phcerdan" target="_blank" title="phcerdan">
<img src="https://github.com/phcerdan.png?size=64" width="64" height="64" alt="phcerdan">
</a>
<a href="https://github.com/sarene" target="_blank" title="sarene">
<img src="https://github.com/sarene.png?size=64" width="64" height="64" alt="sarene">
</a>
<a href="https://github.com/robtrac" target="_blank" title="robtrac">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals89_puer8v.png" width="64" height="64" alt="robtrac">
</a>
<a href="https://github.com/raidou" target="_blank" title="raidou">
<img src="https://github.com/raidou.png?size=64" width="64" height="64" alt="raidou">
</a>
<a href="https://github.com/tomspeak" target="_blank" title="tomspeak">
<img src="https://github.com/tomspeak.png?size=64" width="64" height="64" alt="tomspeak">
</a>
<a href="https://github.com/taigacute" target="_blank" title="taigacute">
<img src="https://github.com/taigacute.png?size=64" width="64" height="64" alt="taigacute">
</a>
<a href="https://github.com/weirongxu" target="_blank" title="weirongxu">
<img src="https://github.com/weirongxu.png?size=64" width="64" height="64" alt="weirongxu">
</a>
<a href="https://github.com/tbo" target="_blank" title="tbo">
<img src="https://github.com/tbo.png?size=64" width="64" height="64" alt="tbo">
</a>
<a href="https://github.com/darthShadow" target="_blank" title="darthShadow">
<img src="https://github.com/darthShadow.png?size=64" width="64" height="64" alt="darthShadow">
</a>
<a href="https://github.com/yatli" target="_blank" title="yatli">
<img src="https://github.com/yatli.png?size=64" width="64" height="64" alt="yatli">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/gravatar/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/f8fbc5df2432deac7557cf5e111439f2" width="64" height="64" alt="Matt Greer">
</a>
<a href="#Backers">
<img src="https://avatars0.githubusercontent.com/u/2914269?v=4&s=100&s=400" width="64" height="64" alt="malob">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/gravatar/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/a8b8103b9131cdf694bea446881c05fb" width="64" height="64" alt="Emigre">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals27_bjhsl8.png" width="64" height="64" alt="OkanEsen">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals57_yatmux.png" width="64" height="64" alt="Lennaert Meijvogel">
</a>
<a href="#Backers">
<img src="https://avatars2.githubusercontent.com/u/557201?s=400&u=ac96c9da87099c27f094eec935a627cb32fdfdf2&v=4&s=400" width="64" height="64" alt="Nils Landt">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals10_mjtuws.png" width="64" height="64" alt="dlants">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals45_ecgl95.png" width="64" height="64" alt="RCVU">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals71_wi5cvo.png" width="64" height="64" alt="yatli">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/gravatar/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/2986e67e29cf2ad3de088f9f8bc131cf" width="64" height="64" alt="mikker">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/gravatar/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/8703a88e1c178112625bcb6970ed40e4" width="64" height="64" alt="Velovix">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals51_byhedz.png" width="64" height="64" alt="stCarolas">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals67_rzqguf.png" width="64" height="64" alt="Robbie Clarken">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/svdunc4lofagkaeobpar.png" width="64" height="64" alt="hallettj">
</a>
<a href="#Backers">
<img src="https://avatars0.githubusercontent.com/u/6803419?v=4&s=100&s=400" width="64" height="64" alt="appelgriebsch">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals75_a0xqeq.png" width="64" height="64" alt="cosminadrianpopescu">
</a>
<a href="#Backers">
<img src="https://avatars3.githubusercontent.com/u/301015?v=4&s=100&s=400" width="64" height="64" alt="partizan">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals24_s1h7ax.png" width="64" height="64" alt="ksaldana1">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals63_olgqd6.png" width="64" height="64" alt="jesperryom">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals70_t5kjmo.png" width="64" height="64" alt="JackCA">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals38_vwccce.png" width="64" height="64" alt="peymanmortazavi">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals92_htl0if.png" width="64" height="64" alt="jonaustin">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals33_ch4hs0.png" width="64" height="64" alt="Yuriy Ivanyuk">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals26_knlvug.png" width="64" height="64" alt="abenz1267">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals100_g8py5g.png" width="64" height="64" alt="Sh3Rm4n">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals14_bnuacq.png" width="64" height="64" alt="mwcz">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals78_hleldd.png" width="64" height="64" alt="Philipp-M">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals37_sikg8d.png" width="64" height="64" alt="gvelchuru">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals62_hxul6y.png" width="64" height="64" alt="JSamir">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals19_zafwti.png" width="64" height="64" alt="toby de havilland">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals97_iuw00n.png" width="64" height="64" alt="viniciusarcanjo">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals70_t5kjmo.png" width="64" height="64" alt="Mike Hearn">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals87_vnmrie.png" width="64" height="64" alt="darsto">
</a>
<a href="#Backers">
<img src="https://avatars2.githubusercontent.com/u/145502?v=4&s=100&s=400" width="64" height="64" alt="pyrho">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals102_hqrga7.png" width="64" height="64" alt="Frydac">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals90_qlafi0.png" width="64" height="64" alt="gsa9">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals16_qlob5k.png" width="64" height="64" alt="_andys8">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals27_bjhsl8.png" width="64" height="64" alt="iago-lito">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals44_xa5xwi.png" width="64" height="64" alt="ddaletski">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals83_ryixly.png" width="64" height="64" alt="jonatan-branting">
</a>
<a href="#Backers">
<img src="https://avatars3.githubusercontent.com/u/8683947?v=4&s=100&s=400" width="64" height="64" alt="yutakatay">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals87_vnmrie.png" width="64" height="64" alt="kevinrambaud">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals76_g3jfjp.png" width="64" height="64" alt="tomaskallup">
</a>
<a href="#Backers">
<img src="https://cloudinary-a.akamaihd.net/bountysource/image/upload/d_noaoqqwxegvmulwus0un.png,c_pad,w_400,h_400,b_white/Bountysource_Animals46_qe2ye0.png" width="64" height="64" alt="LewisSteele">
</a>
## 微信扫码赞助者
- free-easy
- sarene
- tomspeak
- robtrac
- 葫芦小金刚
- leo 陶
- 飞翔的白斩鸡
- mark_ll
- 火冷
- Solomon
- 李宇星
- Yus
- IndexXuan
- Sniper
- 陈达野
- 胖听
- Jimmy
- lightxue
- 小亦俊
- 周慎敏
- 凤鸣
- Wilson
- Abel

View file

@ -0,0 +1,142 @@
# Contributing
## How do I... <a name="toc"></a>
- [Use This Guide](#introduction)?
- Make Something? 🤓👩🏽‍💻📜🍳
- [Project Setup](#project-setup)
- [Contribute Documentation](#contribute-documentation)
- [Contribute Code](#contribute-code)
- Manage Something ✅🙆🏼💃👔
- [Provide Support on Issues](#provide-support-on-issues)
- [Review Pull Requests](#review-pull-requests)
- [Join the Project Team](#join-the-project-team)
## Introduction
Thank you so much for your interest in contributing!. All types of contributions are encouraged and valued. See the [table of contents](#toc) for different ways to help and details about how this project handles them!📝
The [Project Team](#join-the-project-team) looks forward to your contributions. 🙌🏾✨
## Project Setup
So you wanna contribute some code! That's great! This project uses GitHub Pull Requests to manage contributions, so [read up on how to fork a GitHub project and file a PR](https://guides.github.com/activities/forking) if you've never done it before.
If this seems like a lot or you aren't able to do all this setup, you might also be able to [edit the files directly](https://help.github.com/articles/editing-files-in-another-user-s-repository/) without having to do any of this setup. Yes, [even code](#contribute-code).
If you want to go the usual route and run the project locally, though:
- [Install Node.js](https://nodejs.org/en/download/)
- [Install Yarn](https://yarnpkg.com)
- [Fork the project](https://guides.github.com/activities/forking/#fork)
Then in your terminal:
- Add coc.nvim to your vim's rtp by `set runtimepath^=/path/to/coc.nvim`
- `cd path/to/your/coc.nvim`
- `yarn install`
- Install [coc-tsserver](https://github.com/neoclide/coc-tsserver) by
`:CocInstall coc-tsserver` in your vim
- Install [coc-tslint-plugin](https://github.com/neoclide/coc-tslint-plugin) by
`:CocInstall coc-tslint-plugin` in your vim.
And you should be ready to go!
## Contribute Documentation
Documentation is a super important, critical part of this project. Docs are how we keep track of what we're doing, how, and why. It's how we stay on the same page about our policies. And it's how we tell others everything they need in order to be able to use this project -- or contribute to it. So thank you in advance.
Documentation contributions of any size are welcome! Feel free to file a PR even if you're just rewording a sentence to be more clear, or fixing a spelling mistake!
To contribute documentation:
- [Set up the project](#project-setup).
- Edit or add any relevant documentation.
- Make sure your changes are formatted correctly and consistently with the rest of the documentation.
- Re-read what you wrote, and run a spellchecker on it to make sure you didn't miss anything.
- In your commit message(s), begin the first line with `docs:`. For example: `docs: Adding a doc contrib section to CONTRIBUTING.md`.
- Write clear, concise commit message(s) using [conventional-changelog format](https://github.com/conventional-changelog/conventional-changelog-angular/blob/master/convention.md). Documentation commits should use `docs(<component>): <message>`.
- Go to https://github.com/neoclide/coc.nvim/pulls and open a new pull request with your changes.
- If your PR is connected to an open issue, add a line in your PR's description that says `Fixes: #123`, where `#123` is the number of the issue you're fixing.
## Contribute Code
We like code commits a lot! They're super handy, and they keep the project going and doing the work it needs to do to be useful to others.
Code contributions of just about any size are acceptable!
The main difference between code contributions and documentation contributions is that contributing code requires inclusion of relevant tests for the code being added or changed. Contributions without accompanying tests will be held off until a test is added, unless the maintainers consider the specific tests to be either impossible, or way too much of a burden for such a contribution.
To contribute code:
- [Set up the project](#project-setup).
- Make any necessary changes to the source code.
- Include any [additional documentation](#contribute-documentation) the changes might need.
- Make sure the code doesn't have lint issue by command `yarn lint` in your
terminal.
- Write tests that verify that your contribution works as expected when necessary.
- Make sure all tests passed by command `yarn jest` in your terminal.
- Write clear, concise commit message(s) using [conventional-changelog format](https://github.com/conventional-changelog/conventional-changelog-angular/blob/master/convention.md).
- Dependency updates, additions, or removals must be in individual commits, and the message must use the format: `<prefix>(deps): PKG@VERSION`, where `<prefix>` is any of the usual `conventional-changelog` prefixes, at your discretion.
- Go to https://github.com/neoclide/coc.nvim/pulls and open a new pull request with your changes.
- If your PR is connected to an open issue, add a line in your PR's description that says `Fixes: #123`, where `#123` is the number of the issue you're fixing.
Once you've filed the PR:
- Barring special circumstances, maintainers will not review PRs until all checks pass (Travis, AppVeyor, etc).
- One or more maintainers will use GitHub's review feature to review your PR.
- If the maintainer asks for any changes, edit your changes, push, and ask for another review. Additional tags (such as `needs-tests`) will be added depending on the review.
- If the maintainer decides to pass on your PR, they will thank you for the contribution and explain why they won't be accepting the changes. That's ok! We still really appreciate you taking the time to do it, and we don't take that lightly. 💚
- If your PR gets accepted, it will be marked as such, and merged into the `latest` branch soon after. Your contribution will be distributed to the masses next time the maintainers [tag a release](#tag-a-release)
## Provide Support on Issues
[Needs Collaborator](#join-the-project-team): none
Helping out other users with their questions is a really awesome way of contributing to any community. It's not uncommon for most of the issues on an open source projects being support-related questions by users trying to understand something they ran into, or find their way around a known bug.
Sometimes, the `support` label will be added to things that turn out to actually be other things, like bugs or feature requests. In that case, suss out the details with the person who filed the original issue, add a comment explaining what the bug is, and change the label from `support` to `bug` or `feature`. If you can't do this yourself, @mention a maintainer so they can do it.
In order to help other folks out with their questions:
- Go to the issue tracker and [filter open issues by the `support` label](https://github.com/neoclide/coc.nvim/issues?q=is%3Aopen+is%3Aissue+label%3Asupport).
- Read through the list until you find something that you're familiar enough with to give an answer to.
- Respond to the issue with whatever details are needed to clarify the question, or get more details about what's going on.
- Once the discussion wraps up and things are clarified, either close the issue, or ask the original issue filer (or a maintainer) to close it for you.
Some notes on picking up support issues:
- Avoid responding to issues you don't know you can answer accurately.
- As much as possible, try to refer to past issues with accepted answers. Link to them from your replies with the `#123` format.
- Be kind and patient with users -- often, folks who have run into confusing things might be upset or impatient. This is ok. Try to understand where they're coming from, and if you're too uncomfortable with the tone, feel free to stay away or withdraw from the issue. (note: if the user is outright hostile or is violating the CoC, [refer to the Code of Conduct](CODE_OF_CONDUCT.md) to resolve the conflict).
## Review Pull Requests
[Needs Collaborator](#join-the-project-team): Issue Tracker
While anyone can comment on a PR, add feedback, etc, PRs are only _approved_ by team members with Issue Tracker or higher permissions.
PR reviews use [GitHub's own review feature](https://help.github.com/articles/about-pull-request-reviews/), which manages comments, approval, and review iteration.
Some notes:
- You may ask for minor changes ("nitpicks"), but consider whether they are really blockers to merging: try to err on the side of "approve, with comments".
- _ALL PULL REQUESTS_ should be covered by a test: either by a previously-failing test, an existing test that covers the entire functionality of the submitted code, or new tests to verify any new/changed behavior. All tests must also pass and follow established conventions. Test coverage should not drop, unless the specific case is considered reasonable by maintainers.
- Please make sure you're familiar with the code or documentation being updated, unless it's a minor change (spellchecking, minor formatting, etc). You may @mention another project member who you think is better suited for the review, but still provide a non-approving review of your own.
- Be extra kind: people who submit code/doc contributions are putting themselves in a pretty vulnerable position, and have put time and care into what they've done (even if that's not obvious to you!) -- always respond with respect, be understanding, but don't feel like you need to sacrifice your standards for their sake, either. Just don't be a jerk about it?
## Join the Project Team
### Ways to Join
There are many ways to contribute! Most of them don't require any official status unless otherwise noted. That said, there's a couple of positions that grant special repository abilities, and this section describes how they're granted and what they do.
All of the below positions are granted based on the project team's needs, as well as their consensus opinion about whether they would like to work with the person and think that they would fit well into that position. The process is relatively informal, and it's likely that people who express interest in participating can just be granted the permissions they'd like.
You can spot a collaborator on the repo by looking for the `[Collaborator]` or `[Owner]` tags next to their names.
| Permission | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Issue Tracker | Granted to contributors who express a strong interest in spending time on the project's issue tracker. These tasks are mainly [labeling issues](#label-issues), [cleaning up old ones](#clean-up-issues-and-prs), and [reviewing pull requests](#review-pull-requests), as well as all the usual things non-team-member contributors can do. Issue handlers should not merge pull requests, tag releases, or directly commit code themselves: that should still be done through the usual pull request process. Becoming an Issue Handler means the project team trusts you to understand enough of the team's process and context to implement it on the issue tracker. |
| Committer | Granted to contributors who want to handle the actual pull request merges, tagging new versions, etc. Committers should have a good level of familiarity with the codebase, and enough context to understand the implications of various changes, as well as a good sense of the will and expectations of the project team. |
| Admin/Owner | Granted to people ultimately responsible for the project, its community, etc. |

View file

@ -1,7 +1,46 @@
Copyright 2018-2018 by Qiming Zhao <chemzqm@gmail.com>aaa
Copyright (c) <2022> <chemzqm@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
"Anti 996" License Version 1.0 (Draft)
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Permission is hereby granted to any individual or legal entity
obtaining a copy of this licensed work (including the source code,
documentation and/or related items, hereinafter collectively referred
to as the "licensed work"), free of charge, to deal with the licensed
work for any purpose, including without limitation, the rights to use,
reproduce, modify, prepare derivative works of, distribute, publish
and sublicense the licensed work, subject to the following conditions:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1. The individual or the legal entity must conspicuously display,
without modification, this License and the notice on each redistributed
or derivative copy of the Licensed Work.
2. The individual or the legal entity must strictly comply with all
applicable laws, regulations, rules and standards of the jurisdiction
relating to labor and employment where the individual is physically
located or where the individual was born or naturalized; or where the
legal entity is registered or is operating (whichever is stricter). In
case that the jurisdiction has no such laws, regulations, rules and
standards or its laws, regulations, rules and standards are
unenforceable, the individual or the legal entity are required to
comply with Core International Labor Standards.
3. The individual or the legal entity shall not induce, suggest or force
its employee(s), whether full-time or part-time, or its independent
contractor(s), in any methods, to agree in oral or written form, to
directly or indirectly restrict, weaken or relinquish his or her
rights or remedies under such laws, regulations, rules and standards
relating to labor and employment as mentioned above, no matter whether
such written or oral agreements are enforceable under the laws of the
said jurisdiction, nor shall such individual or the legal entity
limit, in any methods, the rights of its employee(s) or independent
contractor(s) from reporting or complaining to the copyright holder or
relevant authorities monitoring the compliance of the license about
its violation(s) of the said license.
THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION WITH THE
LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK.

View file

@ -1,148 +0,0 @@
scriptencoding utf-8
" Helper methods for viml
function! coc#helper#get_charactor(line, col) abort
return strchars(strpart(a:line, 0, a:col - 1))
endfunction
function! coc#helper#last_character(line) abort
return strcharpart(a:line, strchars(a:line) - 1, 1)
endfunction
function! coc#helper#obj_equal(one, two) abort
for key in keys(a:one)
if a:one[key] != a:two[key]
return 0
endif
endfor
return 1
endfunction
" get change between two lines
function! coc#helper#str_diff(curr, previous, col) abort
let end = strpart(a:curr, a:col - 1)
let start = strpart(a:curr, 0, a:col -1)
let endOffset = 0
let startOffset = 0
let currLen = strchars(a:curr)
let prevLen = strchars(a:previous)
if len(end)
let endLen = strchars(end)
for i in range(min([prevLen, endLen]))
if strcharpart(end, endLen - 1 - i, 1) ==# strcharpart(a:previous, prevLen -1 -i, 1)
let endOffset = endOffset + 1
else
break
endif
endfor
endif
let remain = endOffset == 0 ? a:previous : strcharpart(a:previous, 0, prevLen - endOffset)
if len(remain)
for i in range(min([strchars(remain), strchars(start)]))
if strcharpart(remain, i, 1) ==# strcharpart(start, i ,1)
let startOffset = startOffset + 1
else
break
endif
endfor
endif
return {
\ 'start': startOffset,
\ 'end': prevLen - endOffset,
\ 'text': strcharpart(a:curr, startOffset, currLen - startOffset - endOffset)
\ }
endfunction
function! coc#helper#str_apply(content, diff) abort
let totalLen = strchars(a:content)
let endLen = totalLen - a:diff['end']
return strcharpart(a:content, 0, a:diff['start']).a:diff['text'].strcharpart(a:content, a:diff['end'], endLen)
endfunction
" insert inserted to line at position, use ... when result is too long
" line should only contains character has strwidth equals 1
function! coc#helper#str_compose(line, position, inserted) abort
let width = strwidth(a:line)
let text = a:inserted
let res = a:line
let need_truncate = a:position + strwidth(text) + 1 > width
if need_truncate
let remain = width - a:position - 3
if remain < 2
" use text for full line, use first & end of a:line, ignore position
let res = strcharpart(a:line, 0, 1)
let w = strwidth(res)
for i in range(strchars(text))
let c = strcharpart(text, i, 1)
let a = strwidth(c)
if w + a <= width - 1
let w = w + a
let res = res.c
endif
endfor
let res = res.strcharpart(a:line, w)
else
let res = strcharpart(a:line, 0, a:position)
let w = strwidth(res)
for i in range(strchars(text))
let c = strcharpart(text, i, 1)
let a = strwidth(c)
if w + a <= width - 3
let w = w + a
let res = res.c
endif
endfor
let res = res.'..'
let w = w + 2
let res = res.strcharpart(a:line, w)
endif
else
let first = strcharpart(a:line, 0, a:position)
let res = first.text.strcharpart(a:line, a:position + strwidth(text))
endif
return res
endfunction
" Return new dict with keys removed
function! coc#helper#dict_omit(dict, keys) abort
let res = {}
for key in keys(a:dict)
if index(a:keys, key) == -1
let res[key] = a:dict[key]
endif
endfor
return res
endfunction
" Return new dict with keys only
function! coc#helper#dict_pick(dict, keys) abort
let res = {}
for key in keys(a:dict)
if index(a:keys, key) != -1
let res[key] = a:dict[key]
endif
endfor
return res
endfunction
" support for float values
function! coc#helper#min(first, ...) abort
let val = a:first
for i in range(0, len(a:000) - 1)
if a:000[i] < val
let val = a:000[i]
endif
endfor
return val
endfunction
" support for float values
function! coc#helper#max(first, ...) abort
let val = a:first
for i in range(0, len(a:000) - 1)
if a:000[i] > val
let val = a:000[i]
endif
endfor
return val
endfunction

View file

@ -292,8 +292,9 @@ function! coc#notify#close(winid) abort
endif
call coc#window#set_var(a:winid, 'closing', 1)
call s:cancel(a:winid)
let curr = s:is_vim ? {'row': row} : {'winblend': coc#window#get_var(a:winid, 'winblend', 30)}
let dest = s:is_vim ? {'row': row + 1} : {'winblend': 60}
let winblend = coc#window#get_var(a:winid, 'winblend', 0)
let curr = s:is_vim ? {'row': row} : {'winblend': winblend}
let dest = s:is_vim ? {'row': row + 1} : {'winblend': winblend == 0 ? 0 : 60}
call s:animate(a:winid, curr, dest, 0, 1)
endfunction

View file

@ -182,14 +182,10 @@ function! coc#util#jump(cmd, filepath, ...) abort
else
exec 'drop '.fnameescape(file)
endif
elseif a:cmd == 'edit'
if bufloaded(file)
elseif a:cmd == 'edit' && bufloaded(file)
exe 'b '.bufnr(file)
else
exe a:cmd.' '.fnameescape(file)
endif
else
exe a:cmd.' '.fnameescape(file)
call s:safer_open(a:cmd, file)
endif
if !empty(get(a:, 1, []))
let line = getline(a:1[0] + 1)
@ -208,6 +204,43 @@ function! coc#util#jump(cmd, filepath, ...) abort
endif
endfunction
function! s:safer_open(cmd, file) abort
" How to support :pedit and :drop?
let is_supported_cmd = index(["edit", "split", "vsplit", "tabe"], a:cmd) >= 0
" Special handling should be limited to URI.
let looks_like_uri = match(a:file, "^.*://") >= 0
if looks_like_uri && is_supported_cmd && has('win32') && has('nvim')
if bufloaded(a:file)
let buf = bufnr(a:file)
" Do not reload existing buffer
let reload_buffer = v:false
else
" Workaround a bug for Win32 paths (works in Neovim only).
"
" reference:
" - https://github.com/vim/vim/issues/541
" - https://github.com/neoclide/coc-java/issues/82
let buf = nvim_create_buf(v:true, v:false)
" Ignore swap file error occuring when, for example, file is a URI.
silent! call nvim_buf_set_name(buf, a:file)
let reload_buffer = v:true
endif
if a:cmd != 'edit'
" Open split, tab, etc. by a:cmd.
exe a:cmd
endif
" Set filename and reload file (or URI) contents by :edit.
exe 'keepjumps buffer ' . buf
if reload_buffer
exe 'keepjumps edit'
endif
else
exe a:cmd.' '.fnameescape(a:file)
endif
endfunction
function! coc#util#variables(bufnr) abort
let info = getbufinfo(a:bufnr)
let variables = empty(info) ? {} : copy(info[0]['variables'])

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,77 @@
const cp = require('child_process')
const fs = require('fs')
const path = require('path')
let revision = 'master'
if (process.env.NODE_ENV !== 'development') {
try {
let res = cp.execSync(`git log -1 --date=iso --pretty=format:'"%h","%ad"'`, {encoding: 'utf8'})
revision = res.replaceAll('"', '').replace(',', ' ')
} catch (e) {
// ignore
}
}
let envPlugin = {
name: 'env',
setup(build) {
build.onResolve({filter: /\/appenders$/}, args => {
let fullpath = path.join(args.resolveDir, args.path)
return {
path: path.relative(__dirname, fullpath).replace(/\\/g, '/'),
namespace: 'env-ns'
}
})
build.onLoad({filter: /^node_modules\/log4js\/lib\/appenders$/, namespace: 'env-ns'}, args => {
let content = fs.readFileSync(path.join(args.path, 'index.js'), 'utf8')
return {
contents: content.replace(/require\.main/g, '""'),
resolveDir: args.path
}
})
}
}
async function start(watch) {
await require('esbuild').build({
entryPoints: ['src/main.ts'],
bundle: true,
watch,
minify: process.env.NODE_ENV === 'production',
sourcemap: process.env.NODE_ENV === 'development',
define: {REVISION: '"' + revision + '"', ESBUILD: 'true'},
mainFields: ['module', 'main'],
platform: 'node',
target: 'node12.12',
outfile: 'build/index.js',
banner: {
js: `(function () {
var v = process.version
var parts = v.slice(1).split('.')
var major = parseInt(parts[0], 10)
var minor = parseInt(parts[1], 10)
if (major < 12 || (major == 12 && minor < 12)) {
throw new Error('coc.nvim requires node >= v12.12.0, current version: ' + v)
}
})(); `
},
plugins: [envPlugin]
})
}
let watch = false
if (process.argv.includes('--watch')) {
console.log('watching...')
watch = {
onRebuild(error) {
if (error) {
console.error('watch build failed:', error)
} else {
console.log('watch build succeeded')
}
},
}
}
start(watch).catch(e => {
console.error(e)
})

View file

@ -1,3 +1,7 @@
# 2022-06-14
- Add highlight groups `CocListLine` and `CocListSearch`.
# 2022-06-11
- Add configuration "notification.disabledProgressSources"

View file

@ -0,0 +1,21 @@
const path = require('path')
const os = require('os')
const fs = require('fs')
process.on('uncaughtException', err => {
let msg = 'Uncaught exception: ' + err.stack
console.error(msg)
})
process.on('exit', () => {
fs.rmdirSync(process.env.TMPDIR, { recursive: true, force: true })
})
module.exports = async () => {
let dataHome = path.join(os.tmpdir(), `coc-test/${process.pid}`)
fs.mkdirSync(dataHome, { recursive: true })
process.env.NODE_ENV = 'test'
process.env.COC_DATA_HOME = dataHome
process.env.COC_VIMCONFIG = path.join(__dirname, 'src/__tests__')
process.env.TMPDIR = '/tmp/coc-test'
}

View file

@ -1,17 +1,124 @@
{
"name": "coc.nvim-release",
"name": "coc.nvim-master",
"version": "0.0.81",
"description": "LSP based intellisense engine for neovim & vim8.",
"main": "./build/index.js",
"engines": {
"node": ">=12.12.0"
},
"scripts": {
"lint": "eslint . --ext .ts --quiet",
"lint:typecheck": "tsc -p tsconfig.json",
"build": "node esbuild.js",
"test": "./node_modules/.bin/jest --forceExit",
"test-build": "./node_modules/.bin/jest --coverage --forceExit",
"prepare": "node esbuild.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/neoclide/coc.nvim.git"
},
"keywords": [
"complete",
"neovim"
],
"author": "Qiming Zhao <chemzqm@gmail.com>",
"bugs": {
"url": "https://github.com/neoclide/coc.nvim/issues"
},
"homepage": "https://github.com/neoclide/coc.nvim#readme"
"homepage": "https://github.com/neoclide/coc.nvim#readme",
"jest": {
"globals": {
"__TEST__": true
},
"projects": [
"<rootDir>"
],
"watchman": false,
"clearMocks": true,
"globalSetup": "./jest.js",
"testEnvironment": "node",
"coveragePathIgnorePatterns": [
"<rootDir>/src/__tests__/*"
],
"moduleFileExtensions": [
"ts",
"tsx",
"json",
"js"
],
"transform": {
"^.+\\.tsx?$": [
"@swc/jest"
]
},
"testRegex": "src/__tests__/.*\\.(test|spec)\\.ts$",
"coverageReporters": [
"text",
"lcov"
],
"coverageDirectory": "./coverage/"
},
"devDependencies": {
"@swc/core": "^1.2.183",
"@swc/jest": "^0.2.21",
"@types/cli-table": "^0.3.0",
"@types/debounce": "^3.0.0",
"@types/fb-watchman": "^2.0.0",
"@types/fs-extra": "^9.0.6",
"@types/glob": "^7.2.0",
"@types/jest": "^27.0.3",
"@types/marked": "^4.0.1",
"@types/minimatch": "^3.0.3",
"@types/mkdirp": "^1.0.1",
"@types/node": "12.12.12",
"@types/semver": "^7.3.4",
"@types/tar": "^4.0.5",
"@types/uuid": "^8.3.0",
"@types/which": "^1.3.2",
"@typescript-eslint/eslint-plugin": "^5.20.0",
"@typescript-eslint/parser": "^5.20.0",
"bser": "^2.1.1",
"esbuild": "^0.14.25",
"eslint": "^8.14.0",
"eslint-plugin-jest": "^26.1.5",
"eslint-plugin-jsdoc": "^39.2.8",
"jest": "27.4.5",
"typescript": "^4.6.3",
"vscode-languageserver": "7.0.0"
},
"dependencies": {
"@chemzqm/neovim": "^5.7.9",
"@chemzqm/string-width": "^5.1.2",
"ansi-styles": "^5.0.0",
"bytes": "^3.1.0",
"cli-table": "^0.3.4",
"content-disposition": "^0.5.3",
"debounce": "^1.2.0",
"decompress-response": "^6.0.0",
"fast-diff": "^1.2.0",
"fb-watchman": "^2.0.1",
"follow-redirects": "^1.14.8",
"fs-extra": "^9.0.1",
"glob": "^7.2.0",
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.0",
"isuri": "^2.0.3",
"jsonc-parser": "^3.0.0",
"log4js": "^6.4.0",
"marked": "^4.0.12",
"minimatch": "^3.0.4",
"semver": "^7.3.2",
"strip-ansi": "^6.0.0",
"tar": "^6.1.9",
"tslib": "^2.0.3",
"unidecode": "^0.1.8",
"unzip-stream": "^0.3.1",
"uuid": "^7.0.3",
"vscode-languageserver-protocol": "^3.16.0",
"vscode-languageserver-textdocument": "^1.0.3",
"vscode-languageserver-types": "^3.16.0",
"vscode-uri": "^2.1.2",
"which": "^2.0.2"
}
}

View file

@ -0,0 +1,17 @@
" vim source for emails
function! coc#source#email#init() abort
return {
\ 'priority': 9,
\ 'shortcut': 'Email',
\ 'triggerCharacters': ['@']
\}
endfunction
function! coc#source#email#should_complete(opt) abort
return 1
endfunction
function! coc#source#email#complete(opt, cb) abort
let items = ['foo@gmail.com', 'bar@yahoo.com']
call a:cb(items)
endfunction

View file

@ -0,0 +1,62 @@
/* eslint-disable */
import helper from '../helper'
// import * as assert from 'assert'
import fs from 'fs'
import * as lsclient from '../../language-client'
import * as path from 'path'
import { URI } from 'vscode-uri'
// import which from 'which'
beforeAll(async () => {
await helper.setup()
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
await helper.reset()
})
describe('Client integration', () => {
it('should send file change notification', (done) => {
if (global.hasOwnProperty('__TEST__')) return done()
let serverModule = path.join(__dirname, './server/testFileWatcher.js')
let serverOptions: lsclient.ServerOptions = {
module: serverModule,
transport: lsclient.TransportKind.ipc
}
let clientOptions: lsclient.LanguageClientOptions = {
documentSelector: ['css'],
synchronize: {}, initializationOptions: {},
middleware: {
}
}
let client = new lsclient.LanguageClient('css', 'Test Language Server', serverOptions, clientOptions)
let disposable = client.start()
client.onReady().then(_ => {
setTimeout(async () => {
let file = path.join(__dirname, 'test.js')
fs.writeFileSync(file, '', 'utf8')
await helper.wait(300)
let res = await client.sendRequest('custom/received')
expect(res).toEqual({
changes: [{
uri: URI.file(file).toString(),
type: 1
}]
})
fs.unlinkSync(file)
disposable.dispose()
done()
}, 200)
}, e => {
disposable.dispose()
done(e)
})
})
})

View file

@ -0,0 +1,139 @@
import { Duplex } from 'stream'
import { createProtocolConnection, ProgressType, DocumentSymbolParams, DocumentSymbolRequest, InitializeParams, InitializeRequest, InitializeResult, ProtocolConnection, StreamMessageReader, StreamMessageWriter } from 'vscode-languageserver-protocol/node'
import { SymbolInformation, SymbolKind } from 'vscode-languageserver-types'
import { NullLogger } from '../../language-client/client'
class TestStream extends Duplex {
public _write(chunk: string, _encoding: string, done: () => void): void {
this.emit('data', chunk)
done()
}
public _read(_size: number): void {
}
}
let serverConnection: ProtocolConnection
let clientConnection: ProtocolConnection
let progressType: ProgressType<any> = new ProgressType()
beforeEach(() => {
const up = new TestStream()
const down = new TestStream()
const logger = new NullLogger()
serverConnection = createProtocolConnection(new StreamMessageReader(up), new StreamMessageWriter(down), logger)
clientConnection = createProtocolConnection(new StreamMessageReader(down), new StreamMessageWriter(up), logger)
serverConnection.listen()
clientConnection.listen()
})
afterEach(() => {
serverConnection.dispose()
clientConnection.dispose()
})
describe('Connection Tests', () => {
it('should ensure proper param passing', async () => {
let paramsCorrect = false
serverConnection.onRequest(InitializeRequest.type, params => {
paramsCorrect = !Array.isArray(params)
let result: InitializeResult = {
capabilities: {
}
}
return result
})
const init: InitializeParams = {
rootUri: 'file:///home/dirkb',
processId: 1,
capabilities: {},
workspaceFolders: null,
}
await clientConnection.sendRequest(InitializeRequest.type, init)
expect(paramsCorrect).toBe(true)
})
it('should provide token', async () => {
serverConnection.onRequest(DocumentSymbolRequest.type, params => {
expect(params.partialResultToken).toBe('3b1db4c9-e011-489e-a9d1-0653e64707c2')
return []
})
const params: DocumentSymbolParams = {
textDocument: { uri: 'file:///abc.txt' },
partialResultToken: '3b1db4c9-e011-489e-a9d1-0653e64707c2'
}
await clientConnection.sendRequest(DocumentSymbolRequest.type, params)
})
it('should report result', async () => {
let result: SymbolInformation = {
name: 'abc',
kind: SymbolKind.Class,
location: {
uri: 'file:///abc.txt',
range: { start: { line: 0, character: 1 }, end: { line: 2, character: 3 } }
}
}
serverConnection.onRequest(DocumentSymbolRequest.type, params => {
expect(params.partialResultToken).toBe('3b1db4c9-e011-489e-a9d1-0653e64707c2')
serverConnection.sendProgress(progressType, params.partialResultToken, [result])
return []
})
const params: DocumentSymbolParams = {
textDocument: { uri: 'file:///abc.txt' },
partialResultToken: '3b1db4c9-e011-489e-a9d1-0653e64707c2'
}
let progressOK = false
clientConnection.onProgress(progressType, '3b1db4c9-e011-489e-a9d1-0653e64707c2', values => {
progressOK = (values !== undefined && values.length === 1)
})
await clientConnection.sendRequest(DocumentSymbolRequest.type, params)
expect(progressOK).toBeTruthy()
})
it('should provide workDoneToken', async () => {
serverConnection.onRequest(DocumentSymbolRequest.type, params => {
expect(params.workDoneToken).toBe('3b1db4c9-e011-489e-a9d1-0653e64707c2')
return []
})
const params: DocumentSymbolParams = {
textDocument: { uri: 'file:///abc.txt' },
workDoneToken: '3b1db4c9-e011-489e-a9d1-0653e64707c2'
}
await clientConnection.sendRequest(DocumentSymbolRequest.type, params)
})
it('should report work done progress', async () => {
serverConnection.onRequest(DocumentSymbolRequest.type, params => {
expect(params.workDoneToken).toBe('3b1db4c9-e011-489e-a9d1-0653e64707c2')
serverConnection.sendProgress(progressType, params.workDoneToken, {
kind: 'begin',
title: 'progress'
})
serverConnection.sendProgress(progressType, params.workDoneToken, {
kind: 'report',
message: 'message'
})
serverConnection.sendProgress(progressType, params.workDoneToken, {
kind: 'end',
message: 'message'
})
return []
})
const params: DocumentSymbolParams = {
textDocument: { uri: 'file:///abc.txt' },
workDoneToken: '3b1db4c9-e011-489e-a9d1-0653e64707c2'
}
let result = ''
clientConnection.onProgress(progressType, '3b1db4c9-e011-489e-a9d1-0653e64707c2', value => {
result += value.kind
})
await clientConnection.sendRequest(DocumentSymbolRequest.type, params)
expect(result).toBe('beginreportend')
})
})

View file

@ -0,0 +1,86 @@
import { CompletionTriggerKind, Position, TextDocumentItem, TextDocumentSaveReason } from 'vscode-languageserver-protocol'
import { TextDocument } from 'vscode-languageserver-textdocument'
import { URI } from 'vscode-uri'
import * as cv from '../../language-client/utils/converter'
describe('converter', () => {
function createDocument(): TextDocument {
return TextDocument.create('file:///1', 'css', 1, '')
}
it('should convertToTextDocumentItem', () => {
let doc = createDocument()
expect(cv.convertToTextDocumentItem(doc).uri).toBe(doc.uri)
expect(TextDocumentItem.is(cv.convertToTextDocumentItem(doc))).toBe(true)
})
it('should asCloseTextDocumentParams', () => {
let doc = createDocument()
expect(cv.asCloseTextDocumentParams(doc).textDocument.uri).toBe(doc.uri)
})
it('should asChangeTextDocumentParams', () => {
let doc = createDocument()
expect(cv.asChangeTextDocumentParams(doc).textDocument.uri).toBe(doc.uri)
})
it('should asWillSaveTextDocumentParams', () => {
let res = cv.asWillSaveTextDocumentParams({ document: createDocument(), reason: TextDocumentSaveReason.Manual, waitUntil: () => {} })
expect(res.textDocument).toBeDefined()
expect(res.reason).toBeDefined()
})
it('should asVersionedTextDocumentIdentifier', () => {
let res = cv.asVersionedTextDocumentIdentifier(createDocument())
expect(res.uri).toBeDefined()
expect(res.version).toBeDefined()
})
it('should asSaveTextDocumentParams', () => {
let res = cv.asSaveTextDocumentParams(createDocument(), true)
expect(res.textDocument.uri).toBeDefined()
expect(res.text).toBeDefined()
res = cv.asSaveTextDocumentParams(createDocument(), false)
expect(res.text).toBeUndefined()
})
it('should asUri', () => {
let uri = URI.file('/tmp/a')
expect(cv.asUri(uri)).toBe(uri.toString())
})
it('should asCompletionParams', () => {
let params = cv.asCompletionParams(createDocument(), Position.create(0, 0), { triggerKind: CompletionTriggerKind.Invoked })
expect(params.textDocument).toBeDefined()
expect(params.position).toBeDefined()
expect(params.context).toBeDefined()
})
it('should asTextDocumentPositionParams', () => {
let params = cv.asTextDocumentPositionParams(createDocument(), Position.create(0, 0))
expect(params.textDocument).toBeDefined()
expect(params.position).toBeDefined()
})
it('should asTextDocumentIdentifier', () => {
let doc = cv.asTextDocumentIdentifier(createDocument())
expect(doc.uri).toBeDefined()
})
it('should asReferenceParams', () => {
let params = cv.asReferenceParams(createDocument(), Position.create(0, 0), { includeDeclaration: false })
expect(params.textDocument.uri).toBeDefined()
expect(params.position).toBeDefined()
})
it('should asDocumentSymbolParams', () => {
let doc = cv.asDocumentSymbolParams(createDocument())
expect(doc.textDocument.uri).toBeDefined()
})
it('should asCodeLensParams', () => {
let doc = cv.asCodeLensParams(createDocument())
expect(doc.textDocument.uri).toBeDefined()
})
})

View file

@ -0,0 +1,72 @@
/* eslint-disable */
import assert from 'assert'
import { Delayer } from '../../language-client/utils/async'
import { wait } from '../../util/index'
test('Delayer', () => {
let count = 0
let factory = () => {
return Promise.resolve(++count)
}
let delayer = new Delayer(0)
let promises: Thenable<any>[] = []
assert(!delayer.isTriggered())
promises.push(delayer.trigger(factory).then((result) => { assert.equal(result, 1); assert(!delayer.isTriggered()) }))
assert(delayer.isTriggered())
promises.push(delayer.trigger(factory).then((result) => { assert.equal(result, 1); assert(!delayer.isTriggered()) }))
assert(delayer.isTriggered())
promises.push(delayer.trigger(factory).then((result) => { assert.equal(result, 1); assert(!delayer.isTriggered()) }))
assert(delayer.isTriggered())
return Promise.all(promises).then(() => {
assert(!delayer.isTriggered())
}).finally(() => {
delayer.dispose()
})
})
test('Delayer - forceDelivery', async () => {
let count = 0
let factory = () => {
return Promise.resolve(++count)
}
let delayer = new Delayer(150)
delayer.forceDelivery()
delayer.trigger(factory).then((result) => { assert.equal(result, 1); assert(!delayer.isTriggered()) })
await wait(10)
delayer.forceDelivery()
expect(count).toBe(1)
void delayer.trigger(factory)
await wait(10)
delayer.cancel()
expect(count).toBe(1)
})
test('Delayer - last task should be the one getting called', function() {
let factoryFactory = (n: number) => () => {
return Promise.resolve(n)
}
let delayer = new Delayer(0)
let promises: Thenable<any>[] = []
assert(!delayer.isTriggered())
promises.push(delayer.trigger(factoryFactory(1)).then((n) => { assert.equal(n, 3) }))
promises.push(delayer.trigger(factoryFactory(2)).then((n) => { assert.equal(n, 3) }))
promises.push(delayer.trigger(factoryFactory(3)).then((n) => { assert.equal(n, 3) }))
const p = Promise.all(promises).then(() => {
assert(!delayer.isTriggered())
})
assert(delayer.isTriggered())
return p
})

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,123 @@
/* eslint-disable */
import helper from '../helper'
import * as assert from 'assert'
import * as lsclient from '../../language-client'
import path from 'path'
beforeAll(async () => {
await helper.setup()
})
afterAll(async () => {
await helper.shutdown()
})
async function testLanguageServer(serverOptions: lsclient.ServerOptions): Promise<lsclient.LanguageClient> {
let clientOptions: lsclient.LanguageClientOptions = {
documentSelector: ['css'],
synchronize: {},
initializationOptions: {}
}
let client = new lsclient.LanguageClient('css', 'Test Language Server', serverOptions, clientOptions)
client.start()
await client.onReady()
expect(client.initializeResult).toBeDefined()
return client
}
describe('Client integration', () => {
it('should initialize use IPC channel', (done) => {
let serverModule = path.join(__dirname, './server/testInitializeResult.js')
let serverOptions: lsclient.ServerOptions = {
run: { module: serverModule, transport: lsclient.TransportKind.ipc },
debug: { module: serverModule, transport: lsclient.TransportKind.ipc, options: { execArgv: ['--nolazy', '--inspect=6014'] } }
}
let clientOptions: lsclient.LanguageClientOptions = {
documentSelector: ['css'],
synchronize: {}, initializationOptions: {},
middleware: {
handleDiagnostics: (uri, diagnostics, next) => {
assert.equal(uri, "uri:/test.ts")
assert.ok(Array.isArray(diagnostics))
assert.equal(diagnostics.length, 0)
next(uri, diagnostics)
}
}
}
let client = new lsclient.LanguageClient('css', 'Test Language Server', serverOptions, clientOptions)
client.start()
assert.equal(client.initializeResult, undefined)
client.onReady().then(_ => {
try {
let expected = {
capabilities: {
textDocumentSync: 1,
completionProvider: { resolveProvider: true, triggerCharacters: ['"', ':'] },
hoverProvider: true,
renameProvider: {
prepareProvider: true
}
},
customResults: {
"hello": "world"
}
}
assert.deepEqual(client.initializeResult, expected)
setTimeout(async () => {
await client.stop()
done()
}, 50)
} catch (e) {
done(e)
}
}, e => {
done(e)
})
})
it('should initialize use stdio', async () => {
let serverModule = path.join(__dirname, './server/testInitializeResult.js')
let serverOptions: lsclient.ServerOptions = {
module: serverModule,
transport: lsclient.TransportKind.stdio
}
let client = await testLanguageServer(serverOptions)
await client.stop()
})
it('should initialize use pipe', async () => {
let serverModule = path.join(__dirname, './server/testInitializeResult.js')
let serverOptions: lsclient.ServerOptions = {
module: serverModule,
transport: lsclient.TransportKind.pipe
}
let client = await testLanguageServer(serverOptions)
await client.stop()
})
it('should initialize use socket', async () => {
let serverModule = path.join(__dirname, './server/testInitializeResult.js')
let serverOptions: lsclient.ServerOptions = {
module: serverModule,
transport: {
kind: lsclient.TransportKind.socket,
port: 8088
}
}
let client = await testLanguageServer(serverOptions)
await client.stop()
})
it('should initialize as command', async () => {
let serverModule = path.join(__dirname, './server/testInitializeResult.js')
let serverOptions: lsclient.ServerOptions = {
command: 'node',
args: [serverModule, '--stdio']
}
let client = await testLanguageServer(serverOptions)
await client.stop()
})
})

View file

@ -0,0 +1,35 @@
const languageserver = require('vscode-languageserver')
let connection = languageserver.createConnection()
let documents = new languageserver.TextDocuments()
documents.listen(connection)
connection.onInitialize(() => {
let capabilities = {
textDocumentSync: documents.syncKind
}
return { capabilities }
})
connection.onInitialized(() => {
connection.sendRequest('client/registerCapability', {
registrations: [{
id: 'didChangeWatchedFiles',
method: 'workspace/didChangeWatchedFiles',
registerOptions: {
watchers: [{ globPattern: "**" }]
}
}]
})
})
let received
connection.onNotification('workspace/didChangeWatchedFiles', params => {
received = params
})
connection.onRequest('custom/received', async () => {
return received
})
connection.listen()

View file

@ -0,0 +1,36 @@
'use strict'
Object.defineProperty(exports, "__esModule", {value: true})
const tslib_1 = require("tslib")
const assert = tslib_1.__importStar(require("assert"))
const vscode_languageserver_1 = require("vscode-languageserver")
let connection = vscode_languageserver_1.createConnection()
let documents = new vscode_languageserver_1.TextDocuments()
documents.listen(connection)
connection.onInitialize((params) => {
assert.equal(params.capabilities.workspace.applyEdit, true)
assert.equal(params.capabilities.workspace.workspaceEdit.documentChanges, true)
assert.deepEqual(params.capabilities.workspace.workspaceEdit.resourceOperations, [vscode_languageserver_1.ResourceOperationKind.Create, vscode_languageserver_1.ResourceOperationKind.Rename, vscode_languageserver_1.ResourceOperationKind.Delete])
assert.equal(params.capabilities.workspace.workspaceEdit.failureHandling, vscode_languageserver_1.FailureHandlingKind.Undo)
assert.equal(params.capabilities.textDocument.completion.completionItem.deprecatedSupport, true)
assert.equal(params.capabilities.textDocument.completion.completionItem.preselectSupport, true)
assert.equal(params.capabilities.textDocument.signatureHelp.signatureInformation.parameterInformation.labelOffsetSupport, true)
assert.equal(params.capabilities.textDocument.rename.prepareSupport, true)
let valueSet = params.capabilities.textDocument.completion.completionItemKind.valueSet
assert.equal(valueSet[0], 1)
assert.equal(valueSet[valueSet.length - 1], vscode_languageserver_1.CompletionItemKind.TypeParameter)
let capabilities = {
textDocumentSync: documents.syncKind,
completionProvider: {resolveProvider: true, triggerCharacters: ['"', ':']},
hoverProvider: true,
renameProvider: {
prepareProvider: true
}
}
return {capabilities, customResults: {"hello": "world"}}
})
connection.onInitialized(() => {
connection.sendDiagnostics({uri: "uri:/test.ts", diagnostics: []})
})
// Listen on the connection
connection.listen()

View file

@ -0,0 +1,409 @@
const assert = require('assert')
const {URI} = require('vscode-uri')
const {
createConnection, CompletionItemKind, ResourceOperationKind, FailureHandlingKind,
DiagnosticTag, CompletionItemTag, TextDocumentSyncKind, MarkupKind, SignatureInformation, ParameterInformation,
Location, Range, DocumentHighlight, DocumentHighlightKind, CodeAction, Command, TextEdit, Position, DocumentLink,
ColorInformation, Color, ColorPresentation, FoldingRange, SelectionRange, SymbolKind, ProtocolRequestType, WorkDoneProgress,
WorkDoneProgressCreateRequest} = require('vscode-languageserver')
const {
DidCreateFilesNotification,
DidRenameFilesNotification,
DidDeleteFilesNotification,
WillCreateFilesRequest, WillRenameFilesRequest, WillDeleteFilesRequest
} = require('vscode-languageserver-protocol')
let connection = createConnection()
console.log = connection.console.log.bind(connection.console)
console.error = connection.console.error.bind(connection.console)
connection.onInitialize(params => {
assert.equal((params.capabilities.workspace).applyEdit, true)
assert.equal(params.capabilities.workspace.workspaceEdit.documentChanges, true)
assert.equal(params.capabilities.workspace.workspaceEdit.failureHandling, FailureHandlingKind.Undo)
assert.equal(params.capabilities.textDocument.completion.completionItem.deprecatedSupport, true)
assert.equal(params.capabilities.textDocument.completion.completionItem.preselectSupport, true)
assert.equal(params.capabilities.textDocument.completion.completionItem.tagSupport.valueSet.length, 1)
assert.equal(params.capabilities.textDocument.completion.completionItem.tagSupport.valueSet[0], CompletionItemTag.Deprecated)
assert.equal(params.capabilities.textDocument.signatureHelp.signatureInformation.parameterInformation.labelOffsetSupport, true)
// assert.equal(params.capabilities.textDocument.definition.linkSupport, true)
// assert.equal(params.capabilities.textDocument.declaration.linkSupport, true)
// assert.equal(params.capabilities.textDocument.implementation.linkSupport, true)
// assert.equal(params.capabilities.textDocument.typeDefinition.linkSupport, true)
assert.equal(params.capabilities.textDocument.rename.prepareSupport, true)
assert.equal(params.capabilities.textDocument.publishDiagnostics.relatedInformation, true)
assert.equal(params.capabilities.textDocument.publishDiagnostics.tagSupport.valueSet.length, 2)
assert.equal(params.capabilities.textDocument.publishDiagnostics.tagSupport.valueSet[0], DiagnosticTag.Unnecessary)
assert.equal(params.capabilities.textDocument.publishDiagnostics.tagSupport.valueSet[1], DiagnosticTag.Deprecated)
assert.equal(params.capabilities.textDocument.documentLink.tooltipSupport, true)
let valueSet = params.capabilities.textDocument.completion.completionItemKind.valueSet
assert.equal(valueSet[0], 1)
assert.equal(valueSet[valueSet.length - 1], CompletionItemKind.TypeParameter)
assert.deepEqual(params.capabilities.workspace.workspaceEdit.resourceOperations, [ResourceOperationKind.Create, ResourceOperationKind.Rename, ResourceOperationKind.Delete])
assert.equal(params.capabilities.workspace.fileOperations.willCreate, true)
let capabilities = {
textDocumentSync: TextDocumentSyncKind.Full,
definitionProvider: true,
hoverProvider: true,
completionProvider: {resolveProvider: true, triggerCharacters: ['"', ':']},
signatureHelpProvider: {
triggerCharacters: [':'],
retriggerCharacters: [':']
},
referencesProvider: true,
documentHighlightProvider: true,
codeActionProvider: {
resolveProvider: true
},
documentFormattingProvider: true,
documentRangeFormattingProvider: true,
documentOnTypeFormattingProvider: {
firstTriggerCharacter: ':'
},
renameProvider: {
prepareProvider: true
},
documentLinkProvider: {
resolveProvider: true
},
colorProvider: true,
declarationProvider: true,
foldingRangeProvider: true,
implementationProvider: true,
selectionRangeProvider: true,
typeDefinitionProvider: true,
callHierarchyProvider: true,
semanticTokensProvider: {
legend: {
tokenTypes: [],
tokenModifiers: []
},
range: true,
full: {
delta: true
}
},
workspace: {
fileOperations: {
// Static reg is folders + .txt files with operation kind in the path
didCreate: {
filters: [{scheme: 'file', pattern: {glob: '**/created-static/**{/,/*.txt}'}}]
},
didRename: {
filters: [
{scheme: 'file', pattern: {glob: '**/renamed-static/**/', matches: 'folder'}},
{scheme: 'file', pattern: {glob: '**/renamed-static/**/*.txt', matches: 'file'}}
]
},
didDelete: {
filters: [{scheme: 'file', pattern: {glob: '**/deleted-static/**{/,/*.txt}'}}]
},
willCreate: {
filters: [{scheme: 'file', pattern: {glob: '**/created-static/**{/,/*.txt}'}}]
},
willRename: {
filters: [
{scheme: 'file', pattern: {glob: '**/renamed-static/**/', matches: 'folder'}},
{scheme: 'file', pattern: {glob: '**/renamed-static/**/*.txt', matches: 'file'}}
]
},
willDelete: {
filters: [{scheme: 'file', pattern: {glob: '**/deleted-static/**{/,/*.txt}'}}]
},
},
},
linkedEditingRangeProvider: true
}
return {capabilities, customResults: {hello: 'world'}}
})
connection.onInitialized(() => {
// Dynamic reg is folders + .js files with operation kind in the path
connection.client.register(DidCreateFilesNotification.type, {
filters: [{scheme: 'file', pattern: {glob: '**/created-dynamic/**{/,/*.js}'}}]
})
connection.client.register(DidRenameFilesNotification.type, {
filters: [
{scheme: 'file', pattern: {glob: '**/renamed-dynamic/**/', matches: 'folder'}},
{scheme: 'file', pattern: {glob: '**/renamed-dynamic/**/*.js', matches: 'file'}}
]
})
connection.client.register(DidDeleteFilesNotification.type, {
filters: [{scheme: 'file', pattern: {glob: '**/deleted-dynamic/**{/,/*.js}'}}]
})
connection.client.register(WillCreateFilesRequest.type, {
filters: [{scheme: 'file', pattern: {glob: '**/created-dynamic/**{/,/*.js}'}}]
})
connection.client.register(WillRenameFilesRequest.type, {
filters: [
{scheme: 'file', pattern: {glob: '**/renamed-dynamic/**/', matches: 'folder'}},
{scheme: 'file', pattern: {glob: '**/renamed-dynamic/**/*.js', matches: 'file'}}
]
})
connection.client.register(WillDeleteFilesRequest.type, {
filters: [{scheme: 'file', pattern: {glob: '**/deleted-dynamic/**{/,/*.js}'}}]
})
})
connection.onDeclaration((params) => {
assert.equal(params.position.line, 1)
assert.equal(params.position.character, 1)
return {uri: params.textDocument.uri, range: {start: {line: 1, character: 1}, end: {line: 1, character: 2}}}
})
connection.onDefinition((params) => {
assert.equal(params.position.line, 1)
assert.equal(params.position.character, 1)
return {uri: params.textDocument.uri, range: {start: {line: 0, character: 0}, end: {line: 0, character: 1}}}
})
connection.onHover((_params) => {
return {
contents: {
kind: MarkupKind.PlainText,
value: 'foo'
}
}
})
connection.onCompletion((_params) => {
return [
{label: 'item', insertText: 'text'}
]
})
connection.onCompletionResolve((item) => {
item.detail = 'detail'
return item
})
connection.onSignatureHelp((_params) => {
const result = {
signatures: [
SignatureInformation.create('label', 'doc', ParameterInformation.create('label', 'doc'))
],
activeSignature: 1,
activeParameter: 1
}
return result
})
connection.onReferences((params) => {
return [
Location.create(params.textDocument.uri, Range.create(0, 0, 0, 0)),
Location.create(params.textDocument.uri, Range.create(1, 1, 1, 1))
]
})
connection.onDocumentHighlight((_params) => {
return [
DocumentHighlight.create(Range.create(2, 2, 2, 2), DocumentHighlightKind.Read)
]
})
connection.onCodeAction((_params) => {
return [
CodeAction.create('title', Command.create('title', 'id'))
]
})
connection.onCodeActionResolve((codeAction) => {
codeAction.title = 'resolved'
return codeAction
})
connection.onDocumentFormatting((_params) => {
return [
TextEdit.insert(Position.create(0, 0), 'insert')
]
})
connection.onDocumentRangeFormatting((_params) => {
return [
TextEdit.del(Range.create(1, 1, 1, 2))
]
})
connection.onDocumentOnTypeFormatting((_params) => {
return [
TextEdit.replace(Range.create(2, 2, 2, 3), 'replace')
]
})
connection.onPrepareRename((_params) => {
return Range.create(1, 1, 1, 2)
})
connection.onRenameRequest((_params) => {
return {documentChanges: []}
})
connection.onDocumentLinks((_params) => {
return [
DocumentLink.create(Range.create(1, 1, 1, 2))
]
})
connection.onDocumentLinkResolve((link) => {
link.target = URI.file('/target.txt').toString()
return link
})
connection.onDocumentColor((_params) => {
return [
ColorInformation.create(Range.create(1, 1, 1, 2), Color.create(1, 1, 1, 1))
]
})
connection.onColorPresentation((_params) => {
return [
ColorPresentation.create('label')
]
})
connection.onFoldingRanges((_params) => {
return [
FoldingRange.create(1, 2)
]
})
connection.onImplementation((params) => {
assert.equal(params.position.line, 1)
assert.equal(params.position.character, 1)
return {uri: params.textDocument.uri, range: {start: {line: 2, character: 2}, end: {line: 3, character: 3}}}
})
connection.onSelectionRanges((_params) => {
return [
SelectionRange.create(Range.create(1, 2, 3, 4))
]
})
let lastFileOperationRequest
connection.workspace.onDidCreateFiles((params) => {lastFileOperationRequest = {type: 'create', params}})
connection.workspace.onDidRenameFiles((params) => {lastFileOperationRequest = {type: 'rename', params}})
connection.workspace.onDidDeleteFiles((params) => {lastFileOperationRequest = {type: 'delete', params}})
connection.onRequest(
new ProtocolRequestType('testing/lastFileOperationRequest'),
() => {
return lastFileOperationRequest
},
)
connection.workspace.onWillCreateFiles((params) => {
const createdFilenames = params.files.map((f) => `${f.uri}`).join('\n')
return {
documentChanges: [{
textDocument: {uri: '/dummy-edit', version: null},
edits: [
TextEdit.insert(Position.create(0, 0), `WILL CREATE:\n${createdFilenames}`),
]
}],
}
})
connection.workspace.onWillRenameFiles((params) => {
const renamedFilenames = params.files.map((f) => `${f.oldUri} -> ${f.newUri}`).join('\n')
return {
documentChanges: [{
textDocument: {uri: '/dummy-edit', version: null},
edits: [
TextEdit.insert(Position.create(0, 0), `WILL RENAME:\n${renamedFilenames}`),
]
}],
}
})
connection.workspace.onWillDeleteFiles((params) => {
const deletedFilenames = params.files.map((f) => `${f.uri}`).join('\n')
return {
documentChanges: [{
textDocument: {uri: '/dummy-edit', version: null},
edits: [
TextEdit.insert(Position.create(0, 0), `WILL DELETE:\n${deletedFilenames}`),
]
}],
}
})
connection.onTypeDefinition((params) => {
assert.equal(params.position.line, 1)
assert.equal(params.position.character, 1)
return {uri: params.textDocument.uri, range: {start: {line: 2, character: 2}, end: {line: 3, character: 3}}}
})
connection.languages.callHierarchy.onPrepare((params) => {
return [
{
kind: SymbolKind.Function,
name: 'name',
range: Range.create(1, 1, 1, 1),
selectionRange: Range.create(2, 2, 2, 2),
uri: params.textDocument.uri
}
]
})
connection.languages.callHierarchy.onIncomingCalls((params) => {
return [
{
from: params.item,
fromRanges: [Range.create(1, 1, 1, 1)]
}
]
})
connection.languages.callHierarchy.onOutgoingCalls((params) => {
return [
{
to: params.item,
fromRanges: [Range.create(1, 1, 1, 1)]
}
]
})
connection.languages.semanticTokens.onRange(() => {
return {
resultId: '1',
data: []
}
})
connection.languages.semanticTokens.on(() => {
return {
resultId: '2',
data: []
}
})
connection.languages.semanticTokens.onDelta(() => {
return {
resultId: '3',
data: []
}
})
connection.languages.onLinkedEditingRange(() => {
return {
ranges: [Range.create(1, 1, 1, 1)],
wordPattern: '\\w'
}
})
connection.onRequest(
new ProtocolRequestType('testing/sendSampleProgress'),
async (_, __) => {
const progressToken = 'TEST-PROGRESS-TOKEN'
await connection.sendRequest(WorkDoneProgressCreateRequest.type, {token: progressToken})
connection.sendProgress(WorkDoneProgress.type, progressToken, {kind: 'begin', title: 'Test Progress'})
connection.sendProgress(WorkDoneProgress.type, progressToken, {kind: 'report', percentage: 50, message: 'Halfway!'})
connection.sendProgress(WorkDoneProgress.type, progressToken, {kind: 'end', message: 'Completed!'})
},
)
// Listen on the connection
connection.listen()

View file

@ -0,0 +1,4 @@
{
"suggest.timeout": 5000,
"tslint.enable": false
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,198 @@
import { Neovim } from '@chemzqm/neovim'
import Floating from '../../completion/floating'
import sources from '../../sources'
import { CompleteResult, FloatConfig, ISource, SourceType } from '../../types'
import helper from '../helper'
let nvim: Neovim
let source: ISource
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
source = {
name: 'float',
priority: 10,
enable: true,
sourceType: SourceType.Native,
doComplete: (): Promise<CompleteResult> => Promise.resolve({
items: [{
word: 'foo',
info: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
}, {
word: 'foot',
info: 'foot'
}, {
word: 'football',
}]
})
}
sources.addSource(source)
})
afterAll(async () => {
sources.removeSource(source)
await helper.shutdown()
})
afterEach(async () => {
await helper.reset()
})
describe('completion float', () => {
it('should not show float window when disabled', async () => {
helper.updateConfiguration('suggest.floatEnable', false)
await helper.edit()
await nvim.input('if')
await helper.visible('foo', 'float')
let hasFloat = await nvim.call('coc#float#has_float')
expect(hasFloat).toBe(0)
})
it('should cancel float window', async () => {
await helper.edit()
await nvim.input('if')
await helper.visible('foo', 'float')
let items = await helper.getItems()
expect(items[0].word).toBe('foo')
expect(items[0].info.length > 0).toBeTruthy()
await helper.selectCompleteItem(0)
await helper.wait(30)
let hasFloat = await nvim.call('coc#float#has_float')
expect(hasFloat).toBe(0)
})
it('should adjust float window position', async () => {
await helper.edit()
await nvim.setLine(' '.repeat(70))
await nvim.input('Af')
await helper.visible('foo', 'float')
await nvim.input('<C-n>')
await helper.wait(100)
let floatWin = await helper.getFloat()
let config = await floatWin.getConfig()
expect(config.col + config.width).toBeLessThan(180)
})
it('should redraw float window on item change', async () => {
await helper.edit()
await nvim.setLine(' '.repeat(70))
await nvim.input('Af')
await helper.visible('foo', 'float')
await nvim.call('nvim_select_popupmenu_item', [0, false, false, {}])
await helper.wait(50)
await nvim.input('<C-n>')
await helper.wait(100)
let floatWin = await helper.getFloat()
let buf = await floatWin.buffer
let lines = await buf.lines
expect(lines.length).toBeGreaterThan(0)
expect(lines[0]).toMatch('foot')
})
it('should hide float window when item info is empty', async () => {
await helper.edit()
await nvim.setLine(' '.repeat(70))
await nvim.input('Af')
await helper.visible('foo', 'float')
await nvim.call('nvim_select_popupmenu_item', [0, false, false, {}])
await helper.wait(10)
await nvim.input('<C-n>')
await helper.wait(10)
await nvim.input('<C-n>')
await helper.wait(100)
let hasFloat = await nvim.call('coc#float#has_float')
expect(hasFloat).toBe(0)
})
it('should hide float window after completion', async () => {
await helper.edit()
await nvim.setLine(' '.repeat(70))
await nvim.input('Af')
await helper.visible('foo', 'float')
await nvim.input('<C-n>')
await helper.wait(100)
await nvim.input('<C-y>')
await helper.wait(30)
let hasFloat = await nvim.call('coc#float#has_float')
expect(hasFloat).toBe(0)
})
})
describe('float config', () => {
beforeEach(async () => {
await nvim.setLine('foob foot')
await nvim.input('of')
await nvim.input('<C-n>')
})
async function createFloat(config: Partial<FloatConfig>, docs = [{ filetype: 'txt', content: 'doc' }], isVim = false): Promise<Floating> {
let floating = new Floating(nvim, isVim)
let bounding = { col: 6, row: 2, height: 3, width: 16, scrollbar: false }
await floating.show(docs, bounding, Object.assign({
excludeImages: true,
border: false,
}, config))
return floating
}
async function getFloat(): Promise<number> {
let ids = await nvim.call('coc#float#get_float_win_list')
return Array.isArray(ids) ? ids[0] || -1 : -1
}
async function getRelated(winid: number, kind: string): Promise<number> {
if (!winid || winid == -1) return -1
let win = nvim.createWindow(winid)
let related = await win.getVar('related') as number[]
if (!related || !related.length) return -1
for (let id of related) {
let w = nvim.createWindow(id)
let v = await w.getVar('kind')
if (v == kind) {
return id
}
}
return -1
}
it('should not shown with empty lines', async () => {
await createFloat({}, [{ filetype: 'txt', content: '' }])
let winid = await nvim.call('GetFloatWin')
expect(winid).toBe(0)
})
it('should shown on vim', async () => {
let float = await createFloat({}, [{ filetype: 'txt', content: 'ff' }], true)
let winid = await nvim.call('GetFloatWin')
expect(winid).toBeGreaterThan(0)
float.close()
})
it('should show window with border', async () => {
await createFloat({ border: true })
let winid = await getFloat()
expect(winid).toBeGreaterThan(0)
let id = await getRelated(winid, 'border')
expect(id).toBeGreaterThan(0)
})
it('should change window highlights', async () => {
await createFloat({ border: true, highlight: 'WarningMsg', borderhighlight: 'MoreMsg' })
let winid = await getFloat()
expect(winid).toBeGreaterThan(0)
let win = nvim.createWindow(winid)
let res = await win.getOption('winhl') as string
expect(res).toMatch('WarningMsg')
let id = await getRelated(winid, 'border')
expect(id).toBeGreaterThan(0)
win = nvim.createWindow(id)
res = await win.getOption('winhl') as string
expect(res).toMatch('MoreMsg')
})
it('should add shadow and winblend', async () => {
await createFloat({ shadow: true, winblend: 30 })
let winid = await getFloat()
expect(winid).toBeGreaterThan(0)
})
})

View file

@ -0,0 +1,317 @@
import { Neovim } from '@chemzqm/neovim'
import { Disposable } from 'vscode-languageserver-protocol'
import { CompletionItem, CompletionList, InsertTextFormat, Position, Range, TextEdit } from 'vscode-languageserver-types'
import languages from '../../languages'
import { CompletionItemProvider } from '../../provider'
import snippetManager from '../../snippets/manager'
import { disposeAll } from '../../util'
import helper from '../helper'
let nvim: Neovim
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
disposeAll(disposables)
await helper.reset()
})
describe('language source', () => {
describe('additionalTextEdits', () => {
it('should fix cursor position with plain text on additionalTextEdits', async () => {
let provider: CompletionItemProvider = {
provideCompletionItems: async (): Promise<CompletionItem[]> => [{
label: 'foo',
filterText: 'foo',
additionalTextEdits: [TextEdit.insert(Position.create(0, 0), 'a\nbar')]
}]
}
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider))
await nvim.input('if')
await helper.waitPopup()
await helper.selectCompleteItem(0)
await helper.waitFor('getline', ['.'], 'barfoo')
})
it('should fix cursor position with snippet on additionalTextEdits', async () => {
let provider: CompletionItemProvider = {
provideCompletionItems: async (): Promise<CompletionItem[]> => [{
label: 'if',
insertTextFormat: InsertTextFormat.Snippet,
textEdit: { range: Range.create(0, 0, 0, 1), newText: 'if($1)' },
additionalTextEdits: [TextEdit.insert(Position.create(0, 0), 'bar ')],
preselect: true
}]
}
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider))
await nvim.input('ii')
await helper.waitPopup()
let res = await helper.getItems()
let idx = res.findIndex(o => o.menu == '[edit]')
await helper.selectCompleteItem(idx)
await helper.waitFor('col', ['.'], 8)
})
it('should fix cursor position with plain text snippet on additionalTextEdits', async () => {
let provider: CompletionItemProvider = {
provideCompletionItems: async (): Promise<CompletionItem[]> => [{
label: 'if',
insertTextFormat: InsertTextFormat.Snippet,
textEdit: { range: Range.create(0, 0, 0, 2), newText: 'do$0' },
additionalTextEdits: [TextEdit.insert(Position.create(0, 0), 'bar ')],
preselect: true
}]
}
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider))
await nvim.input('iif')
await helper.waitPopup()
let items = await helper.getItems()
let idx = items.findIndex(o => o.word == 'do' && o.menu == '[edit]')
await helper.selectCompleteItem(idx)
await helper.waitFor('getline', ['.'], 'bar do')
await helper.waitFor('col', ['.'], 7)
})
it('should fix cursor position with nested snippet on additionalTextEdits', async () => {
let res = await snippetManager.insertSnippet('func($1)$0')
expect(res).toBe(true)
let provider: CompletionItemProvider = {
provideCompletionItems: async (): Promise<CompletionItem[]> => [{
label: 'if',
insertTextFormat: InsertTextFormat.Snippet,
insertText: 'do$0',
additionalTextEdits: [TextEdit.insert(Position.create(0, 0), 'bar ')],
preselect: true
}]
}
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider))
await nvim.input('if')
await helper.waitPopup()
await helper.selectCompleteItem(0)
await helper.waitFor('getline', ['.'], 'bar func(do)')
let [, lnum, col] = await nvim.call('getcurpos')
expect(lnum).toBe(1)
expect(col).toBe(12)
})
it('should fix cursor position and keep placeholder with snippet on additionalTextEdits', async () => {
let text = 'foo0bar1'
await nvim.setLine(text)
let provider: CompletionItemProvider = {
provideCompletionItems: async (): Promise<CompletionItem[]> => [{
label: 'var',
insertTextFormat: InsertTextFormat.Snippet,
textEdit: { range: Range.create(0, text.length + 1, 0, text.length + 1), newText: '${1:foo} = foo0bar1' },
additionalTextEdits: [TextEdit.del(Range.create(0, 0, 0, text.length + 1))],
preselect: true
}]
}
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider, ['.']))
await nvim.input('A.')
await helper.waitPopup()
let res = await helper.getItems()
let idx = res.findIndex(o => o.menu == '[edit]')
await helper.selectCompleteItem(idx)
await helper.waitFor('getline', ['.'], 'foo = foo0bar1')
await helper.wait(50)
expect(snippetManager.session).toBeDefined()
let [, lnum, col] = await nvim.call('getcurpos')
expect(lnum).toBe(1)
expect(col).toBe(3)
})
it('should cancel current snippet session when additionalTextEdits inside snippet', async () => {
await nvim.input('i')
await snippetManager.insertSnippet('foo($1, $2)$0', true)
let provider: CompletionItemProvider = {
provideCompletionItems: async (): Promise<CompletionItem[]> => [{
label: 'bar',
insertTextFormat: InsertTextFormat.Snippet,
textEdit: { range: Range.create(0, 4, 0, 5), newText: 'bar($1)' },
additionalTextEdits: [TextEdit.del(Range.create(0, 0, 0, 3))]
}]
}
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider, ['.']))
await nvim.input('b')
await helper.waitPopup()
let res = await helper.getItems()
let idx = res.findIndex(o => o.menu == '[edit]')
await helper.selectCompleteItem(idx)
await helper.waitFor('getline', ['.'], '(bar(), )')
let col = await nvim.call('col', ['.'])
expect(col).toBe(6)
})
})
describe('filterText', () => {
it('should fix input for snippet item', async () => {
let provider: CompletionItemProvider = {
provideCompletionItems: async (): Promise<CompletionItem[]> => [{
label: 'foo',
filterText: 'foo',
insertText: '${1:foo}($2)',
insertTextFormat: InsertTextFormat.Snippet,
}]
}
disposables.push(languages.registerCompletionItemProvider('snippets-test', 'st', null, provider))
await nvim.input('if')
await helper.waitPopup()
await nvim.input('<C-n>')
await helper.waitFor('getline', ['.'], 'foo')
})
it('should fix filterText of complete item', async () => {
let provider: CompletionItemProvider = {
provideCompletionItems: async (): Promise<CompletionItem[]> => [{
label: 'name',
sortText: '11',
textEdit: {
range: Range.create(0, 1, 0, 2),
newText: '?.name'
}
}]
}
disposables.push(languages.registerCompletionItemProvider('name', 'N', null, provider, ['.']))
await nvim.setLine('t')
await nvim.input('A.')
await helper.waitPopup()
await helper.selectCompleteItem(0)
await helper.waitFor('getline', ['.'], 't?.name')
})
})
describe('inComplete result', () => {
it('should filter in complete request', async () => {
let provider: CompletionItemProvider = {
provideCompletionItems: async (doc, pos, token, context): Promise<CompletionList> => {
let option = (context as any).option
if (context.triggerCharacter == '.') {
return {
isIncomplete: true,
items: [
{
label: 'foo'
}, {
label: 'bar'
}
]
}
}
if (option.input == 'f') {
if (token.isCancellationRequested) return
return {
isIncomplete: true,
items: [
{
label: 'foo'
}
]
}
}
if (option.input == 'fo') {
if (token.isCancellationRequested) return
return {
isIncomplete: false,
items: [
{
label: 'foo'
}
]
}
}
}
}
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider, ['.']))
await nvim.input('i.')
await helper.waitPopup()
await nvim.input('fo')
await helper.wait(50)
let res = await helper.getItems()
expect(res.length).toBe(1)
})
})
describe('textEdit', () => {
it('should fix bad range', async () => {
let provider: CompletionItemProvider = {
provideCompletionItems: async (): Promise<CompletionItem[]> => [{
label: 'foo',
filterText: 'foo',
textEdit: { range: Range.create(0, 0, 0, 0), newText: 'foo' },
}]
}
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider))
await nvim.input('if')
await helper.waitPopup()
await helper.selectCompleteItem(0)
await helper.waitFor('getline', ['.'], 'foo')
})
it('should applyEdits for empty word', async () => {
let provider: CompletionItemProvider = {
provideCompletionItems: async (): Promise<CompletionItem[]> => [{
label: '',
filterText: '!',
textEdit: { range: Range.create(0, 0, 0, 1), newText: 'foo' },
data: { word: '' }
}]
}
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider, ['!']))
await nvim.input('i!')
await helper.waitPopup()
await helper.selectCompleteItem(0)
await helper.waitFor('getline', ['.'], 'foo')
})
it('should provide word when textEdit after startcol', async () => {
// some LS would send textEdit after first character,
// need fix the word from newText
let provider: CompletionItemProvider = {
provideCompletionItems: async (_, position): Promise<CompletionItem[]> => {
if (position.line != 0) return null
return [{
label: 'bar',
filterText: 'ar',
textEdit: {
range: Range.create(0, 1, 0, 1),
newText: 'ar'
}
}]
}
}
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider))
await nvim.input('ib')
await helper.waitPopup()
let context = await nvim.getVar('coc#_context') as any
expect(context.start).toBe(1)
expect(context.candidates[0].word).toBe('ar')
})
it('should adjust completion position by textEdit start position', async () => {
let provider: CompletionItemProvider = {
provideCompletionItems: async (_document, _position, _token, context): Promise<CompletionItem[]> => {
if (!context.triggerCharacter) return
return [{
label: 'foo',
textEdit: {
range: Range.create(0, 0, 0, 1),
newText: '?foo'
}
}]
}
}
disposables.push(languages.registerCompletionItemProvider('fix', 'f', null, provider, ['?']))
await nvim.input('i?')
await helper.waitPopup()
await nvim.eval('feedkeys("\\<C-n>", "in")')
await helper.waitFor('getline', ['.'], '?foo')
})
})
})

View file

@ -0,0 +1,65 @@
import { Neovim } from '@chemzqm/neovim'
import helper from '../helper'
import { ISource, SourceType, CompleteResult } from '../../types'
import sources from '../../sources'
import workspace from '../../workspace'
let nvim: Neovim
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
await helper.reset()
})
describe('native sources', () => {
it('should works for around source', async () => {
let doc = await workspace.document
await nvim.setLine('foo ')
await doc.synchronize()
let { mode } = await nvim.mode
expect(mode).toBe('n')
await nvim.input('Af')
await helper.waitPopup()
let res = await helper.visible('foo', 'around')
expect(res).toBe(true)
await nvim.input('<esc>')
})
it('should works for buffer source', async () => {
await helper.createDocument()
await nvim.command('set hidden')
let doc = await helper.createDocument()
await nvim.setLine('other')
await nvim.command('bp')
await doc.synchronize()
let { mode } = await nvim.mode
expect(mode).toBe('n')
await nvim.input('io')
let res = await helper.visible('other', 'buffer')
expect(res).toBe(true)
})
it('should works with file source', async () => {
await helper.edit()
await nvim.input('i/')
await helper.waitPopup()
let items = await helper.getItems()
expect(items.length).toBeGreaterThan(0)
let res = await helper.visible(items[0].word, 'file')
expect(res).toBe(true)
await nvim.input('<esc>')
await nvim.input('o./')
await helper.waitPopup()
items = await helper.getItems()
let item = items.find(o => o.word == 'vimrc')
expect(item).toBeTruthy()
})
})

View file

@ -0,0 +1,144 @@
import { CompletionItemKind, TextEdit, Position } from 'vscode-languageserver-types'
import { matchScore, matchScoreWithPositions } from '../../completion/match'
import { shouldStop } from '../../completion/util'
import { getCharCodes } from '../../util/fuzzy'
import { getStartColumn, getKindString } from '../../sources/source-language'
import { CompleteOption } from '../../types'
describe('getKindString()', () => {
it('should get kind text', async () => {
let map = new Map()
map.set(CompletionItemKind.Enum, 'E')
let res = getKindString(CompletionItemKind.Enum, map, '')
expect(res).toBe('E')
})
it('should get default value', async () => {
let map = new Map()
let res = getKindString(CompletionItemKind.Enum, map, 'D')
expect(res).toBe('D')
})
})
describe('shouldStop', () => {
function createOption(bufnr: number, linenr: number, line: string, colnr: number): Pick<CompleteOption, 'bufnr' | 'linenr' | 'line' | 'colnr'> {
return { bufnr, linenr, line, colnr }
}
it('should check stop', async () => {
let opt = createOption(1, 1, 'a', 2)
expect(shouldStop(1, 'foo', { line: '', col: 2, lnum: 1, changedtick: 1, pre: '' }, opt)).toBe(true)
expect(shouldStop(1, 'foo', { line: '', col: 2, lnum: 1, changedtick: 1, pre: ' ' }, opt)).toBe(true)
expect(shouldStop(1, 'foo', { line: '', col: 2, lnum: 1, changedtick: 1, pre: 'fo' }, opt)).toBe(true)
expect(shouldStop(2, 'foo', { line: '', col: 2, lnum: 1, changedtick: 1, pre: 'foob' }, opt)).toBe(true)
expect(shouldStop(1, 'foo', { line: '', col: 2, lnum: 2, changedtick: 1, pre: 'foob' }, opt)).toBe(true)
expect(shouldStop(1, 'foo', { line: '', col: 2, lnum: 1, changedtick: 1, pre: 'barb' }, opt)).toBe(true)
})
})
describe('getStartColumn()', () => {
it('should get start col', async () => {
expect(getStartColumn('', [{ label: 'foo' }])).toBe(undefined)
expect(getStartColumn('', [
{ label: 'foo', textEdit: TextEdit.insert(Position.create(0, 0), 'a') },
{ label: 'bar' }])).toBe(undefined)
expect(getStartColumn('foo', [
{ label: 'foo', textEdit: TextEdit.insert(Position.create(0, 0), 'a') },
{ label: 'bar', textEdit: TextEdit.insert(Position.create(0, 1), 'b') }])).toBe(undefined)
expect(getStartColumn('foo', [
{ label: 'foo', textEdit: TextEdit.insert(Position.create(0, 2), 'a') },
{ label: 'bar', textEdit: TextEdit.insert(Position.create(0, 2), 'b') }])).toBe(2)
})
})
describe('matchScore', () => {
function score(word: string, input: string): number {
return matchScore(word, getCharCodes(input))
}
it('should match score for last letter', () => {
expect(score('#!3', '3')).toBe(1)
expect(score('bar', 'f')).toBe(0)
})
it('should match first letter', () => {
expect(score('abc', 'a')).toBe(5)
expect(score('Abc', 'a')).toBe(2.5)
expect(score('__abc', 'a')).toBe(2)
expect(score('$Abc', 'a')).toBe(1)
expect(score('$Abc', 'A')).toBe(2)
expect(score('$Abc', '$A')).toBe(6)
expect(score('$Abc', '$a')).toBe(5.5)
expect(score('foo_bar', 'b')).toBe(2)
expect(score('foo_Bar', 'b')).toBe(1)
expect(score('_foo_Bar', 'b')).toBe(0.5)
expect(score('_foo_Bar', 'f')).toBe(2)
expect(score('bar', 'a')).toBe(1)
expect(score('fooBar', 'B')).toBe(2)
expect(score('fooBar', 'b')).toBe(1)
})
it('should match follow letters', () => {
expect(score('abc', 'ab')).toBe(6)
expect(score('adB', 'ab')).toBe(5.75)
expect(score('adb', 'ab')).toBe(5.1)
expect(score('adCB', 'ab')).toBe(5.05)
expect(score('a_b_c', 'ab')).toBe(6)
expect(score('FooBar', 'fb')).toBe(3.25)
expect(score('FBar', 'fb')).toBe(3)
expect(score('FooBar', 'FB')).toBe(6)
expect(score('FBar', 'FB')).toBe(6)
expect(score('a__b', 'a__b')).toBe(8)
expect(score('aBc', 'ab')).toBe(5.5)
expect(score('a_B_c', 'ab')).toBe(5.75)
expect(score('abc', 'abc')).toBe(7)
expect(score('abc', 'aC')).toBe(0)
expect(score('abc', 'ac')).toBe(5.1)
expect(score('abC', 'ac')).toBe(5.75)
expect(score('abC', 'aC')).toBe(6)
})
it('should only allow search once', () => {
expect(score('foobar', 'fbr')).toBe(5.2)
expect(score('foobaRow', 'fbr')).toBe(5.85)
expect(score('foobaRow', 'fbR')).toBe(6.1)
expect(score('foobar', 'fa')).toBe(5.1)
})
it('should have higher score for strict match', () => {
expect(score('language-client-protocol', 'lct')).toBe(6.1)
expect(score('language-client-types', 'lct')).toBe(7)
})
it('should find highest score', () => {
expect(score('ArrayRotateTail', 'art')).toBe(3.6)
})
})
describe('matchScoreWithPositions', () => {
function assertMatch(word: string, input: string, res: [number, ReadonlyArray<number>] | undefined): void {
let result = matchScoreWithPositions(word, getCharCodes(input))
if (!res) {
expect(result).toBeUndefined()
} else {
expect(result).toEqual(res)
}
}
it('should return undefined when not match found', async () => {
assertMatch('a', 'abc', undefined)
assertMatch('a', '', undefined)
assertMatch('ab', 'ac', undefined)
})
it('should find matches by position fix', async () => {
assertMatch('this', 'tih', [5.6, [0, 1, 2]])
assertMatch('globalThis', 'tihs', [2.6, [6, 7, 8, 9]])
})
it('should find matched positions', async () => {
assertMatch('this', 'th', [6, [0, 1]])
assertMatch('foo_bar', 'fb', [6, [0, 4]])
assertMatch('assertMatch', 'am', [5.75, [0, 6]])
})
})

View file

@ -0,0 +1,68 @@
import { Neovim } from '@chemzqm/neovim'
import workspace from '../../workspace'
import helper from '../helper'
let nvim: Neovim
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
await helper.reset()
})
describe('setupDynamicAutocmd()', () => {
it('should setup autocmd on vim', async () => {
await nvim.setLine('foo')
let fn = nvim.hasFunction
nvim.hasFunction = () => {
return false
}
let called = false
workspace.registerAutocmd({
event: 'CursorMoved',
request: true,
callback: () => {
called = true
}
})
await helper.wait(50)
await nvim.command('normal! $')
await helper.wait(100)
nvim.hasFunction = fn
expect(called).toBe(true)
nvim.command(`augroup coc_dynamic_autocmd| autocmd!|augroup end`, true)
})
it('should setup user autocmd', async () => {
let called = false
workspace.registerAutocmd({
event: 'User CocJumpPlaceholder',
request: true,
callback: () => {
called = true
}
})
workspace.autocmds.setupDynamicAutocmd(true)
await helper.wait(50)
await nvim.command('doautocmd <nomodeline> User CocJumpPlaceholder')
await helper.wait(100)
expect(called).toBe(true)
})
})
describe('doAutocmd()', () => {
it('should not throw when command id does not exist', async () => {
await workspace.autocmds.doAutocmd(999, [])
})
it('should dispose', async () => {
workspace.autocmds.dispose()
})
})

View file

@ -0,0 +1,85 @@
import { Neovim } from '@chemzqm/neovim'
import os from 'os'
import path from 'path'
import fs from 'fs'
import { v4 as uuid } from 'uuid'
import Documents from '../../core/documents'
import events from '../../events'
import workspace from '../../workspace'
import helper from '../helper'
let documents: Documents
let nvim: Neovim
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
documents = workspace.documentsManager
})
afterEach(async () => {
await helper.reset()
})
afterAll(async () => {
await helper.shutdown()
})
describe('documents', () => {
it('should get document', async () => {
await helper.createDocument('bar')
let doc = await helper.createDocument('foo')
let res = documents.getDocument(doc.uri)
expect(res.uri).toBe(doc.uri)
})
it('should create document', async () => {
await helper.createDocument()
let bufnrs = await nvim.call('coc#ui#open_files', [[__filename]]) as number[]
let bufnr = bufnrs[0]
let doc = workspace.getDocument(bufnr)
expect(doc).toBeUndefined()
doc = await documents.createDocument(bufnr)
expect(doc).toBeDefined()
})
it('should check buffer rename on save', async () => {
let doc = await workspace.document
let bufnr = doc.bufnr
let name = `${uuid()}.vim`
let tmpfile = path.join(os.tmpdir(), name)
await nvim.command(`write ${tmpfile}`)
doc = workspace.getDocument(bufnr)
expect(doc).toBeDefined()
expect(doc.filetype).toBe('vim')
expect(doc.bufname).toMatch(name)
fs.unlinkSync(tmpfile)
})
it('should get current document', async () => {
let p1 = workspace.document
let p2 = workspace.document
let arr = await Promise.all([p1, p2])
expect(arr[0]).toBe(arr[1])
})
it('should get bufnrs', async () => {
await workspace.document
let bufnrs = documents.bufnrs
expect(bufnrs.length).toBe(1)
})
it('should get uri', async () => {
let doc = await workspace.document
expect(documents.uri).toBe(doc.uri)
})
it('should attach events on vim', async () => {
await documents.attach(nvim, workspace.env)
let env = Object.assign(workspace.env, { isVim: true })
documents.detach()
await documents.attach(nvim, env)
documents.detach()
await events.fire('CursorMoved', [1, [1, 1]])
})
})

View file

@ -0,0 +1,169 @@
import { Neovim } from '@chemzqm/neovim'
import Editors, { TextEditor } from '../../core/editors'
import workspace from '../../workspace'
import window from '../../window'
import events from '../../events'
import helper from '../helper'
import { disposeAll } from '../../util'
import { Disposable } from 'vscode-languageserver-protocol'
let editors: Editors
let nvim: Neovim
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
editors = workspace.editors
})
afterEach(async () => {
await helper.reset()
})
afterAll(async () => {
disposeAll(disposables)
await helper.shutdown()
})
describe('editors', () => {
function assertEditor(editor: TextEditor, tabpagenr: number, winid: number) {
expect(editor).toBeDefined()
expect(editor.tabpagenr).toBe(tabpagenr)
expect(editor.winid).toBe(winid)
}
it('should have active editor', async () => {
let winid = await nvim.call('win_getid')
let editor = window.activeTextEditor
assertEditor(editor, 1, winid)
let editors = window.visibleTextEditors
expect(editors.length).toBe(1)
})
it('should change active editor on split', async () => {
let promise = new Promise<TextEditor>(resolve => {
editors.onDidChangeActiveTextEditor(e => {
resolve(e)
}, null, disposables)
})
await nvim.command('vnew')
let editor = await promise
let winid = await nvim.call('win_getid')
expect(editor.winid).toBe(winid)
})
it('should change active editor on tabe', async () => {
let promise = new Promise<TextEditor>(resolve => {
editors.onDidChangeActiveTextEditor(e => {
if (e.document.uri.includes('foo')) {
resolve(e)
}
}, null, disposables)
})
await nvim.command('tabe a | tabe b | tabe foo')
let editor = await promise
let winid = await nvim.call('win_getid')
expect(editor.winid).toBe(winid)
})
it('should change active editor on edit', async () => {
await nvim.call('win_getid')
let fn = jest.fn()
window.onDidChangeVisibleTextEditors(() => {
fn()
}, null, disposables)
let promise = new Promise<TextEditor>(resolve => {
editors.onDidChangeActiveTextEditor(e => {
resolve(e)
})
})
await nvim.command('edit foo')
let editor = await promise
expect(editor.document.uri).toMatch('foo')
expect(fn).toBeCalled()
})
it('should change active editor on window switch', async () => {
let winid = await nvim.call('win_getid')
await nvim.command('vs foo')
await nvim.command('wincmd p')
let curr = editors.activeTextEditor
expect(curr.winid).toBe(winid)
expect(editors.visibleTextEditors.length).toBe(2)
})
it('should not create editor for float window', async () => {
let fn = jest.fn()
await nvim.call('win_getid')
editors.onDidChangeActiveTextEditor(e => {
fn()
})
let res = await nvim.call('coc#float#create_float_win', [0, 0, {
relative: 'editor',
row: 1,
col: 1,
width: 10,
height: 1,
lines: ['foo']
}])
await nvim.call('win_gotoid', [res[0]])
await events.fire('CursorHold', [res[1]])
await nvim.command('wincmd p')
expect(fn).toBeCalledTimes(0)
expect(editors.visibleTextEditors.length).toBe(1)
})
it('should cleanup on CursorHold', async () => {
let winid = await nvim.call('win_getid')
let promise = new Promise<TextEditor>(resolve => {
editors.onDidChangeActiveTextEditor(e => {
if (e.document.uri.includes('foo')) {
resolve(e)
}
}, null, disposables)
})
await nvim.command('tabe foo')
await promise
await nvim.call('win_execute', [winid, 'noa close'])
let bufnr = await nvim.eval("bufnr('%')")
await events.fire('CursorHold', [bufnr])
expect(editors.visibleTextEditors.length).toBe(1)
})
it('should cleanup on create', async () => {
let winid = await nvim.call('win_getid')
let promise = new Promise<TextEditor>(resolve => {
editors.onDidChangeActiveTextEditor(e => {
if (e.document.uri.includes('foo')) {
resolve(e)
}
}, null, disposables)
})
await nvim.command('tabe foo')
await promise
await nvim.call('win_execute', [winid, 'noa close'])
await nvim.command('edit bar')
expect(editors.visibleTextEditors.length).toBe(2)
})
it('should have corrent tabnr after tab changed', async () => {
await nvim.command('tabe')
await helper.waitValue(() => {
return editors.visibleTextEditors.length
}, 2)
let editor = editors.visibleTextEditors.find(o => o.tabpagenr == 2)
await nvim.command('normal! 1gt')
await nvim.command('tabe')
await helper.waitValue(() => {
return editors.visibleTextEditors.length
}, 3)
expect(editor.tabpagenr).toBe(3)
await nvim.command('tabc')
await helper.waitValue(() => {
return editors.visibleTextEditors.length
}, 2)
expect(editor.tabpagenr).toBe(2)
})
})

View file

@ -0,0 +1,380 @@
import bser from 'bser'
import fs from 'fs'
import net from 'net'
import os from 'os'
import path from 'path'
import Watchman, { FileChangeItem, isValidWatchRoot } from '../../core/watchman'
import helper from '../helper'
import { Disposable } from 'vscode-languageserver-protocol'
import Configurations from '../../configuration/index'
import WorkspaceFolderController from '../../core/workspaceFolder'
import { FileSystemWatcherManager, FileSystemWatcher } from '../../core/fileSystemWatcher'
import { disposeAll } from '../../util'
import { URI } from 'vscode-uri'
let server: net.Server
let client: net.Socket
const cwd = process.cwd()
const sockPath = path.join(os.tmpdir(), `watchman-fake-${process.pid}`)
process.env.WATCHMAN_SOCK = sockPath
let workspaceFolder: WorkspaceFolderController
let watcherManager: FileSystemWatcherManager
let configurations: Configurations
let disposables: Disposable[] = []
function wait(ms: number): Promise<any> {
return new Promise(resolve => {
setTimeout(() => {
resolve(undefined)
}, ms)
})
}
function createFileChange(file: string, isNew = true, exists = true): FileChangeItem {
return {
size: 1,
name: file,
exists,
new: isNew,
type: 'f',
mtime_ms: Date.now()
}
}
function sendResponse(data: any): void {
client.write(bser.dumpToBuffer(data))
}
function sendSubscription(uid: string, root: string, files: FileChangeItem[]): void {
client.write(bser.dumpToBuffer({
subscription: uid,
root,
files
}))
}
let capabilities: any
let watchResponse: any
beforeAll(done => {
let userConfigFile = path.join(process.env.COC_VIMCONFIG, 'coc-settings.json')
configurations = new Configurations(userConfigFile, {
$removeConfigurationOption: () => {},
$updateConfigurationOption: () => {}
})
workspaceFolder = new WorkspaceFolderController(configurations)
watcherManager = new FileSystemWatcherManager(workspaceFolder, '')
watcherManager.attach(helper.createNullChannel())
// create a mock sever for watchman
server = net.createServer(c => {
client = c
c.on('data', data => {
let obj = bser.loadFromBuffer(data)
if (obj[0] == 'watch-project') {
sendResponse(watchResponse || { watch: obj[1], warning: 'warning' })
} else if (obj[0] == 'unsubscribe') {
sendResponse({ path: obj[1] })
} else if (obj[0] == 'clock') {
sendResponse({ clock: 'clock' })
} else if (obj[0] == 'version') {
let { optional, required } = obj[1]
let res = {}
for (let key of optional) {
res[key] = true
}
for (let key of required) {
res[key] = true
}
sendResponse({ capabilities: capabilities || res })
} else if (obj[0] == 'subscribe') {
sendResponse({ subscribe: obj[2] })
} else {
sendResponse({})
}
})
})
server.on('error', err => {
throw err
})
server.listen(sockPath, () => {
done()
})
})
afterEach(async () => {
disposeAll(disposables)
capabilities = undefined
watchResponse = undefined
})
afterAll(async () => {
watcherManager.dispose()
server.removeAllListeners()
server.close()
if (fs.existsSync(sockPath)) {
fs.unlinkSync(sockPath)
}
})
describe('watchman', () => {
it('should throw error when not watching', async () => {
let client = new Watchman(null)
disposables.push(client)
let fn = async () => {
await client.subscribe('**/*', () => {})
}
await expect(fn()).rejects.toThrow(/not watching/)
})
it('should checkCapability', async () => {
let client = new Watchman(null)
let res = await client.checkCapability()
expect(res).toBe(true)
capabilities = { relative_root: false }
res = await client.checkCapability()
expect(res).toBe(false)
client.dispose()
})
it('should watchProject', async () => {
let client = new Watchman(null)
disposables.push(client)
let res = await client.watchProject(__dirname)
expect(res).toBe(true)
client.dispose()
})
it('should unsubscribe', async () => {
let client = new Watchman(null)
disposables.push(client)
await client.watchProject(cwd)
let fn = jest.fn()
let disposable = await client.subscribe(`${cwd}/*`, fn)
disposable.dispose()
client.dispose()
})
})
describe('Watchman#subscribe', () => {
it('should subscribe file change', async () => {
let client = new Watchman(null, helper.createNullChannel())
disposables.push(client)
await client.watchProject(cwd)
let fn = jest.fn()
let disposable = await client.subscribe(`${cwd}/*`, fn)
let changes: FileChangeItem[] = [createFileChange(`${cwd}/a`)]
sendSubscription(disposable.subscribe, cwd, changes)
await wait(30)
expect(fn).toBeCalled()
let call = fn.mock.calls[0][0]
disposable.dispose()
expect(call.root).toBe(cwd)
client.dispose()
})
it('should subscribe with relative_path', async () => {
let client = new Watchman(null, helper.createNullChannel())
watchResponse = { watch: cwd, relative_path: 'foo' }
await client.watchProject(cwd)
let fn = jest.fn()
let disposable = await client.subscribe(`${cwd}/*`, fn)
let changes: FileChangeItem[] = [createFileChange(`${cwd}/a`)]
sendSubscription(disposable.subscribe, cwd, changes)
await wait(30)
expect(fn).toBeCalled()
let call = fn.mock.calls[0][0]
disposable.dispose()
expect(call.root).toBe(path.join(cwd, 'foo'))
client.dispose()
})
it('should not subscribe invalid response', async () => {
let c = new Watchman(null, helper.createNullChannel())
disposables.push(c)
watchResponse = { watch: cwd, relative_path: 'foo' }
await c.watchProject(cwd)
let fn = jest.fn()
let disposable = await c.subscribe(`${cwd}/*`, fn)
let changes: FileChangeItem[] = [createFileChange(`${cwd}/a`)]
sendSubscription('uuid', cwd, changes)
await wait(10)
sendSubscription(disposable.subscribe, cwd, [])
await wait(10)
client.write(bser.dumpToBuffer({
subscription: disposable.subscribe,
root: cwd
}))
await wait(10)
expect(fn).toBeCalledTimes(0)
})
})
describe('Watchman#createClient', () => {
it('should not create client when capabilities not match', async () => {
capabilities = { relative_root: false }
let client = await Watchman.createClient(null, cwd)
expect(client).toBe(null)
})
it('should not create when watch failed', async () => {
watchResponse = {}
let client = await Watchman.createClient(null, cwd)
expect(client).toBe(null)
})
it('should create client', async () => {
let client = await Watchman.createClient(null, cwd)
disposables.push(client)
expect(client).toBeDefined()
})
it('should not create client for root', async () => {
let client = await Watchman.createClient(null, '/')
expect(client).toBeNull()
})
})
describe('isValidWatchRoot()', () => {
it('should check valid root', async () => {
expect(isValidWatchRoot('/')).toBe(false)
expect(isValidWatchRoot(os.homedir())).toBe(false)
expect(isValidWatchRoot('/tmp/a/b/c')).toBe(false)
expect(isValidWatchRoot(os.tmpdir())).toBe(false)
})
})
describe('fileSystemWatcher', () => {
function createWatcher(pattern: string, ignoreCreateEvents = false, ignoreChangeEvents = false, ignoreDeleteEvents = false): FileSystemWatcher {
let watcher = watcherManager.createFileSystemWatcher(
pattern,
ignoreCreateEvents,
ignoreChangeEvents,
ignoreDeleteEvents
)
disposables.push(watcher)
return watcher
}
beforeAll(async () => {
workspaceFolder.addWorkspaceFolder(cwd, true)
await watcherManager.waitClient(cwd)
})
it('should watch for file create', async () => {
let watcher = createWatcher('**/*', false, true, true)
let fn = jest.fn()
watcher.onDidCreate(fn)
await helper.wait(50)
let changes: FileChangeItem[] = [createFileChange(`a`)]
sendSubscription(watcher.subscribe, cwd, changes)
await helper.wait(50)
expect(fn).toBeCalled()
})
it('should watch for file delete', async () => {
let watcher = createWatcher('**/*', true, true, false)
let fn = jest.fn()
watcher.onDidDelete(fn)
await helper.wait(50)
let changes: FileChangeItem[] = [createFileChange(`a`, false, false)]
sendSubscription(watcher.subscribe, cwd, changes)
await helper.wait(50)
expect(fn).toBeCalled()
})
it('should watch for file change', async () => {
let watcher = createWatcher('**/*', false, false, false)
let fn = jest.fn()
watcher.onDidChange(fn)
await helper.wait(50)
let changes: FileChangeItem[] = [createFileChange(`a`, false, true)]
sendSubscription(watcher.subscribe, cwd, changes)
await helper.wait(50)
expect(fn).toBeCalled()
})
it('should watch for file rename', async () => {
let watcher = createWatcher('**/*', false, false, false)
let fn = jest.fn()
watcher.onDidRename(fn)
await helper.wait(50)
let changes: FileChangeItem[] = [
createFileChange(`a`, false, false),
createFileChange(`b`, true, true),
]
sendSubscription(watcher.subscribe, cwd, changes)
await helper.wait(50)
expect(fn).toBeCalled()
})
it('should not watch for events', async () => {
let watcher = createWatcher('**/*', true, true, true)
let called = false
let onChange = () => { called = true }
watcher.onDidCreate(onChange)
watcher.onDidChange(onChange)
watcher.onDidDelete(onChange)
await helper.wait(50)
let changes: FileChangeItem[] = [
createFileChange(`a`, false, false),
createFileChange(`b`, true, true),
createFileChange(`c`, false, true),
]
sendSubscription(watcher.subscribe, cwd, changes)
await helper.wait(50)
expect(called).toBe(false)
})
it('should watch for folder rename', async () => {
let watcher = createWatcher('**/*')
let newFiles: string[] = []
let count = 0
watcher.onDidRename(e => {
count++
newFiles.push(e.newUri.fsPath)
})
await helper.wait(50)
let changes: FileChangeItem[] = [
createFileChange(`a/1`, false, false),
createFileChange(`a/2`, false, false),
createFileChange(`b/1`, true, true),
createFileChange(`b/2`, true, true),
]
sendSubscription(watcher.subscribe, cwd, changes)
await helper.waitValue(() => {
return count
}, 2)
})
it('should watch for new folder', async () => {
let watcher = createWatcher('**/*')
expect(watcher).toBeDefined()
workspaceFolder.renameWorkspaceFolder(cwd, __dirname)
await helper.wait(50)
let uri: URI
watcher.onDidCreate(e => {
uri = e
})
await helper.wait(50)
let changes: FileChangeItem[] = [createFileChange(`a`)]
sendSubscription(watcher.subscribe, __dirname, changes)
await helper.wait(50)
expect(uri.fsPath).toEqual(path.join(__dirname, 'a'))
})
})
describe('create FileSystemWatcherManager', () => {
it('should attach to existing workspace folder', async () => {
let workspaceFolder = new WorkspaceFolderController(configurations)
workspaceFolder.addWorkspaceFolder(cwd, false)
let watcherManager = new FileSystemWatcherManager(workspaceFolder, '')
watcherManager.attach(helper.createNullChannel())
await helper.wait(100)
await watcherManager.createClient(os.tmpdir())
await watcherManager.createClient(cwd)
await watcherManager.waitClient(cwd)
watcherManager.dispose()
})
})

View file

@ -0,0 +1,813 @@
import { Buffer, Neovim } from '@chemzqm/neovim'
import fs from 'fs-extra'
import os from 'os'
import path from 'path'
import { v4 as uuid } from 'uuid'
import { CancellationTokenSource, Disposable } from 'vscode-languageserver-protocol'
import { CreateFile, DeleteFile, Position, Range, RenameFile, TextDocumentEdit, TextEdit, VersionedTextDocumentIdentifier, WorkspaceEdit } from 'vscode-languageserver-types'
import { URI } from 'vscode-uri'
import { RecoverFunc } from '../../model/editInspect'
import RelativePattern from '../../model/relativePattern'
import { disposeAll } from '../../util'
import { readFile } from '../../util/fs'
import window from '../../window'
import workspace from '../../workspace'
import events from '../../events'
import helper, { createTmpFile } from '../helper'
let nvim: Neovim
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
await helper.reset()
disposeAll(disposables)
disposables = []
})
describe('RelativePattern', () => {
function testThrow(fn: () => void) {
let err
try {
fn()
} catch (e) {
err = e
}
expect(err).toBeDefined()
}
it('should throw for invalid arguments', async () => {
testThrow(() => {
new RelativePattern('', undefined)
})
testThrow(() => {
new RelativePattern({ uri: undefined } as any, '')
})
})
it('should create relativePattern', async () => {
for (let base of [__filename, URI.file(__filename), { uri: URI.file(__dirname).toString(), name: 'test' }]) {
let p = new RelativePattern(base, '**/*')
expect(URI.isUri(p.baseUri)).toBe(true)
expect(p.toJSON()).toBeDefined()
}
})
})
describe('findFiles()', () => {
beforeEach(() => {
workspace.workspaceFolderControl.setWorkspaceFolders([__dirname])
})
it('should use glob pattern', async () => {
let res = await workspace.findFiles('**/*.ts')
expect(res.length).toBeGreaterThan(0)
})
it('should use relativePattern', async () => {
let relativePattern = new RelativePattern(URI.file(__dirname), '**/*.ts')
let res = await workspace.findFiles(relativePattern)
expect(res.length).toBeGreaterThan(0)
})
it('should respect exclude as glob pattern', async () => {
let arr = await workspace.findFiles('**/*.ts', 'files*')
let res = arr.find(o => path.relative(__dirname, o.fsPath).startsWith('files'))
expect(res).toBeUndefined()
})
it('should respect exclude as relativePattern', async () => {
let relativePattern = new RelativePattern(URI.file(__dirname), 'files*')
let arr = await workspace.findFiles('**/*.ts', relativePattern)
let res = arr.find(o => path.relative(__dirname, o.fsPath).startsWith('files'))
expect(res).toBeUndefined()
})
it('should respect maxResults', async () => {
let arr = await workspace.findFiles('**/*.ts', undefined, 1)
expect(arr.length).toBe(1)
})
it('should respect token', async () => {
let source = new CancellationTokenSource()
source.cancel()
let arr = await workspace.findFiles('**/*.ts', undefined, 1, source.token)
expect(arr.length).toBe(0)
})
it('should cancel findFiles', async () => {
let source = new CancellationTokenSource()
let p = workspace.findFiles('**/*.ts', undefined, 1, source.token)
source.cancel()
let arr = await p
expect(arr.length).toBe(0)
})
})
describe('applyEdits()', () => {
it('should not throw when unable to undo & redo', async () => {
await workspace.files.undoWorkspaceEdit()
await workspace.files.redoWorkspaceEdit()
})
it('should apply TextEdit of documentChanges', async () => {
let doc = await helper.createDocument()
let versioned = VersionedTextDocumentIdentifier.create(doc.uri, doc.version)
let edit = TextEdit.insert(Position.create(0, 0), 'bar')
let change = TextDocumentEdit.create(versioned, [edit])
let workspaceEdit: WorkspaceEdit = {
documentChanges: [change]
}
let res = await workspace.applyEdit(workspaceEdit)
expect(res).toBe(true)
let line = await nvim.getLine()
expect(line).toBe('bar')
})
it('should apply edit with out change buffers', async () => {
let doc = await helper.createDocument()
await nvim.setLine('bar')
await doc.synchronize()
let version = doc.version
let versioned = VersionedTextDocumentIdentifier.create(doc.uri, doc.version)
let edit = TextEdit.replace(Range.create(0, 0, 0, 3), 'bar')
let change = TextDocumentEdit.create(versioned, [edit])
let workspaceEdit: WorkspaceEdit = {
documentChanges: [change]
}
let res = await workspace.applyEdit(workspaceEdit)
expect(res).toBe(true)
expect(doc.version).toBe(version)
})
it('should not apply TextEdit if version miss match', async () => {
let doc = await helper.createDocument()
let versioned = VersionedTextDocumentIdentifier.create(doc.uri, 10)
let edit = TextEdit.insert(Position.create(0, 0), 'bar')
let change = TextDocumentEdit.create(versioned, [edit])
let workspaceEdit: WorkspaceEdit = {
documentChanges: [change]
}
let res = await workspace.applyEdit(workspaceEdit)
expect(res).toBe(false)
})
it('should apply edits with changes to buffer', async () => {
let doc = await helper.createDocument()
let changes = {
[doc.uri]: [TextEdit.insert(Position.create(0, 0), 'bar')]
}
let workspaceEdit: WorkspaceEdit = { changes }
let res = await workspace.applyEdit(workspaceEdit)
expect(res).toBe(true)
let line = await nvim.getLine()
expect(line).toBe('bar')
})
it('should apply edits with changes to file not in buffer list', async () => {
let filepath = await createTmpFile('bar')
let uri = URI.file(filepath).toString()
let changes = {
[uri]: [TextEdit.insert(Position.create(0, 0), 'foo')]
}
let res = await workspace.applyEdit({ changes })
expect(res).toBe(true)
let doc = workspace.getDocument(uri)
let content = doc.getDocumentContent()
expect(content).toMatch(/^foobar/)
await nvim.command('silent! %bwipeout!')
})
it('should apply edits when file does not exist', async () => {
let filepath = path.join(__dirname, 'not_exists')
disposables.push({
dispose: () => {
if (fs.existsSync(filepath)) {
fs.unlinkSync(filepath)
}
}
})
let uri = URI.file(filepath).toString()
let changes = {
[uri]: [TextEdit.insert(Position.create(0, 0), 'foo')]
}
let res = await workspace.applyEdit({ changes })
expect(res).toBe(true)
})
it('should adjust cursor position after applyEdits', async () => {
let doc = await helper.createDocument()
let pos = await window.getCursorPosition()
expect(pos).toEqual({ line: 0, character: 0 })
let edit = TextEdit.insert(Position.create(0, 0), 'foo\n')
let versioned = VersionedTextDocumentIdentifier.create(doc.uri, null)
let documentChanges = [TextDocumentEdit.create(versioned, [edit])]
let res = await workspace.applyEdit({ documentChanges })
expect(res).toBe(true)
pos = await window.getCursorPosition()
expect(pos).toEqual({ line: 1, character: 0 })
})
it('should support null version of documentChanges', async () => {
let file = path.join(__dirname, 'foo')
await workspace.createFile(file, { ignoreIfExists: true, overwrite: true })
let uri = URI.file(file).toString()
let versioned = VersionedTextDocumentIdentifier.create(uri, null)
let edit = TextEdit.insert(Position.create(0, 0), 'bar')
let change = TextDocumentEdit.create(versioned, [edit])
let workspaceEdit: WorkspaceEdit = {
documentChanges: [change]
}
let res = await workspace.applyEdit(workspaceEdit)
expect(res).toBe(true)
await nvim.command('wa')
let content = await readFile(file, 'utf8')
expect(content).toMatch(/^bar/)
await workspace.deleteFile(file, { ignoreIfNotExists: true })
})
it('should support CreateFile edit', async () => {
let file = path.join(__dirname, 'foo')
let uri = URI.file(file).toString()
let workspaceEdit: WorkspaceEdit = {
documentChanges: [CreateFile.create(uri, { overwrite: true })]
}
let res = await workspace.applyEdit(workspaceEdit)
expect(res).toBe(true)
await workspace.deleteFile(file, { ignoreIfNotExists: true })
})
it('should support DeleteFile edit', async () => {
let file = path.join(__dirname, 'foo')
await workspace.createFile(file, { ignoreIfExists: true, overwrite: true })
let uri = URI.file(file).toString()
let workspaceEdit: WorkspaceEdit = {
documentChanges: [DeleteFile.create(uri)]
}
let res = await workspace.applyEdit(workspaceEdit)
expect(res).toBe(true)
})
it('should check uri for CreateFile edit', async () => {
let workspaceEdit: WorkspaceEdit = {
documentChanges: [CreateFile.create('term://.', { overwrite: true })]
}
let res = await workspace.applyEdit(workspaceEdit)
expect(res).toBe(false)
})
it('should support RenameFile edit', async () => {
let file = path.join(__dirname, 'foo')
await workspace.createFile(file, { ignoreIfExists: true, overwrite: true })
let newFile = path.join(__dirname, 'bar')
let uri = URI.file(file).toString()
let workspaceEdit: WorkspaceEdit = {
documentChanges: [RenameFile.create(uri, URI.file(newFile).toString())]
}
let res = await workspace.applyEdit(workspaceEdit)
expect(res).toBe(true)
await workspace.deleteFile(newFile, { ignoreIfNotExists: true })
})
it('should support changes with edit and rename', async () => {
let fsPath = await createTmpFile('test')
let doc = await helper.createDocument(fsPath)
let newFile = path.join(os.tmpdir(), `coc-${process.pid}/new-${uuid()}`)
let newUri = URI.file(newFile).toString()
let edit: WorkspaceEdit = {
documentChanges: [
{
textDocument: {
version: null,
uri: doc.uri,
},
edits: [
{
range: {
start: {
line: 0,
character: 0
},
end: {
line: 0,
character: 4
}
},
newText: 'bar'
}
]
},
{
oldUri: doc.uri,
newUri,
kind: 'rename'
}
]
}
let res = await workspace.applyEdit(edit)
expect(res).toBe(true)
let curr = await workspace.document
expect(curr.uri).toBe(newUri)
expect(curr.getline(0)).toBe('bar')
let line = await nvim.line
expect(line).toBe('bar')
})
it('should support edit new file with CreateFile', async () => {
let file = path.join(os.tmpdir(), 'foo')
let uri = URI.file(file).toString()
let workspaceEdit: WorkspaceEdit = {
documentChanges: [
CreateFile.create(uri, { overwrite: true }),
TextDocumentEdit.create({ uri, version: 0 }, [
TextEdit.insert(Position.create(0, 0), 'foo bar')
])
]
}
let res = await workspace.applyEdit(workspaceEdit)
expect(res).toBe(true)
let doc = workspace.getDocument(uri)
expect(doc).toBeDefined()
let line = doc.getline(0)
expect(line).toBe('foo bar')
await workspace.deleteFile(file, { ignoreIfNotExists: true })
})
it('should undo and redo workspace edit', async () => {
const folder = path.join(os.tmpdir(), uuid())
const pathone = path.join(folder, 'a')
const pathtwo = path.join(folder, 'b')
await workspace.files.createFile(pathone, { overwrite: true })
await workspace.files.createFile(pathtwo, { overwrite: true })
let uris = [URI.file(pathone).toString(), URI.file(pathtwo).toString()]
const assertContent = (one: string, two: string) => {
let doc = workspace.getDocument(uris[0])
expect(doc.getDocumentContent()).toBe(one)
doc = workspace.getDocument(uris[1])
expect(doc.getDocumentContent()).toBe(two)
}
let edits: TextDocumentEdit[] = []
edits.push(TextDocumentEdit.create({ uri: uris[0], version: null }, [
TextEdit.insert(Position.create(0, 0), 'foo')
]))
edits.push(TextDocumentEdit.create({ uri: uris[1], version: null }, [
TextEdit.insert(Position.create(0, 0), 'bar')
]))
await workspace.applyEdit({ documentChanges: edits })
assertContent('foo\n', 'bar\n')
await workspace.files.undoWorkspaceEdit()
assertContent('\n', '\n')
await workspace.files.redoWorkspaceEdit()
assertContent('foo\n', 'bar\n')
})
it('should should support annotations', async () => {
async function assertEdit(confirm: boolean): Promise<void> {
let doc = await helper.createDocument(uuid())
let edit: WorkspaceEdit = {
documentChanges: [
{
textDocument: { version: doc.version, uri: doc.uri },
edits: [
{
range: Range.create(0, 0, 0, 0),
newText: 'bar',
annotationId: '85bc78e2-5ef0-4949-b10c-13f476faf430'
}
]
},
],
changeAnnotations: {
'85bc78e2-5ef0-4949-b10c-13f476faf430': {
needsConfirmation: true,
label: 'Text changes',
description: 'description'
}
}
}
let p = workspace.files.applyEdit(edit)
await helper.waitPrompt()
if (confirm) {
await nvim.input('<cr>')
} else {
await nvim.input('<esc>')
}
await p
let content = doc.getDocumentContent()
if (confirm) {
expect(content).toBe('bar\n')
} else {
expect(content).toBe('\n')
}
}
await assertEdit(true)
await assertEdit(false)
})
})
describe('inspectEdit', () => {
async function inspect(edit: WorkspaceEdit): Promise<Buffer> {
await workspace.applyEdit(edit)
await workspace.files.inspectEdit()
let buf = await nvim.buffer
return buf
}
it('should show wanring when edit not exists', async () => {
(workspace.files as any).editState = undefined
await workspace.files.inspectEdit()
})
it('should render with changes', async () => {
let fsPath = await createTmpFile('foo\n1\n2\nbar')
let doc = await helper.createDocument(fsPath)
let newFile = path.join(os.tmpdir(), `coc-${process.pid}/new-${uuid()}`)
let newUri = URI.file(newFile).toString()
let createFile = path.join(os.tmpdir(), `coc-${process.pid}/create-${uuid()}`)
let deleteFile = await createTmpFile('delete')
disposables.push(Disposable.create(() => {
if (fs.existsSync(newFile)) fs.unlinkSync(newFile)
if (fs.existsSync(createFile)) fs.unlinkSync(createFile)
if (fs.existsSync(deleteFile)) fs.unlinkSync(deleteFile)
}))
let edit: WorkspaceEdit = {
documentChanges: [
{
textDocument: { version: null, uri: doc.uri, },
edits: [
TextEdit.del(Range.create(0, 0, 1, 0)),
TextEdit.replace(Range.create(3, 0, 3, 3), 'xyz'),
]
},
{
kind: 'rename',
oldUri: doc.uri,
newUri
}, {
kind: 'create',
uri: URI.file(createFile).toString()
}, {
kind: 'delete',
uri: URI.file(deleteFile).toString()
}
]
}
let buf = await inspect(edit)
let lines = await buf.lines
let content = lines.join('\n')
expect(content).toMatch('Change')
expect(content).toMatch('Rename')
expect(content).toMatch('Create')
expect(content).toMatch('Delete')
await nvim.command('exe 5')
await nvim.input('<CR>')
await helper.waitFor('expand', ['%:p'], newFile)
let line = await nvim.call('line', ['.'])
expect(line).toBe(3)
})
it('should render annotation label', async () => {
let doc = await helper.createDocument(uuid())
let edit: WorkspaceEdit = {
documentChanges: [
{
textDocument: { version: doc.version, uri: doc.uri },
edits: [
{
range: Range.create(0, 0, 0, 0),
newText: 'bar',
annotationId: 'dd866f37-a24c-4503-9c35-c139fb28e25b'
}
]
},
],
changeAnnotations: {
'dd866f37-a24c-4503-9c35-c139fb28e25b': {
needsConfirmation: false,
label: 'Text changes'
}
}
}
let buf = await inspect(edit)
await events.fire('BufUnload', [buf.id + 1])
let winid = await nvim.call('win_getid')
let lines = await buf.lines
expect(lines[0]).toBe('Text changes')
await nvim.command('exe 1')
await nvim.input('<CR>')
let bufnr = await nvim.call('bufnr', ['%'])
expect(bufnr).toBe(buf.id)
await nvim.command('exe 3')
await nvim.input('<CR>')
let fsPath = URI.parse(doc.uri).fsPath
await helper.waitFor('expand', ['%:p'], fsPath)
await nvim.call('win_gotoid', [winid])
await nvim.input('<esc>')
await helper.wait(10)
})
})
describe('createFile()', () => {
it('should create and revert parent folder', async () => {
const folder = path.join(os.tmpdir(), uuid())
const filepath = path.join(folder, 'bar')
disposables.push(Disposable.create(() => {
if (fs.existsSync(folder)) fs.removeSync(folder)
}))
let fns: RecoverFunc[] = []
expect(fs.existsSync(folder)).toBe(false)
await workspace.files.createFile(filepath, {}, fns)
expect(fs.existsSync(filepath)).toBe(true)
for (let i = fns.length - 1; i >= 0; i--) {
await fns[i]()
}
expect(fs.existsSync(folder)).toBe(false)
})
it('should throw when file already exists', async () => {
let filepath = await createTmpFile('foo', disposables)
let fn = async () => {
await workspace.createFile(filepath, {})
}
await expect(fn()).rejects.toThrow(Error)
})
it('should not create file if file exists with ignoreIfExists', async () => {
let file = await createTmpFile('foo')
await workspace.createFile(file, { ignoreIfExists: true })
let content = fs.readFileSync(file, 'utf8')
expect(content).toBe('foo')
})
it('should create file if does not exist', async () => {
await helper.edit()
let filepath = path.join(__dirname, 'foo')
await workspace.createFile(filepath, { ignoreIfExists: true })
let exists = fs.existsSync(filepath)
expect(exists).toBe(true)
fs.unlinkSync(filepath)
})
it('should revert file create', async () => {
let filepath = path.join(os.tmpdir(), uuid())
disposables.push(Disposable.create(() => {
if (fs.existsSync(filepath)) fs.unlinkSync(filepath)
}))
let fns: RecoverFunc[] = []
await workspace.files.createFile(filepath, { overwrite: true }, fns)
expect(fs.existsSync(filepath)).toBe(true)
let bufnr = await nvim.call('bufnr', [filepath])
expect(bufnr).toBeGreaterThan(0)
let doc = workspace.getDocument(bufnr)
expect(doc).toBeDefined()
for (let fn of fns) {
await fn()
}
expect(fs.existsSync(filepath)).toBe(false)
let loaded = await nvim.call('bufloaded', [filepath])
expect(loaded).toBe(0)
})
})
describe('renameFile', () => {
it('should throw when oldPath not exists', async () => {
let filepath = path.join(__dirname, 'not_exists_file')
let newPath = path.join(__dirname, 'bar')
let fn = async () => {
await workspace.renameFile(filepath, newPath)
}
await expect(fn()).rejects.toThrow(Error)
})
it('should rename file on disk', async () => {
let filepath = await createTmpFile('test')
let newPath = path.join(path.dirname(filepath), 'new_file')
disposables.push(Disposable.create(() => {
if (fs.existsSync(newPath)) fs.unlinkSync(newPath)
if (fs.existsSync(filepath)) fs.unlinkSync(filepath)
}))
let fns: RecoverFunc[] = []
await workspace.files.renameFile(filepath, newPath, { overwrite: true }, fns)
expect(fs.existsSync(newPath)).toBe(true)
for (let fn of fns) {
await fn()
}
expect(fs.existsSync(newPath)).toBe(false)
expect(fs.existsSync(filepath)).toBe(true)
})
it('should rename if file does not exist', async () => {
let filepath = path.join(__dirname, 'foo')
let newPath = path.join(__dirname, 'bar')
await workspace.createFile(filepath)
await workspace.renameFile(filepath, newPath)
expect(fs.existsSync(newPath)).toBe(true)
expect(fs.existsSync(filepath)).toBe(false)
fs.unlinkSync(newPath)
})
it('should rename current buffer with same bufnr', async () => {
let file = await createTmpFile('test')
let doc = await helper.createDocument(file)
await nvim.setLine('bar')
await doc.patchChange()
let newFile = path.join(os.tmpdir(), `coc-${process.pid}/new-${uuid()}`)
disposables.push(Disposable.create(() => {
if (fs.existsSync(newFile)) fs.unlinkSync(newFile)
}))
await workspace.renameFile(file, newFile)
let bufnr = await nvim.call('bufnr', ['%'])
expect(bufnr).toBe(doc.bufnr)
let line = await nvim.line
expect(line).toBe('bar')
let exists = fs.existsSync(newFile)
expect(exists).toBe(true)
})
it('should overwrite if file exists', async () => {
let filepath = path.join(os.tmpdir(), uuid())
let newPath = path.join(os.tmpdir(), uuid())
await workspace.createFile(filepath)
await workspace.createFile(newPath)
await workspace.renameFile(filepath, newPath, { overwrite: true })
expect(fs.existsSync(newPath)).toBe(true)
expect(fs.existsSync(filepath)).toBe(false)
fs.unlinkSync(newPath)
})
it('should rename buffer in directory and revert', async () => {
let folder = path.join(os.tmpdir(), uuid())
let newFolder = path.join(os.tmpdir(), uuid())
fs.mkdirSync(folder)
disposables.push(Disposable.create(() => {
if (fs.existsSync(folder)) fs.removeSync(folder)
if (fs.existsSync(newFolder)) fs.removeSync(newFolder)
}))
let filepath = path.join(folder, 'new_file')
await workspace.createFile(filepath)
let bufnr = await nvim.call('bufnr', [filepath])
expect(bufnr).toBeGreaterThan(0)
let fns: RecoverFunc[] = []
await workspace.files.renameFile(folder, newFolder, { overwrite: true }, fns)
bufnr = await nvim.call('bufnr', [path.join(newFolder, 'new_file')])
expect(bufnr).toBeGreaterThan(0)
for (let i = fns.length - 1; i >= 0; i--) {
await fns[i]()
}
bufnr = await nvim.call('bufnr', [filepath])
expect(bufnr).toBeGreaterThan(0)
})
})
describe('loadResource()', () => {
it('should load file as hidden buffer', async () => {
helper.updateConfiguration('workspace.openResourceCommand', '')
let filepath = await createTmpFile('foo')
let uri = URI.file(filepath).toString()
let doc = await workspace.files.loadResource(uri)
let bufnrs = await nvim.call('coc#window#bufnrs') as number[]
expect(bufnrs.indexOf(doc.bufnr)).toBe(-1)
})
})
describe('deleteFile()', () => {
it('should throw when file not exists', async () => {
let filepath = path.join(__dirname, 'not_exists')
let fn = async () => {
await workspace.deleteFile(filepath)
}
await expect(fn()).rejects.toThrow(Error)
})
it('should ignore when ignoreIfNotExists set', async () => {
let filepath = path.join(__dirname, 'not_exists')
let fns: RecoverFunc[] = []
await workspace.files.deleteFile(filepath, { ignoreIfNotExists: true }, fns)
expect(fns.length).toBe(0)
})
it('should unload loaded buffer', async () => {
let filepath = await createTmpFile('file to delete')
disposables.push(Disposable.create(() => {
if (fs.existsSync(filepath)) fs.unlinkSync(filepath)
}))
await workspace.files.loadResource(URI.file(filepath).toString())
let fns: RecoverFunc[] = []
await workspace.files.deleteFile(filepath, {}, fns)
let loaded = await nvim.call('bufloaded', [filepath])
expect(loaded).toBe(0)
for (let i = fns.length - 1; i >= 0; i--) {
await fns[i]()
}
expect(fs.existsSync(filepath)).toBe(true)
loaded = await nvim.call('bufloaded', [filepath])
expect(loaded).toBe(1)
})
it('should delete and recover folder', async () => {
let folder = path.join(os.tmpdir(), uuid())
disposables.push(Disposable.create(() => {
if (fs.existsSync(folder)) fs.rmdirSync(folder)
}))
fs.mkdirSync(folder)
expect(fs.existsSync(folder)).toBe(true)
let fns: RecoverFunc[] = []
await workspace.files.deleteFile(folder, {}, fns)
expect(fs.existsSync(folder)).toBe(false)
for (let i = fns.length - 1; i >= 0; i--) {
await fns[i]()
}
expect(fs.existsSync(folder)).toBe(true)
await workspace.files.deleteFile(folder, {})
})
it('should delete and recover folder recursive', async () => {
let folder = path.join(os.tmpdir(), uuid())
disposables.push(Disposable.create(() => {
if (fs.existsSync(folder)) fs.removeSync(folder)
}))
fs.mkdirSync(folder)
await fs.writeFile(path.join(folder, 'new_file'), '', 'utf8')
let fns: RecoverFunc[] = []
await workspace.files.deleteFile(folder, { recursive: true }, fns)
expect(fs.existsSync(folder)).toBe(false)
for (let i = fns.length - 1; i >= 0; i--) {
await fns[i]()
}
expect(fs.existsSync(folder)).toBe(true)
expect(fs.existsSync(path.join(folder, 'new_file'))).toBe(true)
await workspace.files.deleteFile(folder, { recursive: true })
})
it('should delete file if exists', async () => {
let filepath = path.join(__dirname, 'foo')
await workspace.createFile(filepath)
expect(fs.existsSync(filepath)).toBe(true)
await workspace.deleteFile(filepath)
expect(fs.existsSync(filepath)).toBe(false)
})
})
describe('loadFile()', () => {
it('should single loadFile', async () => {
let doc = await helper.createDocument()
let newFile = URI.file(path.join(__dirname, 'abc')).toString()
let document = await workspace.loadFile(newFile)
let bufnr = await nvim.call('bufnr', '%')
expect(document.uri.endsWith('abc')).toBe(true)
expect(bufnr).toBe(doc.bufnr)
})
})
describe('loadFiles', () => {
it('should loadFiles', async () => {
let files = ['a', 'b', 'c'].map(key => URI.file(path.join(__dirname, key)).toString())
let docs = await workspace.loadFiles(files)
let uris = docs.map(o => o.uri)
expect(uris).toEqual(files)
})
it('should load empty files array', async () => {
await workspace.loadFiles([])
})
})
describe('openTextDocument()', () => {
it('should open document already exists', async () => {
let doc = await helper.createDocument('a')
await nvim.command('enew')
await workspace.openTextDocument(URI.parse(doc.uri))
let curr = await workspace.document
expect(curr.uri).toBe(doc.uri)
})
it('should throw when file does not exist', async () => {
let err
try {
await workspace.openTextDocument('/a/b/c')
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
it('should open untitled document', async () => {
let doc = await workspace.openTextDocument(URI.parse(`untitled:///a/b.js`))
expect(doc.uri).toBe('file:///a/b.js')
})
it('should load file that exists', async () => {
let doc = await workspace.openTextDocument(URI.file(__filename))
expect(URI.parse(doc.uri).fsPath).toBe(__filename)
let curr = await workspace.document
expect(curr.uri).toBe(doc.uri)
})
})

View file

@ -0,0 +1,94 @@
import os from 'os'
import path from 'path'
import Configurations from '../../configuration/index'
import * as funcs from '../../core/funcs'
let configurations: Configurations
beforeAll(async () => {
let userConfigFile = path.join(process.env.COC_VIMCONFIG, 'coc-settings.json')
configurations = new Configurations(userConfigFile, {
$removeConfigurationOption: () => {},
$updateConfigurationOption: () => {}
})
})
describe('has()', () => {
it('should throw for invalid argument', async () => {
let env = {
isVim: true,
version: '8023956'
}
let err
try {
expect(funcs.has(env, '0.5.0')).toBe(true)
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
it('should detect version on vim8', async () => {
let env = {
isVim: true,
version: '8023956'
}
expect(funcs.has(env, 'patch-7.4.248')).toBe(true)
expect(funcs.has(env, 'patch-8.5.1')).toBe(false)
})
it('should delete version on neovim', async () => {
let env = {
isVim: false,
version: '0.6.1'
}
expect(funcs.has(env, 'nvim-0.5.0')).toBe(true)
expect(funcs.has(env, 'nvim-0.7.0')).toBe(false)
})
})
describe('createNameSpace()', () => {
it('should create namespace', async () => {
let nr = funcs.createNameSpace('ns')
expect(nr).toBeDefined()
expect(nr).toBe(funcs.createNameSpace('ns'))
})
})
describe('getWatchmanPath()', () => {
it('should get watchman path', async () => {
let res = funcs.getWatchmanPath(configurations)
expect(typeof res === 'string' || res == null).toBe(true)
})
})
describe('findUp()', () => {
it('should return null when can not find ', async () => {
let nvim: any = {
call: () => {
return __filename
}
}
let res = await funcs.findUp(nvim, os.homedir(), ['file_not_exists'])
expect(res).toBeNull()
})
it('should return null when unable find cwd in cwd', async () => {
let nvim: any = {
call: () => {
return ''
}
}
let res = await funcs.findUp(nvim, os.homedir(), ['file_not_exists'])
expect(res).toBeNull()
})
})
describe('score()', () => {
it('should return score', async () => {
expect(funcs.score(undefined, 'untitled:///1', '')).toBe(0)
expect(funcs.score({ scheme: '*' }, 'untitled:///1', '')).toBe(3)
expect(funcs.score('vim', 'untitled:///1', 'vim')).toBe(10)
expect(funcs.score('*', 'untitled:///1', '')).toBe(5)
expect(funcs.score('', 'untitled:///1', 'vim')).toBe(0)
})
})

View file

@ -0,0 +1,79 @@
import { Neovim } from '@chemzqm/neovim'
import workspace from '../../workspace'
import Keymaps from '../../core/keymaps'
import helper from '../helper'
import { Disposable } from 'vscode-languageserver-protocol'
import { disposeAll } from '../../util'
let nvim: Neovim
let keymaps: Keymaps
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
keymaps = workspace.keymaps
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
disposeAll(disposables)
await helper.reset()
})
describe('doKeymap()', () => {
it('should not throw when key not mapped', async () => {
await keymaps.doKeymap('<C-a>', '', '{C-a}')
})
})
describe('registerKeymap()', () => {
it('should throw for invalid key', async () => {
let err
try {
keymaps.registerKeymap(['i'], '', jest.fn())
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
it('should throw for duplicated key', async () => {
keymaps.registerKeymap(['i'], 'tmp', jest.fn())
let err
try {
keymaps.registerKeymap(['i'], 'tmp', jest.fn())
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
it('should register insert key mapping', async () => {
let fn = jest.fn()
disposables.push(keymaps.registerKeymap(['i'], 'test', fn))
await helper.wait(10)
let res = await nvim.call('execute', ['verbose imap <Plug>(coc-test)'])
expect(res).toMatch('coc#_insert_key')
})
})
describe('registerExprKeymap()', () => {
it('should visual key mapping', async () => {
await nvim.setLine('foo')
let called = false
let fn = () => {
called = true
return ''
}
disposables.push(keymaps.registerExprKeymap('x', 'x', fn, true))
await helper.wait(50)
await nvim.command('normal! viw')
await nvim.input('x<esc>')
await helper.wait(50)
expect(called).toBe(true)
})
})

View file

@ -0,0 +1,148 @@
import { Neovim } from '@chemzqm/neovim'
import os from 'os'
import path from 'path'
import { Location, Range } from 'vscode-languageserver-protocol'
import { URI } from 'vscode-uri'
import workspace from '../../workspace'
import helper from '../helper'
let nvim: Neovim
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
await nvim.command(`source ${path.join(process.cwd(), 'autoload/coc/ui.vim')}`)
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
await helper.reset()
})
function createLocations(): Location[] {
let uri = URI.file(__filename).toString()
return [Location.create(uri, Range.create(0, 0, 1, 0)), Location.create(uri, Range.create(2, 0, 3, 0))]
}
describe('showLocations()', () => {
it('should show location list by default', async () => {
let locations = createLocations()
await workspace.showLocations(locations)
await helper.waitFor('bufname', ['%'], 'list:///location')
})
it('should fire autocmd when location list disabled', async () => {
Object.assign(workspace.env, {
locationlist: false
})
await nvim.exec(`
function OnLocationsChange()
let g:called = 1
endfunction
autocmd User CocLocationsChange :call OnLocationsChange()`)
let locations = createLocations()
await workspace.showLocations(locations)
await helper.waitFor('eval', [`get(g:,'called',0)`], 1)
})
it('should show quickfix when quickfix enabled', async () => {
helper.updateConfiguration('coc.preferences.useQuickfixForLocations', true)
let locations = createLocations()
await workspace.showLocations(locations)
await helper.waitFor('eval', [`&buftype`], 'quickfix')
})
it('should use customized quickfix open command', async () => {
await nvim.setVar('coc_quickfix_open_command', 'copen 1')
helper.updateConfiguration('coc.preferences.useQuickfixForLocations', true)
let locations = createLocations()
await workspace.showLocations(locations)
await helper.waitFor('eval', [`&buftype`], 'quickfix')
let win = await nvim.window
let height = await win.height
expect(height).toBe(1)
})
})
describe('jumpTo()', () => {
it('should jumpTo position', async () => {
let uri = URI.file('/tmp/foo').toString()
await workspace.jumpTo(uri, { line: 1, character: 1 })
await nvim.command('setl buftype=nofile')
let buf = await nvim.buffer
let name = await buf.name
expect(name).toMatch('/foo')
await buf.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false })
await workspace.jumpTo(uri, { line: 1, character: 1 })
let pos = await nvim.call('getcurpos')
expect(pos.slice(1, 3)).toEqual([2, 2])
})
it('should jumpTo uri without normalize', async () => {
let uri = 'zipfile:///tmp/clojure-1.9.0.jar::clojure/core.clj'
await workspace.jumpTo(uri)
let buf = await nvim.buffer
let name = await buf.name
expect(name).toBe(uri)
})
it('should jump without position', async () => {
let uri = URI.file('/tmp/foo').toString()
await workspace.jumpTo(uri)
let buf = await nvim.buffer
let name = await buf.name
expect(name).toMatch('/foo')
})
it('should jumpTo custom uri scheme', async () => {
let uri = 'jdt://foo'
await workspace.jumpTo(uri, { line: 1, character: 1 })
let buf = await nvim.buffer
let name = await buf.name
expect(name).toBe(uri)
})
})
describe('openResource()', () => {
it('should open resource', async () => {
let uri = URI.file(path.join(os.tmpdir(), 'bar')).toString()
await workspace.openResource(uri)
let buf = await nvim.buffer
let name = await buf.name
expect(name).toMatch('bar')
})
it('should open none file uri', async () => {
workspace.registerTextDocumentContentProvider('jd', {
provideTextDocumentContent: () => 'jd'
})
let uri = 'jd://abc'
await workspace.openResource(uri)
let buf = await nvim.buffer
let name = await buf.name
expect(name).toBe('jd://abc')
})
it('should open opened buffer', async () => {
let buf = await helper.edit()
let doc = workspace.getDocument(buf.id)
await workspace.openResource(doc.uri)
await helper.wait(30)
let bufnr = await nvim.call('bufnr', '%')
expect(bufnr).toBe(buf.id)
})
it('should open url', async () => {
await helper.mockFunction('coc#ui#open_url', 0)
let buf = await helper.edit()
let uri = 'http://example.com'
await workspace.openResource(uri)
await helper.wait(30)
let bufnr = await nvim.call('bufnr', '%')
expect(bufnr).toBe(buf.id)
})
})

View file

@ -0,0 +1,136 @@
import { Neovim } from '@chemzqm/neovim'
import os from 'os'
import path from 'path'
import which from 'which'
import Terminals from '../../core/terminals'
import window from '../../window'
import helper from '../helper'
let nvim: Neovim
let terminals: Terminals
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
terminals = new Terminals()
})
afterEach(() => {
terminals.reset()
})
afterAll(async () => {
await helper.shutdown()
})
describe('create terminal', () => {
it('should use cleaned env', async () => {
let terminal = await terminals.createTerminal(nvim, {
name: 'test',
shellPath: which.sync('bash'),
strictEnv: true
})
await helper.wait(50)
terminal.sendText(`echo $NODE_ENV`, true)
await helper.wait(50)
let buf = nvim.createBuffer(terminal.bufnr)
let lines = await buf.lines
expect(lines.includes('test')).toBe(false)
})
it('should use custom shell command', async () => {
let terminal = await terminals.createTerminal(nvim, {
name: 'test',
shellPath: which.sync('bash')
})
let bufnr = terminal.bufnr
let bufname = await nvim.call('bufname', [bufnr]) as string
expect(bufname.includes('bash')).toBe(true)
})
it('should use custom cwd', async () => {
let basename = path.basename(os.tmpdir())
let terminal = await terminals.createTerminal(nvim, {
name: 'test',
cwd: os.tmpdir()
})
let bufnr = terminal.bufnr
let bufname = await nvim.call('bufname', [bufnr]) as string
expect(bufname.includes(basename)).toBe(true)
})
it('should have exit code', async () => {
let exitStatus
terminals.onDidCloseTerminal(terminal => {
exitStatus = terminal.exitStatus
})
let terminal = await terminals.createTerminal(nvim, {
name: 'test',
shellPath: which.sync('bash'),
strictEnv: true
})
await helper.wait(50)
terminal.sendText('exit', true)
await helper.waitFor('bufloaded', [terminal.bufnr], 0)
await helper.wait(50)
expect(exitStatus).toBeDefined()
expect(exitStatus.code).toBeDefined()
})
it('should not throw when show & hide disposed terminal', async () => {
let terminal = await terminals.createTerminal(nvim, {
name: 'test',
shellPath: which.sync('bash')
})
terminal.dispose()
await terminal.show()
await terminal.hide()
})
it('should show terminal on current window', async () => {
let terminal = await terminals.createTerminal(nvim, {
name: 'test',
shellPath: which.sync('bash')
})
let winid = await nvim.call('bufwinid', [terminal.bufnr])
expect(winid).toBeGreaterThan(0)
await nvim.call('win_gotoid', [winid])
await terminal.show()
})
it('should show terminal that shown', async () => {
let terminal = await terminals.createTerminal(nvim, {
name: 'test',
shellPath: which.sync('bash')
})
let res = await terminal.show(true)
expect(res).toBe(true)
expect(terminal.bufnr).toBeDefined()
let winid = await nvim.call('bufwinid', [terminal.bufnr])
let curr = await nvim.call('win_getid', [])
expect(winid != curr).toBe(true)
})
it('should show hidden terminal', async () => {
let terminal = await terminals.createTerminal(nvim, {
name: 'test',
shellPath: which.sync('bash')
})
await terminal.hide()
await helper.wait(30)
let res = await terminal.show()
expect(res).toBe(true)
})
it('should create terminal', async () => {
let terminal = await window.createTerminal({
name: 'test',
})
expect(terminal).toBeDefined()
expect(terminal.processId).toBeDefined()
expect(terminal.name).toBeDefined()
terminal.dispose()
await helper.wait(30)
expect(terminal.exitStatus).toEqual({ code: undefined })
})
})

View file

@ -0,0 +1,92 @@
import { Neovim } from '@chemzqm/neovim'
import { Position, Range } from 'vscode-languageserver-types'
import * as ui from '../../core/ui'
import helper from '../helper'
let nvim: Neovim
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
await helper.reset()
})
describe('getCursorPosition()', () => {
it('should get cursor position', async () => {
await nvim.call('cursor', [1, 1])
let res = await ui.getCursorPosition(nvim)
expect(res).toEqual({
line: 0,
character: 0
})
})
})
describe('moveTo()', () => {
it('should moveTo position', async () => {
await nvim.setLine('foo')
await ui.moveTo(nvim, Position.create(0, 1), true)
let res = await ui.getCursorPosition(nvim)
expect(res).toEqual({ line: 0, character: 1 })
})
})
describe('getCursorScreenPosition()', () => {
it('should get cursor screen position', async () => {
let res = await ui.getCursorScreenPosition(nvim)
expect(res).toBeDefined()
expect(typeof res.row).toBe('number')
expect(typeof res.col).toBe('number')
})
})
describe('showMessage()', () => {
it('should showMessage on vim', async () => {
ui.showMessage(nvim, 'my message', 'MoreMsg', true)
await helper.wait(100)
let cmdline = await helper.getCmdline()
expect(cmdline).toMatch(/my message/)
})
})
describe('getSelection()', () => {
it('should return null when no selection exists', async () => {
let res = await ui.getSelection(nvim, 'v')
expect(res).toBeNull()
})
it('should return range for line selection', async () => {
await nvim.setLine('foo')
await nvim.input('V')
await nvim.input('<esc>')
let res = await ui.getSelection(nvim, 'V')
expect(res).toEqual({ start: { line: 0, character: 0 }, end: { line: 1, character: 0 } })
})
})
describe('selectRange()', () => {
it('should select range #1', async () => {
await nvim.call('setline', [1, ['foo', 'b']])
await nvim.command('set selection=inclusive')
await nvim.command('set virtualedit=onemore')
await ui.selectRange(nvim, Range.create(0, 0, 1, 1), true)
await nvim.input('<esc>')
let res = await ui.getSelection(nvim, 'v')
expect(res).toEqual(Range.create(0, 0, 1, 1))
})
it('should select range #2', async () => {
await nvim.call('setline', [1, ['foo', 'b']])
await ui.selectRange(nvim, Range.create(0, 0, 1, 0), true)
await nvim.input('<esc>')
let res = await ui.getSelection(nvim, 'v')
expect(res).toEqual(Range.create(0, 0, 0, 3))
})
})

View file

@ -0,0 +1,290 @@
import { Neovim } from '@chemzqm/neovim'
import fs from 'fs'
import os from 'os'
import path from 'path'
import { Disposable, WorkspaceFoldersChangeEvent } from 'vscode-languageserver-protocol'
import { URI } from 'vscode-uri'
import Configurations from '../../configuration/index'
import WorkspaceFolderController from '../../core/workspaceFolder'
import { PatternType } from '../../types'
import { disposeAll } from '../../util'
import workspace from '../../workspace'
import helper from '../helper'
let workspaceFolder: WorkspaceFolderController
let configurations: Configurations
let disposables: Disposable[] = []
let nvim: Neovim
function updateConfiguration(key: string, value: any, defaults: any): void {
configurations.updateUserConfig({ [key]: value })
disposables.push({
dispose: () => {
configurations.updateUserConfig({ [key]: defaults })
}
})
}
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
let userConfigFile = path.join(process.env.COC_VIMCONFIG, 'coc-settings.json')
configurations = new Configurations(userConfigFile, {
$removeConfigurationOption: () => {},
$updateConfigurationOption: () => {}
})
workspaceFolder = new WorkspaceFolderController(configurations)
})
afterEach(async () => {
await helper.reset()
disposeAll(disposables)
workspaceFolder.reset()
})
afterAll(async () => {
await helper.shutdown()
})
describe('WorkspaceFolderController', () => {
describe('asRelativePath()', () => {
function assertAsRelativePath(input: string, expected: string, includeWorkspace?: boolean) {
const actual = workspaceFolder.getRelativePath(input, includeWorkspace)
expect(actual).toBe(expected)
}
it('should get relative path', async () => {
workspaceFolder.addWorkspaceFolder(`/Coding/Applications/NewsWoWBot`, false)
assertAsRelativePath('/Coding/Applications/NewsWoWBot/bernd/das/brot', 'bernd/das/brot')
assertAsRelativePath('/Apps/DartPubCache/hosted/pub.dartlang.org/convert-2.0.1/lib/src/hex.dart',
'/Apps/DartPubCache/hosted/pub.dartlang.org/convert-2.0.1/lib/src/hex.dart')
assertAsRelativePath('', '')
assertAsRelativePath('/foo/bar', '/foo/bar')
assertAsRelativePath('in/out', 'in/out')
})
it('should asRelativePath, same paths, #11402', async () => {
const root = '/home/aeschli/workspaces/samples/docker'
const input = '/home/aeschli/workspaces/samples/docker'
workspaceFolder.addWorkspaceFolder(root, false)
assertAsRelativePath(input, input)
const input2 = '/home/aeschli/workspaces/samples/docker/a.file'
assertAsRelativePath(input2, 'a.file')
})
it('should asRelativePath, not workspaceFolder', async () => {
expect(workspace.getRelativePath('')).toBe('')
assertAsRelativePath('/foo/bar', '/foo/bar')
})
it('should asRelativePath, multiple folders', () => {
workspaceFolder.addWorkspaceFolder(`/Coding/One`, false)
workspaceFolder.addWorkspaceFolder(`/Coding/Two`, false)
assertAsRelativePath('/Coding/One/file.txt', 'One/file.txt')
assertAsRelativePath('/Coding/Two/files/out.txt', 'Two/files/out.txt')
assertAsRelativePath('/Coding/Two2/files/out.txt', '/Coding/Two2/files/out.txt')
})
it('should slightly inconsistent behaviour of asRelativePath and getWorkspaceFolder, #31553', async () => {
workspaceFolder.addWorkspaceFolder(`/Coding/One`, false)
workspaceFolder.addWorkspaceFolder(`/Coding/Two`, false)
assertAsRelativePath('/Coding/One/file.txt', 'One/file.txt')
assertAsRelativePath('/Coding/One/file.txt', 'One/file.txt', true)
assertAsRelativePath('/Coding/One/file.txt', 'file.txt', false)
assertAsRelativePath('/Coding/Two/files/out.txt', 'Two/files/out.txt')
assertAsRelativePath('/Coding/Two/files/out.txt', 'Two/files/out.txt', true)
assertAsRelativePath('/Coding/Two/files/out.txt', 'files/out.txt', false)
assertAsRelativePath('/Coding/Two2/files/out.txt', '/Coding/Two2/files/out.txt')
assertAsRelativePath('/Coding/Two2/files/out.txt', '/Coding/Two2/files/out.txt', true)
assertAsRelativePath('/Coding/Two2/files/out.txt', '/Coding/Two2/files/out.txt', false)
})
})
describe('setWorkspaceFolders()', () => {
it('should set valid folders', async () => {
workspaceFolder.setWorkspaceFolders([os.tmpdir(), '/a/not_exists'])
let folders = workspaceFolder.workspaceFolders
expect(folders.length).toBe(2)
})
})
describe('getWorkspaceFolder()', () => {
it('should get workspaceFolder by uri', async () => {
let res = workspaceFolder.getWorkspaceFolder(URI.parse('untitled://1'))
expect(res).toBeUndefined()
res = workspaceFolder.getWorkspaceFolder(URI.file('/a/b'))
expect(res).toBeUndefined()
let filepath = path.join(process.cwd(), 'a/b')
workspaceFolder.setWorkspaceFolders([process.cwd()])
res = workspaceFolder.getWorkspaceFolder(URI.file(filepath))
expect(URI.parse(res.uri).fsPath).toBe(process.cwd())
})
})
describe('getRootPatterns()', () => {
it('should get patterns from b:coc_root_patterns', async () => {
await nvim.command('edit t.vim | let b:coc_root_patterns=["foo"]')
await nvim.command('setf vim')
let doc = await workspace.document
let res = workspaceFolder.getRootPatterns(doc, PatternType.Buffer)
expect(res).toEqual(['foo'])
})
it('should get patterns from languageserver', async () => {
updateConfiguration('languageserver', {
test: {
filetypes: ['vim'],
rootPatterns: ['bar']
}
}, {})
workspaceFolder.addRootPattern('vim', ['foo'])
await nvim.command('edit t.vim')
await nvim.command('setf vim')
let doc = await workspace.document
let res = workspaceFolder.getRootPatterns(doc, PatternType.LanguageServer)
expect(res).toEqual(['bar', 'foo'])
})
it('should get patterns from user configuration', async () => {
let doc = await workspace.document
let res = workspaceFolder.getRootPatterns(doc, PatternType.Global)
expect(res.includes('.git')).toBe(true)
})
})
describe('resolveRoot()', () => {
const cwd = process.cwd()
const expand = (input: string) => {
return workspace.expand(input)
}
it('should resolve to cwd for file in cwd', async () => {
updateConfiguration('coc.preferences.rootPatterns', [], ['.git', '.hg', '.projections.json'])
let file = path.join(os.tmpdir(), 'foo')
await nvim.command(`edit ${file}`)
let doc = await workspace.document
let res = workspaceFolder.resolveRoot(doc, os.tmpdir(), false, expand)
expect(res).toBe(os.tmpdir())
})
it('should not fallback to cwd as workspace folder', async () => {
updateConfiguration('coc.preferences.rootPatterns', [], ['.git', '.hg', '.projections.json'])
updateConfiguration('workspace.workspaceFolderFallbackCwd', false, true)
let file = path.join(os.tmpdir(), 'foo')
await nvim.command(`edit ${file}`)
let doc = await workspace.document
let res = workspaceFolder.resolveRoot(doc, os.tmpdir(), false, expand)
expect(res).toBe(null)
})
it('should return null for untitled buffer', async () => {
await nvim.command('enew')
let doc = await workspace.document
let res = workspaceFolder.resolveRoot(doc, cwd, false, expand)
expect(res).toBe(null)
})
it('should respect ignored filetypes', async () => {
updateConfiguration('workspace.ignoredFiletypes', ['vim'], [])
await nvim.command('edit t.vim')
await nvim.command('setf vim')
let doc = await workspace.document
let res = workspaceFolder.resolveRoot(doc, cwd, false, expand)
expect(res).toBe(null)
})
it('should respect workspaceFolderCheckCwd', async () => {
let called = 0
disposables.push(workspaceFolder.onDidChangeWorkspaceFolders(() => {
called++
}))
workspaceFolder.addRootPattern('vim', ['.vim'])
await nvim.command('edit a/.vim/t.vim')
await nvim.command('setf vim')
let doc = await workspace.document
let res = workspaceFolder.resolveRoot(doc, cwd, true, expand)
expect(res).toBe(process.cwd())
await nvim.command('edit a/foo')
doc = await workspace.document
res = workspaceFolder.resolveRoot(doc, cwd, true, expand)
expect(res).toBe(process.cwd())
expect(called).toBe(1)
})
it('should respect ignored folders', async () => {
updateConfiguration('workspace.ignoredFolders', ['$HOME/foo', '$HOME'], [])
let file = path.join(os.homedir(), '.vim/bar')
workspaceFolder.addRootPattern('vim', ['.vim'])
await nvim.command(`edit ${file}`)
await nvim.command('setf vim')
let doc = await workspace.document
let res = workspaceFolder.resolveRoot(doc, path.join(os.homedir(), 'foo'), true, expand)
expect(res).toBe(null)
})
describe('bottomUpFileTypes', () => {
it('should respect specific filetype', async () => {
updateConfiguration('coc.preferences.rootPatterns', ['.vim'], ['.git', '.hg', '.projections.json'])
updateConfiguration('workspace.bottomUpFiletypes', ['vim'], [])
let root = path.join(os.tmpdir(), 'a')
let dir = path.join(root, '.vim')
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
let file = path.join(dir, 'foo')
await nvim.command(`edit ${file}`)
await nvim.command('setf vim')
let doc = await workspace.document
let res = workspaceFolder.resolveRoot(doc, file, true, expand)
expect(res).toBe(root)
})
it('should respect wildcard', async () => {
updateConfiguration('coc.preferences.rootPatterns', ['.vim'], ['.git', '.hg', '.projections.json'])
updateConfiguration('workspace.bottomUpFiletypes', ['*'], [])
let root = path.join(os.tmpdir(), 'a')
let dir = path.join(root, '.vim')
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
await helper.wait(30)
}
let file = path.join(dir, 'foo')
await nvim.command(`edit ${file}`)
let doc = await workspace.document
let res = workspaceFolder.resolveRoot(doc, file, true, expand)
expect(res).toBe(root)
})
})
})
describe('renameWorkspaceFolder()', () => {
it('should rename workspaceFolder', async () => {
let e: WorkspaceFoldersChangeEvent
disposables.push(workspaceFolder.onDidChangeWorkspaceFolders(ev => {
e = ev
}))
let cwd = process.cwd()
workspaceFolder.addWorkspaceFolder(cwd, false)
workspaceFolder.addWorkspaceFolder(cwd, false)
workspaceFolder.renameWorkspaceFolder(cwd, path.join(cwd, '.vim'))
expect(e.removed.length).toBe(1)
expect(e.added.length).toBe(1)
})
})
describe('removeWorkspaceFolder()', () => {
it('should remote workspaceFolder', async () => {
let e: WorkspaceFoldersChangeEvent
disposables.push(workspaceFolder.onDidChangeWorkspaceFolders(ev => {
e = ev
}))
let cwd = process.cwd()
workspaceFolder.addWorkspaceFolder(cwd, false)
workspaceFolder.removeWorkspaceFolder(cwd)
workspaceFolder.removeWorkspaceFolder('/a/b')
expect(e.removed.length).toBe(1)
expect(e.added.length).toBe(0)
})
})
})

View file

@ -0,0 +1,7 @@
exports.activate = async context => {
return {
getContext: () => {
return context
}
}
}

View file

@ -0,0 +1,7 @@
{
"name": "global",
"version": "1.0.0",
"engines": {
"coc": "^0.0.46"
}
}

View file

@ -0,0 +1,6 @@
{
"dependencies": {
"global": ">=1.0.0",
"test": ">=1.0.0"
}
}

View file

@ -0,0 +1,7 @@
exports.activate = context => {
return {
root: () => {
return context.extensionPath
}
}
}

View file

@ -0,0 +1,13 @@
exports.activate = async context => {
return {
asAbsolutePath: p => {
return context.asAbsolutePath(p)
},
getContext: () => {
return context
},
echo: x => {
return x
}
}
}

View file

@ -0,0 +1,33 @@
{
"name": "test",
"version": "1.0.0",
"engines": {
"coc": "^0.0.46"
},
"contributes": {
"rootPatterns": [
{
"filetype": "javascript",
"patterns": [
"package.json",
"jsconfig.json"
]
}
],
"commands": [
{
"title": "Test",
"command": "test.run"
}
],
"configuration": {
"properties": {
"test.enable": {
"type": "boolean",
"default": true,
"description": "Enable test"
}
}
}
}
}

View file

@ -0,0 +1,7 @@
exports.activate = async context => {
return {
getContext: () => {
return context
}
}
}

View file

@ -0,0 +1,7 @@
{
"name": "local",
"version": "1.0.0",
"engines": {
"coc": "^0.0.46"
}
}

View file

@ -0,0 +1,390 @@
import { Neovim } from '@chemzqm/neovim'
import { Disposable, CallHierarchyItem, SymbolKind, Range, SymbolTag } from 'vscode-languageserver-protocol'
import CallHierarchyHandler from '../../handler/callHierarchy'
import languages from '../../languages'
import workspace from '../../workspace'
import { disposeAll } from '../../util'
import { URI } from 'vscode-uri'
import helper, { createTmpFile } from '../helper'
let nvim: Neovim
let callHierarchy: CallHierarchyHandler
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
callHierarchy = helper.plugin.getHandler().callHierarchy
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
disposeAll(disposables)
await helper.reset()
})
function createCallItem(name: string, kind: SymbolKind, uri: string, range: Range): CallHierarchyItem {
return {
name,
kind,
uri,
range,
selectionRange: range
}
}
describe('CallHierarchy', () => {
it('should throw for when provider does not exist', async () => {
let err
try {
await callHierarchy.getIncoming()
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
it('should get undefined when prepare failed', async () => {
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], {
prepareCallHierarchy() {
return undefined
},
provideCallHierarchyIncomingCalls() {
return []
},
provideCallHierarchyOutgoingCalls() {
return []
}
}))
let res = await callHierarchy.getOutgoing()
expect(res).toBeUndefined()
})
it('should get incoming & outgoing callHierarchy items', async () => {
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], {
prepareCallHierarchy() {
return createCallItem('foo', SymbolKind.Class, 'test:///foo', Range.create(0, 0, 0, 5))
},
provideCallHierarchyIncomingCalls() {
return [{
from: createCallItem('bar', SymbolKind.Class, 'test:///bar', Range.create(1, 0, 1, 5)),
fromRanges: [Range.create(0, 0, 0, 5)]
}]
},
provideCallHierarchyOutgoingCalls() {
return [{
to: createCallItem('bar', SymbolKind.Class, 'test:///bar', Range.create(1, 0, 1, 5)),
fromRanges: [Range.create(1, 0, 1, 5)]
}]
}
}))
let res = await callHierarchy.getIncoming()
expect(res.length).toBe(1)
expect(res[0].from.name).toBe('bar')
let outgoing = await callHierarchy.getOutgoing()
expect(outgoing.length).toBe(1)
res = await callHierarchy.getIncoming(outgoing[0].to)
expect(res.length).toBe(1)
})
it('should show message when provider does not exist', async () => {
await callHierarchy.showCallHierarchyTree('incoming')
let buf = await nvim.buffer
let lines = await buf.lines
expect(lines[0]).toMatch('callHierarchy provider not found')
await nvim.command('wincmd p')
})
it('should no results when no result returned.', async () => {
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], {
prepareCallHierarchy() {
return []
},
provideCallHierarchyIncomingCalls() {
return []
},
provideCallHierarchyOutgoingCalls() {
return []
}
}))
await callHierarchy.showCallHierarchyTree('incoming')
let buf = await nvim.buffer
let lines = await buf.lines
expect(lines[0]).toBe('No results')
await nvim.command('wincmd p')
})
it('should render description and support default action', async () => {
let doc = await workspace.document
let bufnr = doc.bufnr
await doc.buffer.setLines(['foo'], { start: 0, end: -1, strictIndexing: false })
let fsPath = await createTmpFile('foo\nbar\ncontent\n')
let uri = URI.file(fsPath).toString()
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], {
prepareCallHierarchy() {
return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3))
},
provideCallHierarchyIncomingCalls() {
let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(1, 0, 1, 3))
item.detail = 'Detail'
item.tags = [SymbolTag.Deprecated]
return [{
from: item,
fromRanges: [Range.create(2, 0, 2, 5)]
}]
},
provideCallHierarchyOutgoingCalls() {
return []
}
}))
await callHierarchy.showCallHierarchyTree('incoming')
let buf = await nvim.buffer
let lines = await buf.lines
expect(lines).toEqual([
'INCOMING CALLS',
'- c foo',
' + c bar Detail'
])
await nvim.command('exe 3')
await nvim.input('t')
await helper.waitFor('getline', ['.'], ' - c bar Detail')
await nvim.input('<cr>')
await helper.waitFor('expand', ['%:p'], fsPath)
let res = await nvim.call('coc#cursor#position')
expect(res).toEqual([1, 0])
let matches = await nvim.call('getmatches') as any[]
expect(matches.length).toBe(2)
await nvim.command(`b ${bufnr}`)
await helper.wait(50)
matches = await nvim.call('getmatches')
expect(matches.length).toBe(0)
await nvim.command(`wincmd o`)
await helper.wait(50)
})
it('should invoke open in new tab action', async () => {
let doc = await workspace.document
await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false })
let fsPath = await createTmpFile('foo\nbar\ncontent\n')
let uri = URI.file(fsPath).toString()
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], {
prepareCallHierarchy() {
return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3))
},
provideCallHierarchyIncomingCalls() {
return []
},
provideCallHierarchyOutgoingCalls() {
let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(0, 0, 0, 1))
item.detail = 'Detail'
return [{
to: item,
fromRanges: [Range.create(1, 0, 1, 3)]
}]
}
}))
let win = await nvim.window
let tab = await nvim.call('tabpagenr')
await callHierarchy.showCallHierarchyTree('outgoing')
let buf = await nvim.buffer
let lines = await buf.lines
expect(lines).toEqual([
'OUTGOING CALLS',
'- c foo',
' + c bar Detail'
])
await nvim.command('exe 3')
await nvim.input('<tab>')
await helper.wait(100)
await nvim.input('<cr>')
await helper.wait(200)
let newTab = await nvim.call('tabpagenr')
expect(newTab != tab).toBe(true)
doc = await workspace.document
expect(doc.uri).toBe(uri)
let res = await nvim.call('getmatches', [win.id])
expect(res.length).toBe(1)
})
it('should invoke show incoming calls action', async () => {
let doc = await workspace.document
await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false })
let fsPath = await createTmpFile('foo\nbar\ncontent\n')
let uri = URI.file(fsPath).toString()
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], {
prepareCallHierarchy() {
return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3))
},
provideCallHierarchyIncomingCalls() {
return [{
from: createCallItem('test', SymbolKind.Class, 'test:///bar', Range.create(1, 0, 1, 5)),
fromRanges: [Range.create(0, 0, 0, 5)]
}]
},
provideCallHierarchyOutgoingCalls() {
let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(0, 0, 0, 1))
item.detail = 'Detail'
return [{
to: item,
fromRanges: [Range.create(1, 0, 1, 3)]
}]
}
}))
await callHierarchy.showCallHierarchyTree('outgoing')
let buf = await nvim.buffer
let lines = await buf.lines
expect(lines).toEqual([
'OUTGOING CALLS',
'- c foo',
' + c bar Detail'
])
await nvim.command('exe 3')
await nvim.input('<tab>')
await helper.wait(50)
await nvim.input('2')
await helper.wait(200)
lines = await buf.lines
expect(lines).toEqual([
'INCOMING CALLS',
'- c bar Detail',
' + c test'
])
await nvim.command('bd!')
})
it('should invoke show outgoing calls action', async () => {
let doc = await workspace.document
await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false })
let fsPath = await createTmpFile('foo\nbar\ncontent\n')
let uri = URI.file(fsPath).toString()
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], {
prepareCallHierarchy() {
return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3))
},
provideCallHierarchyIncomingCalls() {
return [{
from: createCallItem('test', SymbolKind.Class, 'test:///bar', Range.create(1, 0, 1, 5)),
fromRanges: [Range.create(0, 0, 0, 5)]
}]
},
provideCallHierarchyOutgoingCalls() {
let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(0, 0, 0, 1))
item.detail = 'Detail'
return [{
to: item,
fromRanges: [Range.create(1, 0, 1, 3)]
}]
}
}))
await callHierarchy.showCallHierarchyTree('incoming')
let buf = await nvim.buffer
let lines = await buf.lines
expect(lines).toEqual([
'INCOMING CALLS',
'- c foo',
' + c test'
])
await nvim.command('exe 3')
await nvim.input('<tab>')
await helper.wait(50)
await nvim.input('3')
await helper.wait(200)
lines = await buf.lines
expect(lines).toEqual([
'OUTGOING CALLS',
'- c test',
' + c bar Detail'
])
await nvim.command('bd!')
})
it('should invoke dismiss action #1', async () => {
let doc = await workspace.document
await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false })
let fsPath = await createTmpFile('foo\nbar\ncontent\n')
let uri = URI.file(fsPath).toString()
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], {
prepareCallHierarchy() {
return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3))
},
provideCallHierarchyIncomingCalls() {
return []
},
provideCallHierarchyOutgoingCalls() {
let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(0, 0, 0, 1))
item.detail = 'Detail'
return [{
to: item,
fromRanges: [Range.create(1, 0, 1, 3)]
}]
}
}))
await callHierarchy.showCallHierarchyTree('outgoing')
let buf = await nvim.buffer
let lines = await buf.lines
expect(lines).toEqual([
'OUTGOING CALLS',
'- c foo',
' + c bar Detail'
])
await nvim.command('exe 3')
await nvim.input('<tab>')
await helper.wait(50)
await nvim.input('4')
await helper.wait(200)
lines = await buf.lines
expect(lines).toEqual([
'OUTGOING CALLS',
'- c foo'
])
await nvim.command('wincmd c')
})
it('should invoke dismiss action #2', async () => {
let doc = await workspace.document
await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false })
let fsPath = await createTmpFile('foo\nbar\ncontent\n')
let uri = URI.file(fsPath).toString()
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], {
prepareCallHierarchy() {
return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3))
},
provideCallHierarchyIncomingCalls() {
return []
},
provideCallHierarchyOutgoingCalls() {
let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(0, 0, 0, 1))
item.detail = 'Detail'
return [{
to: item,
fromRanges: [Range.create(1, 0, 1, 3)]
}]
}
}))
await callHierarchy.showCallHierarchyTree('outgoing')
let buf = await nvim.buffer
let lines = await buf.lines
expect(lines).toEqual([
'OUTGOING CALLS',
'- c foo',
' + c bar Detail'
])
await nvim.command('exe 3')
await nvim.input('t')
await helper.wait(50)
await nvim.command('exe 4')
await nvim.input('<tab>')
await helper.wait(50)
await nvim.input('4')
await helper.wait(200)
lines = await buf.lines
expect(lines).toEqual([
'OUTGOING CALLS',
'- c foo',
' - c bar Detail'
])
await nvim.command('wincmd c')
})
})

View file

@ -0,0 +1,414 @@
import { Neovim } from '@chemzqm/neovim'
import { CancellationToken, CodeAction, Command, CodeActionContext, CodeActionKind, TextEdit, Disposable, Range, Position } from 'vscode-languageserver-protocol'
import { TextDocument } from 'vscode-languageserver-textdocument'
import commands from '../../commands'
import ActionsHandler from '../../handler/codeActions'
import languages from '../../languages'
import { ProviderResult } from '../../provider'
import { disposeAll } from '../../util'
import { rangeInRange } from '../../util/position'
import helper from '../helper'
let nvim: Neovim
let disposables: Disposable[] = []
let codeActions: ActionsHandler
let currActions: CodeAction[]
let resolvedAction: CodeAction
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
codeActions = helper.plugin.getHandler().codeActions
})
afterAll(async () => {
await helper.shutdown()
})
beforeEach(async () => {
disposables.push(languages.registerCodeActionProvider([{ language: '*' }], {
provideCodeActions: (
_document: TextDocument,
_range: Range,
_context: CodeActionContext,
_token: CancellationToken
) => currActions,
resolveCodeAction: (
_action: CodeAction,
_token: CancellationToken
): ProviderResult<CodeAction> => resolvedAction
}, undefined))
})
afterEach(async () => {
disposeAll(disposables)
await helper.reset()
})
describe('handler codeActions', () => {
describe('organizeImport', () => {
it('should throw error when organize import action not found', async () => {
currActions = []
await helper.createDocument()
let err
try {
await codeActions.organizeImport()
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
it('should perform organize import action', async () => {
let doc = await helper.createDocument()
await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false })
let edits: TextEdit[] = []
edits.push(TextEdit.replace(Range.create(0, 0, 0, 3), 'bar'))
edits.push(TextEdit.replace(Range.create(1, 0, 1, 3), 'foo'))
let edit = { changes: { [doc.uri]: edits } }
let action = CodeAction.create('organize import', edit, CodeActionKind.SourceOrganizeImports)
currActions = [action, CodeAction.create('another action')]
await codeActions.organizeImport()
let lines = await doc.buffer.lines
expect(lines).toEqual(['bar', 'foo'])
})
it('should register editor.action.organizeImport command', async () => {
let doc = await helper.createDocument()
await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false })
let edits: TextEdit[] = []
edits.push(TextEdit.replace(Range.create(0, 0, 0, 3), 'bar'))
edits.push(TextEdit.replace(Range.create(1, 0, 1, 3), 'foo'))
let edit = { changes: { [doc.uri]: edits } }
let action = CodeAction.create('organize import', edit, CodeActionKind.SourceOrganizeImports)
currActions = [action, CodeAction.create('another action')]
await commands.executeCommand('editor.action.organizeImport')
let lines = await doc.buffer.lines
expect(lines).toEqual(['bar', 'foo'])
})
})
describe('codeActionRange', () => {
it('should show warning when no action available', async () => {
await helper.createDocument()
currActions = []
await codeActions.codeActionRange(1, 2, CodeActionKind.QuickFix)
let line = await helper.getCmdline()
expect(line).toMatch(/No quickfix code action/)
})
it('should apply chosen action', async () => {
let doc = await helper.createDocument()
let edits: TextEdit[] = []
edits.push(TextEdit.insert(Position.create(0, 0), 'bar'))
let edit = { changes: { [doc.uri]: edits } }
let action = CodeAction.create('code fix', edit, CodeActionKind.QuickFix)
currActions = [action]
let p = codeActions.codeActionRange(1, 2, CodeActionKind.QuickFix)
await helper.wait(100)
await nvim.input('<CR>')
await p
let buf = nvim.createBuffer(doc.bufnr)
let lines = await buf.lines
expect(lines[0]).toBe('bar')
})
})
describe('getCodeActions', () => {
it('should get empty actions', async () => {
currActions = []
let doc = await helper.createDocument()
let res = await codeActions.getCodeActions(doc)
expect(res.length).toBe(0)
})
it('should not filter disabled actions', async () => {
currActions = []
let action = CodeAction.create('foo', CodeActionKind.QuickFix)
action.disabled = { reason: 'disabled' }
currActions.push(action)
action = CodeAction.create('foo', CodeActionKind.QuickFix)
action.disabled = { reason: 'disabled' }
currActions.push(action)
let doc = await helper.createDocument()
let res = await codeActions.getCodeActions(doc)
expect(res.length).toBe(1)
})
it('should get all actions', async () => {
let doc = await helper.createDocument()
await doc.buffer.setLines(['', '', ''], { start: 0, end: -1, strictIndexing: false })
let action = CodeAction.create('curr action', CodeActionKind.Empty)
currActions = [action]
let range: Range
disposables.push(languages.registerCodeActionProvider([{ language: '*' }], {
provideCodeActions: (
_document: TextDocument,
r: Range,
_context: CodeActionContext, _token: CancellationToken
) => {
range = r
return [CodeAction.create('a'), CodeAction.create('b'), CodeAction.create('c')]
},
}, undefined))
let res = await codeActions.getCodeActions(doc)
expect(range).toEqual(Range.create(0, 0, 3, 0))
expect(res.length).toBe(4)
})
it('should filter actions by range', async () => {
let doc = await helper.createDocument()
await doc.buffer.setLines(['', '', ''], { start: 0, end: -1, strictIndexing: false })
currActions = []
let range: Range
disposables.push(languages.registerCodeActionProvider([{ language: '*' }], {
provideCodeActions: (
_document: TextDocument,
r: Range,
_context: CodeActionContext, _token: CancellationToken
) => {
range = r
if (rangeInRange(r, Range.create(0, 0, 1, 0))) return [CodeAction.create('a')]
return [CodeAction.create('a'), CodeAction.create('b'), CodeAction.create('c')]
},
}, undefined))
let res = await codeActions.getCodeActions(doc, Range.create(0, 0, 0, 0))
expect(range).toEqual(Range.create(0, 0, 0, 0))
expect(res.length).toBe(1)
})
it('should filter actions by kind prefix', async () => {
let doc = await helper.createDocument()
let action = CodeAction.create('my action', CodeActionKind.SourceFixAll)
currActions = [action]
let res = await codeActions.getCodeActions(doc, undefined, [CodeActionKind.Source])
expect(res.length).toBe(1)
expect(res[0].kind).toBe(CodeActionKind.SourceFixAll)
})
})
describe('getCurrentCodeActions', () => {
let range: Range
beforeEach(() => {
disposables.push(languages.registerCodeActionProvider([{ language: '*' }], {
provideCodeActions: (
_document: TextDocument,
r: Range,
_context: CodeActionContext, _token: CancellationToken
) => {
range = r
return [CodeAction.create('a'), CodeAction.create('b'), CodeAction.create('c')]
},
}, undefined))
})
it('should get codeActions by line', async () => {
currActions = []
await helper.createDocument()
let res = await codeActions.getCurrentCodeActions('line')
expect(range).toEqual(Range.create(0, 0, 1, 0))
expect(res.length).toBe(3)
})
it('should get codeActions by cursor', async () => {
currActions = []
await helper.createDocument()
let res = await codeActions.getCurrentCodeActions('cursor')
expect(range).toEqual(Range.create(0, 0, 0, 0))
expect(res.length).toBe(3)
})
it('should get codeActions by visual mode', async () => {
currActions = []
await helper.createDocument()
await nvim.setLine('foo')
await nvim.command('normal! 0v$')
await nvim.input('<esc>')
let res = await codeActions.getCurrentCodeActions('v')
expect(range).toEqual(Range.create(0, 0, 0, 3))
expect(res.length).toBe(3)
})
})
describe('doCodeAction', () => {
it('should not throw when no action exists', async () => {
currActions = []
await helper.createDocument()
let err
try {
await codeActions.doCodeAction(undefined)
} catch (e) {
err = e
}
expect(err).toBeUndefined()
})
it('should apply single code action when only is title', async () => {
let doc = await helper.createDocument()
let edits: TextEdit[] = []
edits.push(TextEdit.insert(Position.create(0, 0), 'bar'))
let edit = { changes: { [doc.uri]: edits } }
let action = CodeAction.create('code fix', edit, CodeActionKind.QuickFix)
currActions = [action]
await codeActions.doCodeAction(undefined, 'code fix')
let lines = await doc.buffer.lines
expect(lines).toEqual(['bar'])
})
it('should apply single code action when only is codeAction array', async () => {
let doc = await helper.createDocument()
let edits: TextEdit[] = []
edits.push(TextEdit.insert(Position.create(0, 0), 'bar'))
let edit = { changes: { [doc.uri]: edits } }
let action = CodeAction.create('code fix', edit, CodeActionKind.QuickFix)
currActions = [action]
await codeActions.doCodeAction(undefined, [CodeActionKind.QuickFix])
let lines = await doc.buffer.lines
expect(lines).toEqual(['bar'])
})
it('should show disabled code action', async () => {
let doc = await helper.createDocument()
let edits: TextEdit[] = []
edits.push(TextEdit.insert(Position.create(0, 0), 'bar'))
let edit = { changes: { [doc.uri]: edits } }
let refactorAction = CodeAction.create('code refactor', edit, CodeActionKind.Refactor)
refactorAction.disabled = { reason: 'invalid position' }
let fixAction = CodeAction.create('code fix', edit, CodeActionKind.QuickFix)
currActions = [refactorAction, fixAction]
let p = codeActions.doCodeAction(undefined)
let winid = await helper.waitFloat()
let win = nvim.createWindow(winid)
let buf = await win.buffer
let lines = await buf.lines
expect(lines.length).toBe(2)
expect(lines[1]).toMatch(/code refactor/)
await nvim.input('2')
await helper.wait(50)
await nvim.input('j')
await nvim.input('<cr>')
await helper.wait(50)
let valid = await win.valid
expect(valid).toBe(true)
let cmdline = await helper.getCmdline()
expect(cmdline).toMatch(/invalid position/)
await nvim.input('<esc>')
})
it('should action dialog to choose action', async () => {
let doc = await helper.createDocument()
let edits: TextEdit[] = []
edits.push(TextEdit.insert(Position.create(0, 0), 'bar'))
let edit = { changes: { [doc.uri]: edits } }
let action = CodeAction.create('code fix', edit, CodeActionKind.QuickFix)
currActions = [action, CodeAction.create('foo')]
let promise = codeActions.doCodeAction(null)
await helper.wait(50)
let ids = await nvim.call('coc#float#get_float_win_list') as number[]
expect(ids.length).toBeGreaterThan(0)
await nvim.input('<CR>')
await promise
let lines = await doc.buffer.lines
expect(lines).toEqual(['bar'])
})
it('should choose code actions by range', async () => {
let range: Range
disposables.push(languages.registerCodeActionProvider([{ language: '*' }], {
provideCodeActions: (
_document: TextDocument,
r: Range,
_context: CodeActionContext, _token: CancellationToken
) => {
range = r
return [CodeAction.create('my title'), CodeAction.create('b'), CodeAction.create('c')]
},
}, undefined))
await helper.createDocument()
await nvim.setLine('abc')
await nvim.command('normal! 0v$')
await nvim.input('<esc>')
await codeActions.doCodeAction('v', 'my title')
expect(range).toEqual({ start: { line: 0, character: 0 }, end: { line: 0, character: 3 } })
})
})
describe('doQuickfix', () => {
it('should throw when quickfix action does not exist', async () => {
let err
currActions = []
await helper.createDocument()
try {
await codeActions.doQuickfix()
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
it('should do preferred quickfix action', async () => {
let doc = await helper.createDocument()
let edits: TextEdit[] = []
edits.push(TextEdit.insert(Position.create(0, 0), 'bar'))
let edit = { changes: { [doc.uri]: edits } }
let action = CodeAction.create('code fix', edit, CodeActionKind.QuickFix)
action.isPreferred = true
currActions = [CodeAction.create('foo', CodeActionKind.QuickFix), action, CodeAction.create('bar')]
await codeActions.doQuickfix()
let lines = await doc.buffer.lines
expect(lines).toEqual(['bar'])
})
})
describe('applyCodeAction', () => {
it('should resolve codeAction', async () => {
let doc = await helper.createDocument()
let edits: TextEdit[] = []
edits.push(TextEdit.insert(Position.create(0, 0), 'bar'))
let edit = { changes: { [doc.uri]: edits } }
let action = CodeAction.create('code fix', CodeActionKind.QuickFix)
action.isPreferred = true
currActions = [action]
resolvedAction = Object.assign({ edit }, action)
let arr = await codeActions.getCurrentCodeActions('line', [CodeActionKind.QuickFix])
await codeActions.applyCodeAction(arr[0])
let lines = await doc.buffer.lines
expect(lines).toEqual(['bar'])
})
it('should throw for disabled action', async () => {
let action: any = CodeAction.create('my action', CodeActionKind.Empty)
action.disabled = { reason: 'disabled', providerId: 'x' }
let err
try {
await codeActions.applyCodeAction(action)
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
it('should invoke registered command after apply edit', async () => {
let called
disposables.push(commands.registerCommand('test.execute', async (s: string) => {
called = s
await nvim.command(s)
}))
let doc = await helper.createDocument()
let edits: TextEdit[] = []
edits.push(TextEdit.insert(Position.create(0, 0), 'bar'))
let edit = { changes: { [doc.uri]: edits } }
let action = CodeAction.create('code fix', CodeActionKind.QuickFix)
action.isPreferred = true
currActions = [action]
resolvedAction = Object.assign({
edit,
command: Command.create('run vim command', 'test.execute', 'normal! $')
}, action)
let arr = await codeActions.getCurrentCodeActions('line', [CodeActionKind.QuickFix])
await codeActions.applyCodeAction(arr[0])
let lines = await doc.buffer.lines
expect(lines).toEqual(['bar'])
expect(called).toBe('normal! $')
})
})
})

View file

@ -0,0 +1,316 @@
import { Neovim } from '@chemzqm/neovim'
import { Command, CodeLens, Disposable, Position, Range, TextEdit } from 'vscode-languageserver-protocol'
import commands from '../../commands'
import events from '../../events'
import CodeLensHandler from '../../handler/codelens/index'
import CodeLensBuffer, { getCommands } from '../../handler/codelens/buffer'
import languages from '../../languages'
import { disposeAll } from '../../util'
import helper from '../helper'
import workspace from '../../workspace'
let nvim: Neovim
let codeLens: CodeLensHandler
let disposables: Disposable[] = []
let srcId: number
jest.setTimeout(10000)
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
srcId = await nvim.createNamespace('coc-codelens')
codeLens = helper.plugin.getHandler().codeLens
})
beforeEach(() => {
helper.updateConfiguration('codeLens.enable', true)
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
await helper.reset()
disposeAll(disposables)
})
async function createBufferWithCodeLens(): Promise<CodeLensBuffer> {
disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], {
provideCodeLenses: () => {
return [{
range: Range.create(0, 0, 0, 1)
}]
},
resolveCodeLens: codeLens => {
codeLens.command = Command.create('save', '__save', 1, 2, 3)
return codeLens
}
}))
let doc = await helper.createDocument('e.js')
await nvim.call('setline', [1, ['a', 'b', 'c']])
await doc.synchronize()
await codeLens.checkProvider()
return codeLens.buffers.getItem(doc.bufnr)
}
describe('codeLenes featrue', () => {
it('should do codeLenes request and resolve codeLenes', async () => {
let buf = await createBufferWithCodeLens()
let doc = await workspace.document
let codelens = buf.currentCodeLens
expect(codelens).toBeDefined()
expect(codelens[0].command).toBeDefined()
let markers = await helper.getMarkers(doc.bufnr, srcId)
expect(markers.length).toBe(1)
})
it('should refresh on empty changes', async () => {
await createBufferWithCodeLens()
let doc = await workspace.document
await nvim.call('setline', [1, ['a', 'b', 'c']])
await doc.synchronize()
let markers = await helper.getMarkers(doc.bufnr, srcId)
expect(markers.length).toBe(1)
})
it('should work with empty codeLens', async () => {
disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], {
provideCodeLenses: () => {
return []
}
}))
let doc = await helper.createDocument('t.js')
let buf = codeLens.buffers.getItem(doc.bufnr)
let codelens = buf.currentCodeLens
expect(codelens).toBeUndefined()
})
it('should change codeLenes position', async () => {
let fn = jest.fn()
helper.updateConfiguration('codeLens.position', 'eol')
disposables.push(commands.registerCommand('__save', (...args) => {
fn(...args)
}))
disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], {
provideCodeLenses: () => {
return [{
range: Range.create(0, 0, 0, 1)
}]
},
resolveCodeLens: codeLens => {
codeLens.command = Command.create('save', '__save', 1, 2, 3)
return codeLens
}
}))
let doc = await helper.createDocument('example.js')
await nvim.call('setline', [1, ['a', 'b', 'c']])
await codeLens.checkProvider()
let res = await doc.buffer.getExtMarks(srcId, 0, -1, { details: true })
expect(res.length).toBeGreaterThan(0)
let arr = res[0][3]['virt_text']
expect(arr[0][0]).toBe('save')
})
it('should refresh codeLens on CursorHold', async () => {
disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], {
provideCodeLenses: document => {
let n = document.lineCount
let arr: any[] = []
for (let i = 0; i <= n - 2; i++) {
arr.push({
range: Range.create(i, 0, i, 1),
command: Command.create('save', '__save', i)
})
}
return arr
}
}))
let doc = await helper.createDocument('example.js')
await helper.wait(100)
let markers = await helper.getMarkers(doc.bufnr, srcId)
await nvim.call('setline', [1, ['a', 'b', 'c']])
await doc.synchronize()
await events.fire('CursorHold', [doc.bufnr])
await helper.wait(200)
markers = await helper.getMarkers(doc.bufnr, srcId)
expect(markers.length).toBe(3)
})
it('should cancel codeLenes request on document change', async () => {
let cancelled = false
disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], {
provideCodeLenses: (_, token) => {
return new Promise(resolve => {
token.onCancellationRequested(() => {
cancelled = true
clearTimeout(timer)
resolve(null)
})
let timer = setTimeout(() => {
resolve([{
range: Range.create(0, 0, 0, 1)
}, {
range: Range.create(1, 0, 1, 1)
}])
}, 2000)
disposables.push({
dispose: () => {
clearTimeout(timer)
}
})
})
},
resolveCodeLens: codeLens => {
codeLens.command = Command.create('save', '__save')
return codeLens
}
}))
let doc = await helper.createDocument('codelens.js')
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'a\nb\nc')])
expect(cancelled).toBe(true)
})
it('should resolve on CursorMoved', async () => {
disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], {
provideCodeLenses: () => {
return [{
range: Range.create(90, 0, 90, 1)
}, {
range: Range.create(91, 0, 91, 1)
}]
},
resolveCodeLens: async codeLens => {
codeLens.command = Command.create('save', '__save')
return codeLens
}
}))
let doc = await helper.createDocument('example.js')
let arr = new Array(100)
arr.fill('')
await nvim.call('setline', [1, arr])
await doc.synchronize()
await codeLens.checkProvider()
await nvim.command('normal! gg')
await nvim.command('normal! G')
await helper.wait(100)
let buf = codeLens.buffers.getItem(doc.bufnr)
let codelens = buf.currentCodeLens
expect(codelens).toBeDefined()
expect(codelens[0].command).toBeDefined()
expect(codelens[1].command).toBeDefined()
})
it('should invoke codeLenes action', async () => {
let fn = jest.fn()
disposables.push(commands.registerCommand('__save', (...args) => {
fn(...args)
}))
await createBufferWithCodeLens()
await helper.doAction('codeLensAction')
expect(fn).toBeCalledWith(1, 2, 3)
await nvim.command('normal! G')
await helper.doAction('codeLensAction')
})
it('should use picker for multiple codeLenses', async () => {
let fn = jest.fn()
disposables.push(commands.registerCommand('__save', (...args) => {
fn(...args)
}))
disposables.push(commands.registerCommand('__delete', (...args) => {
fn(...args)
}))
disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], {
provideCodeLenses: () => {
return [{
range: Range.create(0, 0, 0, 1),
command: Command.create('save', '__save', 1, 2, 3)
}, {
range: Range.create(0, 1, 0, 2),
command: Command.create('save', '__delete', 4, 5, 6)
}]
}
}))
let doc = await helper.createDocument('example.js')
await nvim.call('setline', [1, ['a', 'b', 'c']])
await doc.synchronize()
await codeLens.checkProvider()
let p = helper.doAction('codeLensAction')
await helper.waitFloat()
await nvim.input('<cr>')
await p
expect(fn).toBeCalledWith(1, 2, 3)
})
it('should refresh for failed codeLens request', async () => {
let called = 0
let fn = jest.fn()
disposables.push(commands.registerCommand('__save', (...args) => {
fn(...args)
}))
disposables.push(commands.registerCommand('__foo', (...args) => {
fn(...args)
}))
disposables.push(languages.registerCodeLensProvider([{ language: '*' }], {
provideCodeLenses: () => {
called++
if (called == 1) {
return null
}
return [{
range: Range.create(0, 0, 0, 1),
command: Command.create('foo', '__foo')
}]
}
}))
disposables.push(languages.registerCodeLensProvider([{ language: '*' }], {
provideCodeLenses: () => {
return [{
range: Range.create(0, 0, 0, 1),
command: Command.create('save', '__save')
}]
}
}))
let doc = await helper.createDocument('example.js')
await nvim.call('setline', [1, ['a', 'b', 'c']])
await codeLens.checkProvider()
let markers = await helper.getMarkers(doc.buffer.id, srcId)
expect(markers.length).toBeGreaterThan(0)
let codeLensBuffer = codeLens.buffers.getItem(doc.buffer.id)
await codeLensBuffer.forceFetch()
let curr = codeLensBuffer.currentCodeLens
expect(curr.length).toBeGreaterThan(1)
})
it('should use custom separator & position', async () => {
helper.updateConfiguration('codeLens.separator', '|')
helper.updateConfiguration('codeLens.position', 'eol')
let doc = await helper.createDocument('example.js')
await nvim.call('setline', [1, ['a', 'b', 'c']])
await doc.synchronize()
disposables.push(languages.registerCodeLensProvider([{ language: '*' }], {
provideCodeLenses: () => {
return [{
range: Range.create(0, 0, 1, 0),
command: Command.create('save', '__save')
}, {
range: Range.create(0, 0, 1, 0),
command: Command.create('save', '__save')
}]
}
}))
await codeLens.checkProvider()
let res = await doc.buffer.getExtMarks(srcId, 0, -1, { details: true })
expect(res.length).toBe(1)
})
it('should get commands from codeLenses', async () => {
expect(getCommands(1, undefined)).toEqual([])
let codeLenses = [CodeLens.create(Range.create(0, 0, 0, 0))]
expect(getCommands(0, codeLenses)).toEqual([])
codeLenses = [CodeLens.create(Range.create(0, 0, 1, 0)), CodeLens.create(Range.create(2, 0, 3, 0))]
codeLenses[0].command = Command.create('save', '__save')
expect(getCommands(0, codeLenses).length).toEqual(1)
})
})

View file

@ -0,0 +1,264 @@
import { Neovim } from '@chemzqm/neovim'
import { CancellationToken, Color, ColorInformation, ColorPresentation, Disposable, Position, Range } from 'vscode-languageserver-protocol'
import { TextDocument } from 'vscode-languageserver-textdocument'
import commands from '../../commands'
import { toHexString } from '../../util/color'
import Colors from '../../handler/colors/index'
import languages from '../../languages'
import { ProviderResult } from '../../provider'
import { disposeAll } from '../../util'
import path from 'path'
import helper from '../helper'
let nvim: Neovim
let state = 'normal'
let colors: Colors
let disposables: Disposable[] = []
let colorPresentations: ColorPresentation[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
await nvim.command(`source ${path.join(process.cwd(), 'autoload/coc/color.vim')}`)
colors = helper.plugin.getHandler().colors
disposables.push(languages.registerDocumentColorProvider([{ language: '*' }], {
provideColorPresentations: (
_color: Color,
_context: { document: TextDocument; range: Range },
_token: CancellationToken
): ColorPresentation[] => colorPresentations,
provideDocumentColors: (
document: TextDocument,
_token: CancellationToken
): ProviderResult<ColorInformation[]> => {
if (state == 'empty') return []
if (state == 'error') return Promise.reject(new Error('no color'))
let matches = Array.from((document.getText() as any).matchAll(/#\w{6}/g)) as any
return matches.map(o => {
let start = document.positionAt(o.index)
let end = document.positionAt(o.index + o[0].length)
return {
range: Range.create(start, end),
color: getColor(255, 255, 255)
}
})
}
}))
})
beforeEach(() => {
helper.updateConfiguration('colors.filetypes', ['*'])
})
afterAll(async () => {
disposeAll(disposables)
await helper.shutdown()
})
afterEach(async () => {
colorPresentations = []
await helper.reset()
})
function getColor(r: number, g: number, b: number): Color {
return { red: r / 255, green: g / 255, blue: b / 255, alpha: 1 }
}
describe('Colors', () => {
describe('utils', () => {
it('should get hex string', () => {
let color = getColor(255, 255, 255)
let hex = toHexString(color)
expect(hex).toBe('ffffff')
})
})
describe('configuration', () => {
it('should toggle enable state on configuration change', async () => {
let doc = await helper.createDocument()
helper.updateConfiguration('colors.filetypes', [])
let enabled = colors.isEnabled(doc.bufnr)
helper.updateConfiguration('colors.filetypes', ['*'])
expect(enabled).toBe(false)
})
})
describe('commands', () => {
it('should register editor.action.pickColor command', async () => {
await helper.mockFunction('coc#color#pick_color', [0, 0, 0])
let doc = await helper.createDocument()
await nvim.setLine('#ffffff')
doc.forceSync()
await colors.doHighlight(doc.bufnr)
await commands.executeCommand('editor.action.pickColor')
let line = await nvim.getLine()
expect(line).toBe('#000000')
})
it('should register editor.action.colorPresentation command', async () => {
colorPresentations = [ColorPresentation.create('red'), ColorPresentation.create('#ff0000')]
let doc = await helper.createDocument()
await nvim.setLine('#ffffff')
doc.forceSync()
await colors.doHighlight(doc.bufnr)
let p = commands.executeCommand('editor.action.colorPresentation')
await helper.wait(100)
await nvim.input('1<enter>')
await p
let line = await nvim.getLine()
expect(line).toBe('red')
})
})
describe('doHighlight', () => {
it('should clearHighlight on empty result', async () => {
let doc = await helper.createDocument()
await nvim.setLine('#ffffff')
state = 'empty'
await colors.doHighlight(doc.bufnr)
let res = colors.hasColor(doc.bufnr)
expect(res).toBe(false)
state = 'normal'
})
it('should not highlight on error result', async () => {
let doc = await helper.createDocument()
await nvim.setLine('#ffffff')
state = 'error'
let err
try {
await colors.doHighlight(doc.bufnr)
} catch (e) {
err = e
}
expect(err).toBeDefined()
state = 'normal'
})
it('should highlight after document changed', async () => {
let doc = await helper.createDocument()
doc.forceSync()
await colors.doHighlight(doc.bufnr)
expect(colors.hasColor(doc.bufnr)).toBe(false)
expect(colors.hasColorAtPosition(doc.bufnr, Position.create(0, 1))).toBe(false)
await nvim.setLine('#ffffff #ff0000')
await doc.synchronize()
await helper.wait(100)
expect(colors.hasColorAtPosition(doc.bufnr, Position.create(0, 1))).toBe(true)
expect(colors.hasColor(doc.bufnr)).toBe(true)
})
it('should clearHighlight on clearHighlight', async () => {
let doc = await helper.createDocument()
await nvim.setLine('#ffffff #ff0000')
doc.forceSync()
await colors.doHighlight(doc.bufnr)
expect(colors.hasColor(doc.bufnr)).toBe(true)
colors.clearHighlight(doc.bufnr)
expect(colors.hasColor(doc.bufnr)).toBe(false)
})
it('should highlight colors', async () => {
let doc = await helper.createDocument()
await nvim.setLine('#ffffff')
await colors.doHighlight(doc.bufnr)
let exists = await nvim.call('hlexists', 'BGffffff')
expect(exists).toBe(1)
})
})
describe('hasColor()', () => {
it('should return false when bufnr does not exist', async () => {
let res = colors.hasColor(99)
colors.clearHighlight(99)
expect(res).toBe(false)
})
})
describe('getColorInformation()', () => {
it('should return null when highlighter does not exist', async () => {
let res = await colors.getColorInformation(99)
expect(res).toBe(null)
})
it('should return null when color not found', async () => {
let doc = await helper.createDocument()
await nvim.setLine('#ffffff foo ')
doc.forceSync()
await colors.doHighlight(doc.bufnr)
await nvim.call('cursor', [1, 12])
let res = await colors.getColorInformation(doc.bufnr)
expect(res).toBe(null)
})
})
describe('hasColorAtPosition()', () => {
it('should return false when bufnr does not exist', async () => {
let res = colors.hasColorAtPosition(99, Position.create(0, 0))
expect(res).toBe(false)
})
})
describe('pickPresentation()', () => {
it('should show warning when color does not exist', async () => {
await helper.createDocument()
await colors.pickPresentation()
let msg = await helper.getCmdline()
expect(msg).toMatch('Color not found')
})
it('should not throw when presentations do not exist', async () => {
colorPresentations = []
let doc = await helper.createDocument()
await nvim.setLine('#ffffff')
doc.forceSync()
await colors.doHighlight(99)
await colors.doHighlight(doc.bufnr)
await helper.doAction('colorPresentation')
})
it('should pick presentations', async () => {
colorPresentations = [ColorPresentation.create('red'), ColorPresentation.create('#ff0000')]
let doc = await helper.createDocument()
await nvim.setLine('#ffffff')
doc.forceSync()
await colors.doHighlight(doc.bufnr)
let p = helper.doAction('colorPresentation')
await helper.wait(100)
await nvim.input('1<enter>')
await p
let line = await nvim.getLine()
expect(line).toBe('red')
})
})
describe('pickColor()', () => {
it('should show warning when color does not exist', async () => {
await helper.createDocument()
await colors.pickColor()
let msg = await helper.getCmdline()
expect(msg).toMatch('not found')
})
it('should pickColor', async () => {
await helper.mockFunction('coc#color#pick_color', [0, 0, 0])
let doc = await helper.createDocument()
await nvim.setLine('#ffffff')
doc.forceSync()
await colors.doHighlight(doc.bufnr)
await helper.doAction('pickColor')
let line = await nvim.getLine()
expect(line).toBe('#000000')
})
it('should not throw when pick color return 0', async () => {
await helper.mockFunction('coc#color#pick_color', 0)
let doc = await helper.createDocument()
await nvim.setLine('#ffffff')
doc.forceSync()
await colors.doHighlight(doc.bufnr)
await helper.doAction('pickColor')
let line = await nvim.getLine()
expect(line).toBe('#ffffff')
})
})
})

View file

@ -0,0 +1,82 @@
import { Neovim } from '@chemzqm/neovim'
import { Disposable } from 'vscode-languageserver-protocol'
import CommandsHandler from '../../handler/commands'
import commandManager from '../../commands'
import { disposeAll } from '../../util'
import helper from '../helper'
let nvim: Neovim
let commands: CommandsHandler
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
commands = (helper.plugin as any).handler.commands
})
afterAll(async () => {
await helper.shutdown()
})
beforeEach(async () => {
await helper.createDocument()
})
afterEach(async () => {
disposeAll(disposables)
await helper.reset()
})
describe('Commands', () => {
describe('addVimCommand', () => {
it('should register global vim commands', async () => {
await commandManager.executeCommand('vim.config')
await helper.wait(50)
let bufname = await nvim.call('bufname', ['%'])
expect(bufname).toMatch('coc-settings.json')
let list = commands.getCommandList()
expect(list.includes('vim.config')).toBe(true)
})
it('should add vim command with title', async () => {
commands.addVimCommand({ id: 'list', cmd: 'CocList', title: 'list of coc.nvim' })
let res = commandManager.titles.get('vim.list')
expect(res).toBe('list of coc.nvim')
commandManager.unregister('vim.list')
})
})
describe('getCommands', () => {
it('should get command items', async () => {
let res = commands.getCommands()
let idx = res.findIndex(o => o.id == 'workspace.showOutput')
expect(idx != -1).toBe(true)
})
})
describe('repeat', () => {
it('should repeat command', async () => {
// let buf = await nvim.buffer
await nvim.call('setline', [1, ['a', 'b', 'c']])
await nvim.call('cursor', [1, 1])
commands.addVimCommand({ id: 'remove', cmd: 'normal! dd' })
await commands.runCommand('vim.remove')
await helper.wait(50)
let res = await nvim.call('getline', [1, '$'])
expect(res).toEqual(['b', 'c'])
await commands.repeat()
await helper.wait(50)
res = await nvim.call('getline', [1, '$'])
expect(res).toEqual(['c'])
})
})
describe('runCommand', () => {
it('should open command list without id', async () => {
await commands.runCommand()
await helper.wait(100)
let bufname = await nvim.call('bufname', ['%'])
expect(bufname).toBe('list:///commands')
})
})
})

View file

@ -0,0 +1,77 @@
import { Neovim } from '@chemzqm/neovim'
import { CancellationTokenSource, Disposable, FoldingRange, Range } from 'vscode-languageserver-protocol'
import FoldHandler from '../../handler/fold'
import languages from '../../languages'
import workspace from '../../workspace'
import { disposeAll } from '../../util'
import helper from '../helper'
let nvim: Neovim
let folds: FoldHandler
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
folds = (helper.plugin as any).handler.fold
})
afterAll(async () => {
await helper.shutdown()
})
beforeEach(async () => {
await helper.createDocument()
})
afterEach(async () => {
disposeAll(disposables)
await helper.reset()
})
describe('Folds', () => {
it('should return null when provider does not exist', async () => {
let doc = await workspace.document
let token = (new CancellationTokenSource()).token
expect(await languages.provideFoldingRanges(doc.textDocument, {}, token)).toBe(null)
})
it('should return false when no fold ranges found', async () => {
disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], {
provideFoldingRanges(_doc) {
return []
}
}))
let res = await folds.fold()
expect(res).toBe(false)
})
it('should fold all fold ranges', async () => {
disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], {
provideFoldingRanges(_doc) {
return [FoldingRange.create(1, 3), FoldingRange.create(4, 6, 0, 0, 'comment')]
}
}))
await nvim.call('setline', [1, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']])
let res = await folds.fold()
expect(res).toBe(true)
let closed = await nvim.call('foldclosed', [2])
expect(closed).toBe(2)
closed = await nvim.call('foldclosed', [5])
expect(closed).toBe(5)
})
it('should fold comment ranges', async () => {
disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], {
provideFoldingRanges(_doc) {
return [FoldingRange.create(1, 3), FoldingRange.create(4, 6, 0, 0, 'comment')]
}
}))
await nvim.call('setline', [1, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']])
let res = await folds.fold('comment')
expect(res).toBe(true)
let closed = await nvim.call('foldclosed', [2])
expect(closed).toBe(-1)
closed = await nvim.call('foldclosed', [5])
expect(closed).toBe(5)
})
})

View file

@ -0,0 +1,252 @@
import { Neovim } from '@chemzqm/neovim'
import { CancellationTokenSource, Disposable, Position, Range, TextEdit } from 'vscode-languageserver-protocol'
import Format from '../../handler/format'
import languages from '../../languages'
import { disposeAll } from '../../util'
import window from '../../window'
import workspace from '../../workspace'
import helper, { createTmpFile } from '../helper'
let nvim: Neovim
let disposables: Disposable[] = []
let format: Format
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
format = helper.plugin.getHandler().format
})
beforeEach(() => {
helper.updateConfiguration('coc.preferences.formatOnType', true)
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
await helper.reset()
disposeAll(disposables)
})
describe('format handler', () => {
describe('documentFormat', () => {
it('should throw when provider not found', async () => {
let doc = await helper.createDocument()
let err
try {
await format.documentFormat(doc)
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
it('should return false when get empty edits ', async () => {
disposables.push(languages.registerDocumentFormatProvider(['*'], {
provideDocumentFormattingEdits: () => {
return []
}
}))
let doc = await helper.createDocument()
let res = await format.documentFormat(doc)
expect(res).toBe(false)
})
})
describe('formatOnSave', () => {
it('should not throw when provider not found', async () => {
helper.updateConfiguration('coc.preferences.formatOnSaveFiletypes', ['javascript'])
let filepath = await createTmpFile('')
await helper.edit(filepath)
await nvim.command('setf javascript')
await nvim.setLine('foo')
await nvim.command('silent w')
await helper.wait(100)
})
it('should invoke format on save', async () => {
helper.updateConfiguration('coc.preferences.formatOnSaveFiletypes', ['text'])
disposables.push(languages.registerDocumentFormatProvider(['text'], {
provideDocumentFormattingEdits: document => {
let lines = document.getText().replace(/\n$/, '').split(/\n/)
let edits: TextEdit[] = []
for (let i = 0; i < lines.length; i++) {
let text = lines[i]
if (!text.startsWith(' ')) {
edits.push(TextEdit.insert(Position.create(i, 0), ' '))
}
}
return edits
}
}))
let filepath = await createTmpFile('a\nb\nc\n')
let buf = await helper.edit(filepath)
await nvim.command('setf text')
await nvim.command('w')
let lines = await buf.lines
expect(lines).toEqual([' a', ' b', ' c'])
})
it('should cancel when timeout', async () => {
helper.updateConfiguration('coc.preferences.formatOnSaveFiletypes', ['*'])
disposables.push(languages.registerDocumentFormatProvider(['*'], {
provideDocumentFormattingEdits: () => {
return new Promise(resolve => {
setTimeout(() => {
resolve(undefined)
}, 2000)
})
}
}))
let filepath = await createTmpFile('a\nb\nc\n')
await helper.edit(filepath)
let n = Date.now()
await nvim.command('w')
expect(Date.now() - n).toBeLessThan(1000)
})
})
describe('rangeFormat', () => {
it('should return null when provider does not exist', async () => {
let doc = (await workspace.document).textDocument
let range = Range.create(0, 0, 1, 0)
let options = await workspace.getFormatOptions()
let token = (new CancellationTokenSource()).token
expect(await languages.provideDocumentRangeFormattingEdits(doc, range, options, token)).toBe(null)
expect(languages.hasProvider('onTypeEdit', doc)).toBe(false)
let edits = await languages.provideDocumentFormattingEdits(doc, options, token)
expect(edits).toBe(null)
})
it('should invoke range format', async () => {
disposables.push(languages.registerDocumentRangeFormatProvider(['text'], {
provideDocumentRangeFormattingEdits: (_document, range) => {
let lines: number[] = []
for (let i = range.start.line; i <= range.end.line; i++) {
lines.push(i)
}
return lines.map(i => {
return TextEdit.insert(Position.create(i, 0), ' ')
})
}
}))
let doc = await helper.createDocument()
await nvim.call('setline', [1, ['a', 'b', 'c']])
await nvim.command('setf text')
await nvim.command('normal! ggvG')
await nvim.input('<esc>')
expect(languages.hasFormatProvider(doc.textDocument)).toBe(true)
expect(languages.hasProvider('format', doc.textDocument)).toBe(true)
await helper.doAction('formatSelected', 'v')
let buf = nvim.createBuffer(doc.bufnr)
let lines = await buf.lines
expect(lines).toEqual([' a', ' b', ' c'])
let options = await workspace.getFormatOptions(doc.uri)
let token = (new CancellationTokenSource()).token
let edits = await languages.provideDocumentFormattingEdits(doc.textDocument, options, token)
expect(edits.length).toBeGreaterThan(0)
})
it('should format range by formatexpr option', async () => {
let range: Range
disposables.push(languages.registerDocumentRangeFormatProvider(['text'], {
provideDocumentRangeFormattingEdits: (_document, r) => {
range = r
return []
}
}))
await helper.createDocument()
await nvim.call('setline', [1, ['a', 'b', 'c']])
await nvim.command('setf text')
await nvim.command(`setl formatexpr=CocAction('formatSelected')`)
await nvim.command('normal! ggvGgq')
expect(range).toEqual({
start: { line: 0, character: 0 }, end: { line: 3, character: 0 }
})
})
})
describe('formatOnType', () => {
it('should invoke format', async () => {
disposables.push(languages.registerDocumentFormatProvider(['text'], {
provideDocumentFormattingEdits: () => {
return [TextEdit.insert(Position.create(0, 0), ' ')]
}
}))
await helper.createDocument()
await nvim.setLine('foo')
await nvim.command('setf text')
await helper.doAction('format')
let line = await nvim.line
expect(line).toEqual(' foo')
})
it('should does format on type', async () => {
disposables.push(languages.registerOnTypeFormattingEditProvider(['text'], {
provideOnTypeFormattingEdits: () => {
return [TextEdit.insert(Position.create(0, 0), ' ')]
}
}, ['|']))
await helper.edit()
await nvim.command('setf text')
await nvim.input('i|')
await helper.wait(200)
let line = await nvim.line
expect(line).toBe(' |')
let cursor = await window.getCursorPosition()
expect(cursor).toEqual({ line: 0, character: 3 })
})
it('should adjust cursor after format on type', async () => {
disposables.push(languages.registerOnTypeFormattingEditProvider(['text'], {
provideOnTypeFormattingEdits: () => {
return [
TextEdit.insert(Position.create(0, 0), ' '),
TextEdit.insert(Position.create(0, 2), 'end')
]
}
}, ['|']))
await helper.edit()
await nvim.command('setf text')
await nvim.setLine('"')
await nvim.input('i|')
await helper.wait(100)
let line = await nvim.line
expect(line).toBe(' |"end')
let cursor = await window.getCursorPosition()
expect(cursor).toEqual({ line: 0, character: 3 })
})
})
describe('bracketEnterImprove', () => {
afterEach(() => {
nvim.command('iunmap <CR>', true)
})
it('should format vim file on enter', async () => {
let buf = await helper.edit('foo.vim')
await nvim.command(`inoremap <silent><expr> <cr> pumvisible() ? coc#_select_confirm() : "\\<C-g>u\\<CR>\\<c-r>=coc#on_enter()\\<CR>"`)
await nvim.setLine('let foo={}')
await nvim.command(`normal! gg$`)
await nvim.input('i')
await nvim.eval(`feedkeys("\\<CR>", 'im')`)
await helper.wait(100)
let lines = await buf.lines
expect(lines).toEqual(['let foo={', ' \\ ', ' \\ }'])
})
it('should add new line between bracket', async () => {
let buf = await helper.edit()
await nvim.command(`inoremap <silent><expr> <cr> pumvisible() ? coc#_select_confirm() : "\\<C-g>u\\<CR>\\<c-r>=coc#on_enter()\\<CR>"`)
await nvim.setLine(' {}')
await nvim.command(`normal! gg$`)
await nvim.input('i')
await nvim.eval(`feedkeys("\\<CR>", 'im')`)
await helper.wait(100)
let lines = await buf.lines
expect(lines).toEqual([' {', ' ', ' }'])
})
})
})

View file

@ -0,0 +1,140 @@
import { Neovim } from '@chemzqm/neovim'
import { Disposable, DocumentHighlightKind, Position, Range } from 'vscode-languageserver-protocol'
import Highlights from '../../handler/highlights'
import languages from '../../languages'
import workspace from '../../workspace'
import { disposeAll } from '../../util'
import helper from '../helper'
let nvim: Neovim
let disposables: Disposable[] = []
let highlights: Highlights
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
highlights = helper.plugin.getHandler().documentHighlighter
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
await helper.reset()
disposeAll(disposables)
disposables = []
})
function registProvider(): void {
disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], {
provideDocumentHighlights: async document => {
let word = await nvim.eval('expand("<cword>")')
// let word = document.get
let matches = Array.from((document.getText() as any).matchAll(/\w+/g)) as any[]
let filtered = matches.filter(o => o[0] == word)
return filtered.map((o, i) => {
let start = document.positionAt(o.index)
let end = document.positionAt(o.index + o[0].length)
return {
range: Range.create(start, end),
kind: i % 2 == 0 ? DocumentHighlightKind.Read : DocumentHighlightKind.Write
}
})
}
}))
}
describe('document highlights', () => {
function registerTimerProvider(fn: Function, timeout: number): void {
disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], {
provideDocumentHighlights: (_document, _position, token) => {
return new Promise(resolve => {
token.onCancellationRequested(() => {
clearTimeout(timer)
fn()
resolve([])
})
let timer = setTimeout(() => {
resolve([{ range: Range.create(0, 0, 0, 3) }])
}, timeout)
})
}
}))
}
it('should return null when highlights provide does not exist', async () => {
let doc = await helper.createDocument()
let res = await highlights.getHighlights(doc, Position.create(0, 0))
expect(res).toBeNull()
})
it('should cancel request on CursorMoved', async () => {
let fn = jest.fn()
registerTimerProvider(fn, 3000)
await helper.edit()
await nvim.setLine('foo')
let p = highlights.highlight()
await helper.wait(50)
await nvim.call('cursor', [1, 2])
await p
expect(fn).toBeCalled()
})
it('should cancel on timeout', async () => {
helper.updateConfiguration('documentHighlight.timeout', 10)
let fn = jest.fn()
registerTimerProvider(fn, 3000)
await helper.edit()
await nvim.setLine('foo')
await highlights.highlight()
expect(fn).toBeCalled()
})
it('should add highlights to symbols', async () => {
registProvider()
await helper.createDocument()
await nvim.setLine('foo bar foo')
await helper.doAction('highlight')
let winid = await nvim.call('win_getid') as number
expect(highlights.hasHighlights(winid)).toBe(true)
})
it('should return highlight ranges', async () => {
registProvider()
await helper.createDocument()
await nvim.setLine('foo bar foo')
let res = await helper.doAction('symbolRanges')
expect(res.length).toBe(2)
})
it('should return null when cursor not in word range', async () => {
disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], {
provideDocumentHighlights: () => {
return [{ range: Range.create(0, 0, 0, 3) }]
}
}))
let doc = await helper.createDocument()
await nvim.setLine(' oo')
await nvim.call('cursor', [1, 2])
let res = await highlights.getHighlights(doc, Position.create(0, 0))
expect(res).toBeNull()
})
it('should not throw when document is command line', async () => {
await nvim.call('feedkeys', ['q:', 'in'])
let doc = await workspace.document
expect(doc.isCommandLine).toBe(true)
await highlights.highlight()
await nvim.input('<C-c>')
})
it('should not throw when provider not found', async () => {
disposeAll(disposables)
await helper.createDocument()
await nvim.setLine(' oo')
await nvim.call('cursor', [1, 2])
await highlights.highlight()
})
})

View file

@ -0,0 +1,178 @@
import { Neovim } from '@chemzqm/neovim'
import { Disposable, MarkedString, Hover, Range, TextEdit, Position } from 'vscode-languageserver-protocol'
import HoverHandler from '../../handler/hover'
import { URI } from 'vscode-uri'
import languages from '../../languages'
import { disposeAll } from '../../util'
import helper, { createTmpFile } from '../helper'
let nvim: Neovim
let hover: HoverHandler
let disposables: Disposable[] = []
let hoverResult: Hover
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
hover = helper.plugin.getHandler().hover
})
afterAll(async () => {
await helper.shutdown()
})
beforeEach(async () => {
await helper.createDocument()
disposables.push(languages.registerHoverProvider([{ language: '*' }], {
provideHover: (_doc, _pos, _token) => {
return hoverResult
}
}))
})
afterEach(async () => {
disposeAll(disposables)
await helper.reset()
})
async function getDocumentText(): Promise<string> {
let lines = await nvim.call('getbufline', ['coc://document', 1, '$']) as string[]
return lines.join('\n')
}
describe('Hover', () => {
describe('onHover', () => {
it('should return false when hover not found', async () => {
hoverResult = null
let res = await hover.onHover('preview')
expect(res).toBe(false)
})
it('should show MarkupContent hover', async () => {
hoverResult = { contents: { kind: 'plaintext', value: 'my hover' } }
await hover.onHover('preview')
let res = await getDocumentText()
expect(res).toMatch('my hover')
})
it('should show MarkedString hover', async () => {
hoverResult = { contents: 'string hover' }
disposables.push(languages.registerHoverProvider([{ language: '*' }], {
provideHover: (_doc, _pos, _token) => {
return { contents: { language: 'typescript', value: 'language hover' } }
}
}))
await hover.onHover('preview')
let res = await getDocumentText()
expect(res).toMatch('string hover')
expect(res).toMatch('language hover')
})
it('should show MarkedString hover array', async () => {
hoverResult = { contents: ['foo', { language: 'typescript', value: 'bar' }] }
await hover.onHover('preview')
let res = await getDocumentText()
expect(res).toMatch('foo')
expect(res).toMatch('bar')
})
it('should highlight hover range', async () => {
await nvim.setLine('var')
await nvim.command('normal! 0')
hoverResult = { contents: ['foo'], range: Range.create(0, 0, 0, 3) }
await hover.onHover('preview')
let res = await nvim.call('getmatches') as any[]
expect(res.length).toBe(1)
expect(res[0].group).toBe('CocHoverRange')
await helper.wait(600)
res = await nvim.call('getmatches')
expect(res.length).toBe(0)
})
})
describe('previewHover', () => {
it('should echo hover message', async () => {
hoverResult = { contents: ['foo'] }
let res = await hover.onHover('echo')
expect(res).toBe(true)
let msg = await helper.getCmdline()
expect(msg).toMatch('foo')
})
it('should show hover in float window', async () => {
hoverResult = { contents: { kind: 'markdown', value: '```typescript\nconst foo:number\n```' } }
await hover.onHover('float')
let win = await helper.getFloat()
expect(win).toBeDefined()
let lines = await nvim.eval(`getbufline(winbufnr(${win.id}),1,'$')`)
expect(lines).toEqual(['const foo:number'])
})
})
describe('getHover', () => {
it('should get hover from MarkedString array', async () => {
hoverResult = { contents: ['foo', { language: 'typescript', value: 'bar' }] }
disposables.push(languages.registerHoverProvider([{ language: '*' }], {
provideHover: (_doc, _pos, _token) => {
return { contents: { language: 'typescript', value: 'MarkupContent hover' } }
}
}))
disposables.push(languages.registerHoverProvider([{ language: '*' }], {
provideHover: (_doc, _pos, _token) => {
return { contents: MarkedString.fromPlainText('MarkedString hover') }
}
}))
let res = await hover.getHover()
expect(res.includes('foo')).toBe(true)
expect(res.includes('bar')).toBe(true)
expect(res.includes('MarkupContent hover')).toBe(true)
expect(res.includes('MarkedString hover')).toBe(true)
})
it('should filter empty hover message', async () => {
hoverResult = { contents: [''] }
let res = await hover.getHover()
expect(res.length).toBe(0)
})
})
describe('definitionHover', () => {
it('should load definition from buffer', async () => {
hoverResult = { contents: 'string hover' }
let doc = await helper.createDocument()
await nvim.call('cursor', [1, 1])
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar')])
disposables.push(languages.registerDefinitionProvider([{ language: '*' }], {
provideDefinition() {
return [{
targetUri: doc.uri,
targetRange: Range.create(0, 0, 1, 3),
targetSelectionRange: Range.create(0, 0, 0, 3),
}]
}
}))
await hover.definitionHover('preview')
let res = await getDocumentText()
expect(res).toBe('string hover\n\nfoo\nbar')
})
it('should load definition link from file', async () => {
let fsPath = await createTmpFile('foo\nbar\n')
hoverResult = { contents: 'string hover' }
let doc = await helper.createDocument()
await nvim.call('cursor', [1, 1])
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar')])
disposables.push(languages.registerDefinitionProvider([{ language: '*' }], {
provideDefinition() {
return [{
targetUri: URI.file(fsPath).toString(),
targetRange: Range.create(0, 0, 1, 3),
targetSelectionRange: Range.create(0, 0, 0, 3),
}]
}
}))
await hover.definitionHover('preview')
let res = await getDocumentText()
expect(res).toBe('string hover\n\nfoo\nbar')
})
})
})

View file

@ -0,0 +1,93 @@
import { Neovim } from '@chemzqm/neovim'
import { Disposable } from 'vscode-languageserver-protocol'
import Handler from '../../handler/index'
import { disposeAll } from '../../util'
import helper from '../helper'
let nvim: Neovim
let handler: Handler
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
handler = (helper.plugin as any).handler
})
afterAll(async () => {
await helper.shutdown()
})
beforeEach(async () => {
await helper.createDocument()
})
afterEach(async () => {
disposeAll(disposables)
await helper.reset()
})
describe('Handler', () => {
describe('hasProvider', () => {
it('should check provider for document', async () => {
let res = await handler.hasProvider('definition')
expect(res).toBe(false)
})
})
describe('checkProvier', () => {
it('should throw error when provider not found', async () => {
let doc = await helper.createDocument()
let err
try {
handler.checkProvier('definition', doc.textDocument)
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
})
describe('withRequestToken', () => {
it('should cancel previous request when called again', async () => {
let cancelled = false
let p = handler.withRequestToken('test', token => {
return new Promise(s => {
token.onCancellationRequested(() => {
cancelled = true
clearTimeout(timer)
s(undefined)
})
let timer = setTimeout(() => {
s(undefined)
}, 3000)
})
}, false)
setTimeout(async () => {
await handler.withRequestToken('test', () => {
return Promise.resolve(undefined)
}, false)
}, 50)
await p
expect(cancelled).toBe(true)
})
it('should cancel request on insert start', async () => {
let cancelled = false
let p = handler.withRequestToken('test', token => {
return new Promise(s => {
token.onCancellationRequested(() => {
cancelled = true
clearTimeout(timer)
s(undefined)
})
let timer = setTimeout(() => {
s(undefined)
}, 3000)
})
}, false)
await nvim.input('i')
await p
expect(cancelled).toBe(true)
})
})
})

View file

@ -0,0 +1,233 @@
import { Neovim } from '@chemzqm/neovim'
import { CancellationTokenSource, Disposable, Position, Range } from 'vscode-languageserver-protocol'
import InlayHintHandler from '../../handler/inlayHint/index'
import { InlayHint } from '../../inlayHint'
import languages from '../../languages'
import { isValidInlayHint, sameHint } from '../../provider/inlayHintManager'
import { disposeAll } from '../../util'
import workspace from '../../workspace'
import helper from '../helper'
let nvim: Neovim
let handler: InlayHintHandler
let disposables: Disposable[] = []
let ns: number
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
handler = helper.plugin.getHandler().inlayHintHandler
ns = await nvim.createNamespace('coc-inlayHint')
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
disposeAll(disposables)
await helper.reset()
})
describe('InlayHint', () => {
describe('utils', () => {
it('should check same hint', () => {
let hint = InlayHint.create(Position.create(0, 0), 'foo')
expect(sameHint(hint, InlayHint.create(Position.create(0, 0), 'bar'))).toBe(false)
expect(sameHint(hint, InlayHint.create(Position.create(0, 0), [{ value: 'foo' }]))).toBe(true)
})
it('should check valid hint', () => {
let hint = InlayHint.create(Position.create(0, 0), 'foo')
expect(isValidInlayHint(hint, Range.create(0, 0, 1, 0))).toBe(true)
expect(isValidInlayHint(InlayHint.create(Position.create(0, 0), ''), Range.create(0, 0, 1, 0))).toBe(false)
expect(isValidInlayHint(InlayHint.create(Position.create(3, 0), 'foo'), Range.create(0, 0, 1, 0))).toBe(false)
expect(isValidInlayHint({ label: 'f' } as any, Range.create(0, 0, 1, 0))).toBe(false)
})
})
describe('provideInlayHints', () => {
it('should not throw when failed', async () => {
disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], {
provideInlayHints: () => {
return Promise.reject(new Error('Test failure'))
}
}))
let doc = await workspace.document
let tokenSource = new CancellationTokenSource()
let res = await languages.provideInlayHints(doc.textDocument, Range.create(0, 0, 1, 0), tokenSource.token)
expect(res).toEqual([])
})
it('should merge provide results', async () => {
disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], {
provideInlayHints: () => {
return [InlayHint.create(Position.create(0, 0), 'foo')]
}
}))
disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], {
provideInlayHints: () => {
return [
InlayHint.create(Position.create(0, 0), 'foo'),
InlayHint.create(Position.create(1, 0), 'bar'),
InlayHint.create(Position.create(5, 0), 'bad')]
}
}))
let doc = await workspace.document
let tokenSource = new CancellationTokenSource()
let res = await languages.provideInlayHints(doc.textDocument, Range.create(0, 0, 3, 0), tokenSource.token)
expect(res.length).toBe(2)
})
it('should resolve inlay hint', async () => {
disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], {
provideInlayHints: () => {
return [InlayHint.create(Position.create(0, 0), 'foo')]
},
resolveInlayHint: hint => {
hint.tooltip = 'tooltip'
return hint
}
}))
let doc = await workspace.document
let tokenSource = new CancellationTokenSource()
let res = await languages.provideInlayHints(doc.textDocument, Range.create(0, 0, 1, 0), tokenSource.token)
let resolved = await languages.resolveInlayHint(res[0], tokenSource.token)
expect(resolved.tooltip).toBe('tooltip')
resolved = await languages.resolveInlayHint(resolved, tokenSource.token)
expect(resolved.tooltip).toBe('tooltip')
})
it('should not resolve when cancelled', async () => {
disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], {
provideInlayHints: () => {
return [InlayHint.create(Position.create(0, 0), 'foo')]
},
resolveInlayHint: (hint, token) => {
return new Promise(resolve => {
token.onCancellationRequested(() => {
clearTimeout(timer)
resolve(null)
})
let timer = setTimeout(() => {
resolve(Object.assign({}, hint, { tooltip: 'tooltip' }))
}, 200)
})
}
}))
let doc = await workspace.document
let tokenSource = new CancellationTokenSource()
let res = await languages.provideInlayHints(doc.textDocument, Range.create(0, 0, 1, 0), tokenSource.token)
let p = languages.resolveInlayHint(res[0], tokenSource.token)
tokenSource.cancel()
let resolved = await p
expect(resolved.tooltip).toBeUndefined()
})
})
describe('setVirtualText', () => {
async function registerProvider(content: string): Promise<Disposable> {
let doc = await workspace.document
let disposable = languages.registerInlayHintsProvider([{ language: '*' }], {
provideInlayHints: (document, range) => {
let content = document.getText(range)
let lines = content.split(/\r?\n/)
let hints: InlayHint[] = []
for (let i = 0; i < lines.length; i++) {
let line = lines[i]
if (!line.length) continue
let parts = line.split(/\s+/)
hints.push(...parts.map(s => InlayHint.create(Position.create(range.start.line + i, line.length), s)))
}
return hints
}
})
await doc.buffer.setLines(content.split(/\n/), { start: 0, end: -1 })
await doc.synchronize()
return disposable
}
async function waitRefresh(bufnr: number) {
let buf = handler.getItem(bufnr)
return new Promise<void>((resolve, reject) => {
let timer = setTimeout(() => {
reject(new Error('not refresh after 1s'))
}, 1000)
buf.onDidRefresh(() => {
clearTimeout(timer)
resolve()
})
})
}
it('should not refresh when languageId not match', async () => {
let doc = await workspace.document
disposables.push(languages.registerInlayHintsProvider([{ language: 'javascript' }], {
provideInlayHints: () => {
let hint = InlayHint.create(Position.create(0, 0), 'foo')
return [hint]
}
}))
await nvim.setLine('foo')
await doc.synchronize()
await helper.wait(30)
let markers = await doc.buffer.getExtMarks(ns, 0, -1, { details: true })
expect(markers.length).toBe(0)
})
it('should refresh on text change', async () => {
let buf = await nvim.buffer
let disposable = await registerProvider('foo')
disposables.push(disposable)
await waitRefresh(buf.id)
await buf.setLines(['a', 'b', 'c'], { start: 0, end: -1 })
await waitRefresh(buf.id)
let markers = await buf.getExtMarks(ns, 0, -1, { details: true })
expect(markers.length).toBe(3)
let item = handler.getItem(buf.id)
await item.renderRange()
expect(item.current.length).toBe(3)
})
it('should refresh on provider dispose', async () => {
let buf = await nvim.buffer
let disposable = await registerProvider('foo bar')
await waitRefresh(buf.id)
disposable.dispose()
let markers = await buf.getExtMarks(ns, 0, -1, { details: true })
expect(markers.length).toBe(0)
let item = handler.getItem(buf.id)
expect(item.current.length).toBe(0)
await item.renderRange()
expect(item.current.length).toBe(0)
})
it('should refresh on scroll', async () => {
let arr = new Array(200)
let content = arr.fill('foo').join('\n')
let buf = await nvim.buffer
let disposable = await registerProvider(content)
disposables.push(disposable)
await waitRefresh(buf.id)
let markers = await buf.getExtMarks(ns, 0, -1, { details: true })
let len = markers.length
await nvim.command('normal! G')
await waitRefresh(buf.id)
await nvim.input('<C-y>')
await waitRefresh(buf.id)
markers = await buf.getExtMarks(ns, 0, -1, { details: true })
expect(markers.length).toBeGreaterThan(len)
})
it('should cancel previous render', async () => {
let buf = await nvim.buffer
let disposable = await registerProvider('foo')
disposables.push(disposable)
await waitRefresh(buf.id)
let item = handler.getItem(buf.id)
await item.renderRange()
await item.renderRange()
expect(item.current.length).toBe(1)
})
})
})

View file

@ -0,0 +1,157 @@
import { Neovim } from '@chemzqm/neovim'
import { Disposable, Range, Position } from 'vscode-languageserver-protocol'
import LinkedEditingHandler from '../../handler/linkedEditing'
import languages from '../../languages'
import workspace from '../../workspace'
import { disposeAll } from '../../util'
import helper from '../helper'
let nvim: Neovim
let handler: LinkedEditingHandler
let disposables: Disposable[] = []
let wordPattern: string | undefined
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
handler = helper.plugin.getHandler().linkedEditingHandler
})
afterAll(async () => {
await helper.shutdown()
})
beforeEach(async () => {
helper.updateConfiguration('coc.preferences.enableLinkedEditing', true)
})
afterEach(async () => {
disposeAll(disposables)
await helper.reset()
})
async function registerProvider(content: string, position: Position): Promise<void> {
let doc = await workspace.document
disposables.push(languages.registerLinkedEditingRangeProvider([{ language: '*' }], {
provideLinkedEditingRanges: (doc, pos) => {
let document = workspace.getDocument(doc.uri)
let range = document.getWordRangeAtPosition(pos)
if (!range) return null
let text = doc.getText(range)
let ranges: Range[] = document.getSymbolRanges(text)
return { ranges, wordPattern }
}
}))
await nvim.setLine(content)
await doc.synchronize()
await handler.enable(doc, position)
}
async function assertMatches(len: number): Promise<void> {
let res = await nvim.call('getmatches') as any[]
res = res.filter(o => o.group === 'CocLinkedEditing')
expect(res.length).toBe(len)
}
describe('LinkedEditing', () => {
it('should active and cancel on cursor moved', async () => {
await registerProvider('foo foo a ', Position.create(0, 0))
await assertMatches(2)
await nvim.command(`normal! $`)
await helper.wait(50)
await assertMatches(0)
})
it('should active when moved to another word', async () => {
await registerProvider('foo foo bar bar bar', Position.create(0, 0))
await nvim.call('cursor', [1, 9])
await helper.wait(50)
await assertMatches(3)
})
it('should active on text change', async () => {
let doc = await workspace.document
await registerProvider('foo foo a ', Position.create(0, 0))
await assertMatches(2)
await nvim.call('cursor', [1, 1])
await nvim.call('nvim_buf_set_text', [doc.bufnr, 0, 0, 0, 0, ['i']])
await doc.synchronize()
let line = await nvim.line
expect(line).toBe('ifoo ifoo a ')
await nvim.call('nvim_buf_set_text', [doc.bufnr, 0, 0, 0, 1, []])
await doc.synchronize()
line = await nvim.line
expect(line).toBe('foo foo a ')
})
it('should cancel when change out of range', async () => {
let doc = await workspace.document
await registerProvider('foo foo bar', Position.create(0, 0))
await assertMatches(2)
await nvim.call('nvim_buf_set_text', [doc.bufnr, 0, 9, 0, 10, ['']])
await doc.synchronize()
await assertMatches(0)
})
it('should cancel on editor change', async () => {
await registerProvider('foo foo a ', Position.create(0, 0))
await nvim.command(`enew`)
await helper.wait(50)
await assertMatches(0)
})
it('should cancel when insert none word character', async () => {
await registerProvider('foo foo a ', Position.create(0, 0))
await nvim.call('cursor', [1, 4])
await nvim.input('i')
await nvim.input('a')
await helper.wait(50)
await assertMatches(2)
await nvim.input('i')
await nvim.input('@')
await helper.wait(50)
await assertMatches(0)
})
it('should cancel when insert not match wordPattern', async () => {
wordPattern = '[A-Z]'
await registerProvider('foo foo a ', Position.create(0, 0))
await nvim.call('cursor', [1, 4])
await nvim.input('i')
await nvim.input('A')
await helper.wait(50)
await assertMatches(2)
await nvim.input('i')
await nvim.input('3')
await helper.wait(50)
await assertMatches(0)
})
it('should cancel request on cursor moved', async () => {
disposables.push(languages.registerLinkedEditingRangeProvider([{ language: '*' }], {
provideLinkedEditingRanges: (doc, pos, token) => {
return new Promise(resolve => {
token.onCancellationRequested(() => {
clearTimeout(timer)
resolve(null)
})
let timer = setTimeout(() => {
let document = workspace.getDocument(doc.uri)
let range = document.getWordRangeAtPosition(pos)
if (!range) return resolve(null)
let text = doc.getText(range)
let ranges: Range[] = document.getSymbolRanges(text)
resolve({ ranges, wordPattern })
}, 1000)
})
}
}))
let doc = await workspace.document
await nvim.setLine('foo foo ')
await doc.synchronize()
await nvim.call('cursor', [1, 2])
await helper.wait(30)
await nvim.call('cursor', [1, 9])
await helper.wait(30)
await assertMatches(0)
})
})

View file

@ -0,0 +1,137 @@
import { Neovim } from '@chemzqm/neovim'
import { Disposable, DocumentLink, Range } from 'vscode-languageserver-protocol'
import LinksHandler from '../../handler/links'
import languages from '../../languages'
import workspace from '../../workspace'
import events from '../../events'
import { disposeAll } from '../../util'
import helper from '../helper'
let nvim: Neovim
let links: LinksHandler
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
links = helper.plugin.getHandler().links
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
disposeAll(disposables)
await helper.reset()
})
describe('Links', () => {
it('should get document links', async () => {
disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], {
provideDocumentLinks: (_doc, _token) => {
return [
DocumentLink.create(Range.create(0, 0, 0, 5), 'test:///foo'),
DocumentLink.create(Range.create(1, 0, 1, 5), 'test:///bar')
]
}
}))
let res = await links.getLinks()
expect(res.length).toBe(2)
})
it('should throw error when link target not resolved', async () => {
disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], {
provideDocumentLinks(_doc, _token) {
return [
DocumentLink.create(Range.create(0, 0, 0, 5))
]
},
resolveDocumentLink(link) {
return link
}
}))
let res = await links.getLinks()
let err
try {
await links.openLink(res[0])
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
it('should open link at current position', async () => {
await nvim.setLine('foo')
await nvim.command('normal! 0')
disposables.push(workspace.registerTextDocumentContentProvider('test', {
provideTextDocumentContent: () => {
return 'test'
}
}))
disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], {
provideDocumentLinks(_doc, _token) {
return [
DocumentLink.create(Range.create(0, 0, 0, 5)),
]
},
resolveDocumentLink(link) {
link.target = 'test:///foo'
return link
}
}))
await links.openCurrentLink()
let bufname = await nvim.call('bufname', '%')
expect(bufname).toBe('test:///foo')
await nvim.call('setline', [1, ['a', 'b', 'c']])
await nvim.call('cursor', [3, 1])
let res = await links.openCurrentLink()
expect(res).toBe(false)
})
it('should return false when current links not found', async () => {
await nvim.setLine('foo')
await nvim.command('normal! 0')
disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], {
provideDocumentLinks(_doc, _token) {
return []
}
}))
let res = await links.openCurrentLink()
expect(res).toBe(false)
})
it('should show tooltip', async () => {
await nvim.setLine('foo')
await nvim.call('cursor', [1, 1])
disposables.push(languages.registerDocumentLinkProvider([{ language: '*' }], {
provideDocumentLinks(_doc, _token) {
let link = DocumentLink.create(Range.create(0, 0, 0, 5))
link.tooltip = 'test'
return [link]
},
resolveDocumentLink(link) {
link.target = 'http://example.com'
return link
}
}))
await links.showTooltip()
let win = await helper.getFloat()
let buf = await win.buffer
let lines = await buf.lines
expect(lines[0]).toMatch('test')
})
it('should enable tooltip on CursorHold', async () => {
let doc = await workspace.document
helper.updateConfiguration('links.tooltip', true)
await nvim.setLine('http://www.baidu.com')
await nvim.call('cursor', [1, 1])
let link = await links.getCurrentLink()
expect(link).toBeDefined()
await events.fire('CursorHold', [doc.bufnr])
let win = await helper.getFloat()
let buf = await win.buffer
let lines = await buf.lines
expect(lines[0]).toMatch('Press')
})
})

View file

@ -0,0 +1,323 @@
import { Neovim } from '@chemzqm/neovim'
import { Disposable, LocationLink, Location, Range, Position, CancellationTokenSource } from 'vscode-languageserver-protocol'
import LocationHandler from '../../handler/locations'
import languages from '../../languages'
import services from '../../services'
import workspace from '../../workspace'
import { disposeAll } from '../../util'
import helper from '../helper'
import { URI } from 'vscode-uri'
let nvim: Neovim
let locations: LocationHandler
let disposables: Disposable[] = []
let currLocations: Location[]
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
Object.assign(workspace.env, {
locationlist: false
})
locations = helper.plugin.getHandler().locations
})
afterAll(async () => {
await helper.shutdown()
})
beforeEach(async () => {
await helper.createDocument()
})
afterEach(async () => {
disposeAll(disposables)
await helper.reset()
})
function createLocation(name: string, sl: number, sc: number, el: number, ec: number): Location {
return Location.create(`test://${name}`, Range.create(sl, sc, el, ec))
}
describe('locations', () => {
describe('no provider', () => {
it('should return null when provider does not exist', async () => {
let doc = (await workspace.document).textDocument
let pos = Position.create(0, 0)
let tokenSource = new CancellationTokenSource()
let token = tokenSource.token
expect(await languages.getDefinition(doc, pos, token)).toBe(null)
expect(await languages.getDefinitionLinks(doc, pos, token)).toBe(null)
expect(await languages.getDeclaration(doc, pos, token)).toBe(null)
expect(await languages.getTypeDefinition(doc, pos, token)).toBe(null)
expect(await languages.getImplementation(doc, pos, token)).toBe(null)
expect(await languages.getReferences(doc, { includeDeclaration: false }, pos, token)).toBe(null)
})
})
describe('reference', () => {
beforeEach(() => {
disposables.push(languages.registerReferencesProvider([{ language: '*' }], {
provideReferences: () => {
return currLocations
}
}))
})
it('should get references', async () => {
currLocations = [createLocation('foo', 0, 0, 0, 0), createLocation('bar', 0, 0, 0, 0)]
let res = await locations.references()
expect(res.length).toBe(2)
})
it('should jump to references', async () => {
currLocations = [createLocation('foo', 0, 0, 0, 0)]
let res = await locations.gotoReferences('edit', true)
expect(res).toBe(true)
let name = await nvim.call('bufname', ['%'])
expect(name).toBe('test://foo')
})
it('should return false when references not found', async () => {
currLocations = []
let res = await locations.gotoReferences('edit', true)
expect(res).toBe(false)
})
})
describe('definition', () => {
beforeEach(() => {
disposables.push(languages.registerDefinitionProvider([{ language: '*' }], {
provideDefinition: () => {
return currLocations
}
}))
})
it('should get definitions', async () => {
currLocations = [createLocation('foo', 0, 0, 0, 0), createLocation('bar', 0, 0, 0, 0)]
let res = await locations.definitions()
expect(res.length).toBe(2)
})
it('should jump to definitions', async () => {
currLocations = [createLocation('foo', 0, 0, 0, 0)]
let res = await locations.gotoDefinition('edit')
expect(res).toBe(true)
let name = await nvim.call('bufname', ['%'])
expect(name).toBe('test://foo')
})
it('should return false when definitions not found', async () => {
currLocations = []
let res = await locations.gotoDefinition('edit')
expect(res).toBe(false)
})
})
describe('declaration', () => {
beforeEach(() => {
disposables.push(languages.registerDeclarationProvider([{ language: '*' }], {
provideDeclaration: () => {
return currLocations
}
}))
})
it('should get declarations', async () => {
currLocations = [createLocation('foo', 0, 0, 0, 0), createLocation('bar', 0, 0, 0, 0)]
let res = await locations.declarations() as Location[]
expect(res.length).toBe(2)
})
it('should jump to declaration', async () => {
currLocations = [createLocation('foo', 0, 0, 0, 0)]
let res = await locations.gotoDeclaration('edit')
expect(res).toBe(true)
let name = await nvim.call('bufname', ['%'])
expect(name).toBe('test://foo')
})
it('should return false when declaration not found', async () => {
currLocations = []
let res = await locations.gotoDeclaration('edit')
expect(res).toBe(false)
})
})
describe('typeDefinition', () => {
beforeEach(() => {
disposables.push(languages.registerTypeDefinitionProvider([{ language: '*' }], {
provideTypeDefinition: () => {
return currLocations
}
}))
})
it('should get type definition', async () => {
currLocations = [createLocation('foo', 0, 0, 0, 0), createLocation('bar', 0, 0, 0, 0)]
let res = await locations.typeDefinitions() as Location[]
expect(res.length).toBe(2)
})
it('should jump to type definition', async () => {
currLocations = [createLocation('foo', 0, 0, 0, 0)]
let res = await locations.gotoTypeDefinition('edit')
expect(res).toBe(true)
let name = await nvim.call('bufname', ['%'])
expect(name).toBe('test://foo')
})
it('should return false when type definition not found', async () => {
currLocations = []
let res = await locations.gotoTypeDefinition('edit')
expect(res).toBe(false)
})
})
describe('implementation', () => {
beforeEach(() => {
disposables.push(languages.registerImplementationProvider([{ language: '*' }], {
provideImplementation: () => {
return currLocations
}
}))
})
it('should get implementations', async () => {
currLocations = [createLocation('foo', 0, 0, 0, 0), createLocation('bar', 0, 0, 0, 0)]
let res = await locations.implementations() as Location[]
expect(res.length).toBe(2)
})
it('should jump to implementation', async () => {
currLocations = [createLocation('foo', 0, 0, 0, 0)]
let res = await locations.gotoImplementation('edit')
expect(res).toBe(true)
let name = await nvim.call('bufname', ['%'])
expect(name).toBe('test://foo')
})
it('should return false when implementation not found', async () => {
currLocations = []
let res = await locations.gotoImplementation('edit')
expect(res).toBe(false)
})
})
describe('getTagList', () => {
it('should return null when cword does not exist', async () => {
let res = await locations.getTagList()
expect(res).toBe(null)
})
it('should return null when provider does not exist', async () => {
await nvim.setLine('foo')
await nvim.command('normal! ^')
let res = await locations.getTagList()
expect(res).toBe(null)
})
it('should return null when result is empty', async () => {
disposables.push(languages.registerDefinitionProvider([{ language: '*' }], {
provideDefinition: () => {
return []
}
}))
await nvim.setLine('foo')
await nvim.command('normal! ^')
let res = await locations.getTagList()
expect(res).toBe(null)
})
it('should return tag definitions', async () => {
disposables.push(languages.registerDefinitionProvider([{ language: '*' }], {
provideDefinition: () => {
return [createLocation('bar', 2, 0, 2, 5), Location.create(URI.file('/foo').toString(), Range.create(1, 0, 1, 5))]
}
}))
await nvim.setLine('foo')
await nvim.command('normal! ^')
let res = await locations.getTagList()
expect(res).toEqual([
{
name: 'foo',
cmd: 'keepjumps 3 | normal 1|',
filename: 'test://bar'
},
{ name: 'foo', cmd: 'keepjumps 2 | normal 1|', filename: '/foo' }
])
})
})
describe('findLocations', () => {
// hook result
let fn
let result: any
beforeAll(() => {
fn = services.sendRequest
services.sendRequest = () => {
return Promise.resolve(result)
}
})
afterAll(() => {
services.sendRequest = fn
})
it('should handle locations from language client', async () => {
result = [createLocation('bar', 2, 0, 2, 5)]
await locations.findLocations('foo', 'mylocation', {}, false)
let res = await nvim.getVar('coc_jump_locations')
expect(res).toEqual([{
uri: 'test://bar',
lnum: 3,
end_lnum: 3,
col: 1,
end_col: 6,
filename: 'test://bar',
text: '',
range: Range.create(2, 0, 2, 5)
}])
})
it('should handle nested locations', async () => {
let location: any = {
location: createLocation('file', 0, 0, 0, 0),
children: [{
location: createLocation('foo', 3, 0, 3, 5),
children: []
}, {
location: createLocation('bar', 4, 0, 4, 5),
children: []
}]
}
result = location
await locations.findLocations('foo', 'mylocation', {}, false)
let res = await nvim.getVar('coc_jump_locations') as any[]
expect(res.length).toBe(3)
})
})
describe('handleLocations', () => {
it('should not throw when location is undefined', async () => {
await locations.handleLocations(null)
})
it('should not throw when locations is empty array', async () => {
await locations.handleLocations([])
})
it('should handle single location', async () => {
await locations.handleLocations(createLocation('single', 0, 0, 0, 0))
let bufname = await nvim.call('bufname', ['%'])
expect(bufname).toBe('test://single')
})
it('should handle location link', async () => {
let link = LocationLink.create('test://link', Range.create(0, 0, 0, 3), Range.create(1, 0, 1, 3))
await locations.handleLocations([link])
let bufname = await nvim.call('bufname', ['%'])
expect(bufname).toBe('test://link')
})
})
})

View file

@ -0,0 +1,467 @@
import { Buffer, Neovim } from '@chemzqm/neovim'
import { CodeAction, CodeActionKind, Disposable, DocumentSymbol, Range, SymbolKind, SymbolTag, TextEdit } from 'vscode-languageserver-protocol'
import events from '../../events'
import Symbols from '../../handler/symbols/index'
import languages from '../../languages'
import { ProviderResult } from '../../provider'
import { disposeAll } from '../../util'
import workspace from '../../workspace'
import helper from '../helper'
import Parser from './parser'
let nvim: Neovim
let symbols: Symbols
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
symbols = helper.plugin.getHandler().symbols
})
beforeEach(() => {
disposables.push(languages.registerDocumentSymbolProvider([{ language: 'javascript' }], {
provideDocumentSymbols: document => {
let content = document.getText()
let showDetail = content.includes('detail')
let parser = new Parser(content, showDetail)
let res: DocumentSymbol[] = parser.parse()
if (res.length) {
res[0].tags = [SymbolTag.Deprecated]
}
return Promise.resolve(res)
}
}))
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
disposeAll(disposables)
disposables = []
await helper.reset()
await nvim.command(`let w:cocViewId = ''`)
})
async function getOutlineBuffer(): Promise<Buffer | undefined> {
let winid = await nvim.call('coc#window#find', ['cocViewId', 'OUTLINE'])
if (winid == -1) return undefined
let bufnr = await nvim.call('winbufnr', [winid])
if (bufnr == -1) return undefined
return nvim.createBuffer(bufnr)
}
describe('symbols outline', () => {
let defaultCode = `class myClass {
fun1() { }
fun2() {}
}`
async function createBuffer(code = defaultCode): Promise<Buffer> {
await helper.edit()
let buf = await nvim.buffer
await nvim.command('setf javascript')
await buf.setLines(code.split('\n'), { start: 0, end: -1, strictIndexing: false })
let doc = await workspace.document
await doc.synchronize()
return buf
}
describe('configuration', () => {
it('should follow cursor', async () => {
await createBuffer()
let curr = await nvim.call('bufnr', ['%'])
await symbols.showOutline(0)
let bufnr = await nvim.call('bufnr', ['%'])
await nvim.command('wincmd p')
await nvim.command('exe 3')
await events.fire('CursorHold', [curr])
await helper.wait(50)
let buf = nvim.createBuffer(bufnr)
let lines = await buf.getLines()
expect(lines.slice(1)).toEqual([
'- c myClass 1', ' m fun1 2', ' m fun2 3'
])
let signs = await buf.getSigns({ group: 'CocTree' })
expect(signs.length).toBe(1)
expect(signs[0]).toEqual({
lnum: 2,
id: 3001,
name: 'CocTreeSelected',
priority: 10,
group: 'CocTree'
})
})
it('should not follow cursor', async () => {
workspace.configurations.updateUserConfig({
'outline.followCursor': false,
})
await createBuffer()
let curr = await nvim.call('bufnr', ['%'])
await symbols.showOutline(0)
let bufnr = await nvim.call('bufnr', ['%'])
await nvim.command('wincmd p')
await nvim.command('exe 3')
await events.fire('CursorHold', [curr])
await helper.wait(50)
let buf = nvim.createBuffer(bufnr)
let signs = await buf.getSigns({ group: 'CocTree' })
expect(signs.length).toBe(0)
})
it('should keep current window', async () => {
workspace.configurations.updateUserConfig({
'outline.keepWindow': true,
})
await createBuffer()
let curr = await nvim.call('bufnr', ['%'])
await symbols.showOutline()
let bufnr = await nvim.call('bufnr', ['%'])
expect(curr).toBe(bufnr)
})
it('should check on buffer switch', async () => {
workspace.configurations.updateUserConfig({
'outline.checkBufferSwitch': true,
})
await createBuffer()
await symbols.showOutline(1)
await helper.edit('unnamed')
await helper.wait(200)
let buf = await getOutlineBuffer()
let lines = await buf.lines
expect(lines[0]).toMatch('Document symbol provider not found')
})
it('should not check on buffer switch', async () => {
workspace.configurations.updateUserConfig({
'outline.checkBufferSwitch': false
})
await helper.wait(30)
await createBuffer()
await symbols.showOutline(1)
await helper.edit('unnamed')
await helper.wait(100)
let buf = await getOutlineBuffer()
let lines = await buf.lines
expect(lines.slice(1)).toEqual([
'- c myClass 1', ' m fun1 2', ' m fun2 3'
])
})
it('should not check on buffer reload', async () => {
workspace.configurations.updateUserConfig({
'outline.checkBufferSwitch': false
})
await symbols.showOutline(1)
await helper.wait(50)
await createBuffer()
await helper.wait(50)
let buf = await getOutlineBuffer()
expect(buf).toBeDefined()
})
it('should sort by position', async () => {
let code = `class myClass {
fun2() { }
fun1() {}
}`
workspace.configurations.updateUserConfig({
'outline.sortBy': 'position',
})
await createBuffer(code)
await symbols.showOutline(1)
let buf = await getOutlineBuffer()
let lines = await buf.lines
expect(lines).toEqual([
'OUTLINE Position', '- c myClass 1', ' m fun2 2', ' m fun1 3'
])
})
it('should sort by name', async () => {
let code = `class myClass {
fun2() {}
fun1() {}
}`
workspace.configurations.updateUserConfig({
'outline.sortBy': 'name',
})
await createBuffer(code)
await symbols.showOutline(1)
let buf = await getOutlineBuffer()
let lines = await buf.lines
expect(lines).toEqual([
'OUTLINE Name', '- c myClass 1', ' m fun1 3', ' m fun2 2'
])
})
it('should change sort method', async () => {
workspace.configurations.updateUserConfig({
'outline.detailAsDescription': false
})
let code = `class detail {
fun2() {}
fun1() {}
}`
await createBuffer(code)
await symbols.showOutline(0)
await helper.wait(30)
await nvim.input('<C-s>')
await helper.waitFloat()
await nvim.input('<esc>')
await helper.wait(30)
await nvim.input('<C-s>')
await helper.waitFloat()
await nvim.input('3')
await helper.waitFor('getline', [1], 'OUTLINE Position')
})
it('should show detail as description', async () => {
workspace.configurations.updateUserConfig({
'outline.detailAsDescription': true
})
let code = `class detail {
fun2() {}
}`
await createBuffer(code)
await symbols.showOutline(1)
let buf = await getOutlineBuffer()
let lines = await buf.lines
expect(lines.slice(1)).toEqual([
'- c detail 1', ' m fun2 () 2'
])
})
})
describe('events', () => {
it('should not close TreeView on buffer reload', async () => {
await createBuffer()
await symbols.showOutline(0)
await nvim.command('edit')
await helper.wait(30)
let winid = await nvim.call('coc#window#find', ['cocViewId', 'OUTLINE'])
expect(winid).toBeGreaterThan(0)
})
it('should dispose on buffer unload', async () => {
await createBuffer()
let curr = await nvim.call('bufnr', ['%'])
await symbols.showOutline(0)
await nvim.command('tabe')
await nvim.command(`bd! ${curr}`)
await helper.wait(30)
let buf = await getOutlineBuffer()
expect(buf).toBeUndefined()
})
it('should check current window on BufEnter', async () => {
await createBuffer()
await symbols.showOutline(1)
let winid = await nvim.call('win_getid', [])
await nvim.command('enew')
await helper.wait(100)
let win = await nvim.window
expect(win.id).toBe(winid)
})
it('should recreated when original window exists', async () => {
await symbols.showOutline(1)
await helper.wait(50)
await createBuffer()
await helper.wait(50)
let buf = await getOutlineBuffer()
expect(buf).toBeDefined()
})
it('should keep old outline when new buffer not attached', async () => {
await createBuffer()
await symbols.showOutline(1)
await nvim.command(`vnew +setl\\ buftype=nofile`)
await helper.wait(50)
let buf = await getOutlineBuffer()
expect(buf).toBeDefined()
let lines = await buf.lines
expect(lines.slice(1)).toEqual([
'- c myClass 1', ' m fun1 2', ' m fun2 3'
])
})
it('should not reload when switch to original buffer', async () => {
await createBuffer()
await symbols.showOutline(0)
let buf = await getOutlineBuffer()
let name = await buf.name
await nvim.command('wincmd p')
await helper.wait(50)
buf = await getOutlineBuffer()
let curr = await buf.name
expect(curr).toBe(name)
})
})
describe('show()', () => {
it('should not throw when document not attached', async () => {
await nvim.command(`edit +setl\\ buftype=nofile t`)
await workspace.document
await symbols.showOutline(1)
})
it('should not throw when provider does not exist', async () => {
await symbols.showOutline(1)
let buf = await getOutlineBuffer()
expect(buf).toBeDefined()
})
it('should not throw when symbols is empty', async () => {
await createBuffer('')
await symbols.showOutline(1)
let buf = await getOutlineBuffer()
expect(buf).toBeDefined()
})
it('should jump to selected symbol', async () => {
await createBuffer()
let bufnr = await nvim.call('bufnr', ['%'])
await symbols.showOutline(0)
await helper.waitFor('getline', [3], ' m fun1 2')
await nvim.command('exe 3')
await nvim.input('<cr>')
await helper.wait(50)
let curr = await nvim.call('bufnr', ['%'])
expect(curr).toBe(bufnr)
let cursor = await nvim.call('coc#cursor#position')
expect(cursor).toEqual([1, 2])
})
it('should update symbols', async () => {
await createBuffer()
let doc = await workspace.document
let bufnr = await nvim.call('bufnr', ['%'])
await symbols.showOutline(1)
await helper.wait(10)
let buf = nvim.createBuffer(bufnr)
let code = 'class foo{}'
await buf.setLines(code.split('\n'), {
start: 0,
end: -1,
strictIndexing: false
})
await doc.synchronize()
buf = await getOutlineBuffer()
await helper.waitFor('eval', [`getbufline(${buf.id},1)[0]`], /No\sresults/)
let lines = await buf.lines
expect(lines).toEqual([
'No results',
'',
'OUTLINE Category'
])
})
it('should show label in description', async () => {
disposables.push(languages.registerDocumentSymbolProvider([{ language: 'vim' }], {
meta: {
label: 'vimlsp'
},
provideDocumentSymbols: _ => {
let res: DocumentSymbol[] = [{
name: 'let',
range: Range.create(0, 0, 0, 3),
kind: SymbolKind.Constant,
selectionRange: Range.create(0, 0, 0, 3),
tags: [SymbolTag.Deprecated]
}]
return Promise.resolve(res)
}
}))
let doc = await helper.createDocument('t.vim')
await nvim.command('setf vim')
let buf = await nvim.buffer
await buf.setLines(['let'], { start: 0, end: -1, strictIndexing: false })
await doc.synchronize()
await symbols.showOutline(0)
await helper.waitFor('getline', [1], 'OUTLINE vimlsp')
})
})
describe('actions', () => {
it('should invoke visual select', async () => {
await createBuffer()
let bufnr = await nvim.call('bufnr', ['%'])
await symbols.showOutline(0)
await helper.waitFor('getline', [3], /fun1/)
await nvim.command('exe 3')
await nvim.input('<tab>')
await helper.waitFloat()
await nvim.input('<cr>')
await helper.waitFor('mode', [], 'v')
let buf = await nvim.buffer
expect(buf.id).toBe(bufnr)
})
it('should invoke selected code action', async () => {
const codeAction = CodeAction.create('my action', CodeActionKind.Refactor)
let uri: string
disposables.push(languages.registerCodeActionProvider([{ language: '*' }], {
provideCodeActions: () => [codeAction],
resolveCodeAction: (action): ProviderResult<CodeAction> => {
action.edit = {
changes: {
[uri]: [TextEdit.del(Range.create(0, 0, 0, 5))]
}
}
return action
}
}, undefined))
await createBuffer()
let bufnr = await nvim.call('bufnr', ['%'])
let doc = workspace.getDocument(bufnr)
uri = doc.uri
await symbols.showOutline(0)
await helper.wait(200)
await nvim.command('exe 3')
await nvim.input('<tab>')
await helper.wait(50)
await nvim.input('<cr>')
await helper.wait(200)
let buf = await nvim.buffer
let lines = await buf.lines
expect(lines[0]).toBe(' myClass {')
})
})
describe('hide()', () => {
it('should hide outline', async () => {
await createBuffer('')
await symbols.showOutline(1)
await helper.wait(50)
await symbols.hideOutline()
let buf = await getOutlineBuffer()
expect(buf).toBeUndefined()
})
it('should not throw when outline does not exist', async () => {
await symbols.hideOutline()
let buf = await getOutlineBuffer()
expect(buf).toBeUndefined()
})
})
describe('dispose', () => {
it('should dispose provider and views', async () => {
await createBuffer('')
let bufnr = await nvim.call('bufnr', ['%'])
await symbols.showOutline(1)
symbols.dispose()
await helper.wait(50)
expect(symbols.hasOutline(bufnr)).toBe(false)
let buf = await getOutlineBuffer()
expect(buf).toBeUndefined()
})
})
})

View file

@ -0,0 +1,118 @@
import { DocumentSymbol, Range, SymbolKind } from 'vscode-languageserver-protocol'
import { TextDocument } from 'vscode-languageserver-textdocument'
/**
* A syntax parser that parse `class` and `method` only.
*/
export default class Parser {
private _curr = 0
private _symbols: DocumentSymbol[] = []
private currSymbol: DocumentSymbol | undefined
private len: number
private textDocument: TextDocument
constructor(private _content: string, private showDetail = false) {
this.len = _content.length
this.textDocument = TextDocument.create('test:///a', 'txt', 1, _content)
}
public parse(): DocumentSymbol[] {
while (this._curr <= this.len - 1) {
this.parseToken()
}
return this._symbols
}
/**
* Parse a symbol, reset currSymbol & _curr
*/
private parseToken(): void {
this.skipSpaces()
if (this.currSymbol) {
let endOffset = this.textDocument.offsetAt(this.currSymbol.range.end)
if (this._curr > endOffset) {
this.currSymbol = undefined
}
}
let remain = this.getLineRemain()
let ms = remain.match(/^(class)\s(\w+)\s\{\s*/)
if (ms) {
// find class
let start = this._curr + 6
let end = start + ms[2].length
let selectionRange = Range.create(this.textDocument.positionAt(start), this.textDocument.positionAt(end))
let endPosition = this.findMatchedIndex(this._curr + ms[0].length)
let range = Range.create(this.textDocument.positionAt(this._curr), this.textDocument.positionAt(endPosition))
let symbolInfo: DocumentSymbol = {
range,
selectionRange,
kind: SymbolKind.Class,
name: ms[2],
children: []
}
if (this.currSymbol && this.currSymbol.children) {
this.currSymbol.children.push(symbolInfo)
} else {
this._symbols.push(symbolInfo)
}
this.currSymbol = symbolInfo
} else if (this.currSymbol && this.currSymbol.kind == SymbolKind.Class) {
let ms = remain.match(/(\w+)\((.*)\)\s*\{/)
if (ms) {
// find method
let start = this._curr
let end = start + ms[1].length
let selectionRange = Range.create(this.textDocument.positionAt(start), this.textDocument.positionAt(end))
let endPosition = this.findMatchedIndex(this._curr + ms[0].length)
let range = Range.create(this.textDocument.positionAt(this._curr), this.textDocument.positionAt(endPosition))
let symbolInfo: DocumentSymbol = {
range,
selectionRange,
kind: SymbolKind.Method,
detail: this.showDetail ? `(${ms[2]})` : undefined,
name: ms[1]
}
if (this.currSymbol && this.currSymbol.children) {
this.currSymbol.children.push(symbolInfo)
} else {
this._symbols.push(symbolInfo)
}
}
}
this._curr = this._curr + remain.length + 1
}
private findMatchedIndex(start: number): number {
let level = 0
for (let i = start; i < this.len; i++) {
let ch = this._content[i]
if (ch == '{') {
level = level + 1
}
if (ch == '}') {
if (level == 0) return i
level = level - 1
}
}
throw new Error(`Can't find matched }`)
}
private getLineRemain(): string {
let chars = ''
for (let i = this._curr; i < this.len; i++) {
let ch = this._content[i]
if (ch == '\n') break
chars = chars + ch
}
return chars
}
private skipSpaces(): void {
for (let i = this._curr; i < this.len; i++) {
let ch = this._content[i]
if (!ch || /\S/.test(ch)) {
this._curr = i
break
}
}
}
}

View file

@ -0,0 +1,677 @@
import { Neovim } from '@chemzqm/neovim'
import { Disposable } from '@chemzqm/neovim/lib/api/Buffer'
import fs from 'fs'
import { Position, Range, TextDocumentEdit, TextEdit, WorkspaceEdit } from 'vscode-languageserver-types'
import { URI } from 'vscode-uri'
import RefactorBuffer, { FileItemDef, fixChangeParams } from '../../handler/refactor/buffer'
import Changes from '../../handler/refactor/changes'
import Refactor from '../../handler/refactor/index'
import languages from '../../languages'
import { DidChangeTextDocumentParams } from '../../types'
import workspace from '../../workspace'
import helper, { createTmpFile } from '../helper'
let nvim: Neovim
let refactor: Refactor
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
refactor = helper.plugin.getHandler().refactor
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
refactor.reset()
await helper.reset()
})
function createEdit(uri: string): WorkspaceEdit {
let edit = TextEdit.insert(Position.create(0, 0), 'a')
let doc = { uri, version: null }
return { documentChanges: [TextDocumentEdit.create(doc, [edit])] }
}
// assert ranges is expected.
async function assertSynchronized(buf: RefactorBuffer) {
let buffer = nvim.createBuffer(buf.bufnr)
let lines = await buffer.lines
let items: { lnum: number, lines: string[] }[] = []
for (let i = 0; i < lines.length; i++) {
let line = lines[i]
if (line.includes('\u3000') && line.length > 1) {
items.push({ lnum: i + 1, lines: [] })
}
}
let curr: { lnum: number, lines: string[] }[] = []
buf.fileItems.forEach(item => {
item.ranges.forEach(r => {
curr.push({ lnum: r.lnum, lines: [] })
})
})
curr.sort((a, b) => a.lnum - b.lnum)
expect(items).toEqual(curr)
}
describe('fixChangeParams', () => {
function createChangeParams(range: Range, text: string, original: string, originalLines: ReadonlyArray<string>): DidChangeTextDocumentParams {
return {
textDocument: {
uri: 'untitled:/1',
version: 1,
},
originalLines,
original,
bufnr: 1,
contentChanges: [{ range, text }]
}
}
it('should fix delete change params', async () => {
let e = createChangeParams(Range.create(0, 4, 2, 4), '', 'x\nfoo\n\u3000bar', [
'\u3000barx',
'foo',
'\u3000bara'
])
e = fixChangeParams(e)
expect(e.original).toBe('\u3000barx\nfoo\n')
expect(e.contentChanges[0].range).toEqual(Range.create(0, 0, 2, 0))
})
it('should fix insert change params', async () => {
let e = createChangeParams(Range.create(0, 4, 0, 4), 'x\nfoo\n\u3000bar', '', [
'\u3000bara'
])
e = fixChangeParams(e)
expect(e.original).toBe('')
let change = e.contentChanges[0]
expect(change.range).toEqual(Range.create(0, 0, 0, 0))
expect(change.text).toBe('\u3000barx\nfoo\n')
})
})
describe('refactor', () => {
describe('checkInsert()', () => {
it('should check inserted ranges', async () => {
let c = new Changes()
expect(c.checkInsert([1])).toBeUndefined()
c.add([{ filepath: __filename, start: 1, lnum: 1, lines: [''] }])
expect(c.checkInsert([2])).toBeUndefined()
})
})
describe('getFileRange()', () => {
it('should throw when range does not exist', async () => {
let uri = URI.file(__filename).toString()
let locations = [{ uri, range: Range.create(0, 0, 0, 6) }]
let buf = await refactor.fromLocations(locations)
let fn = () => {
buf.getFileRange(1)
}
expect(fn).toThrow(Error)
})
it('should find file range', async () => {
let uri = URI.file(__filename).toString()
let locations = [{ uri, range: Range.create(0, 0, 0, 6) }]
let buf = await refactor.fromLocations(locations)
let res = buf.getFileRange(4)
expect(res).toBeDefined()
})
})
describe('getRange()', () => {
it('should get delete range', async () => {
let filename = await createTmpFile('foo\n\nbar\n')
let fileItem: FileItemDef = {
filepath: filename,
ranges: [{ start: 0, end: 1 }, { start: 2, end: 3 }]
}
let buf = await refactor.createRefactorBuffer()
await buf.addFileItems([fileItem])
let res = buf.getFileRange(4)
let r = buf.getDeleteRange(res)
expect(r).toEqual(Range.create(3, 0, 6, 0))
res = buf.getFileRange(7)
r = buf.getDeleteRange(res)
expect(r).toEqual(Range.create(6, 0, 8, 0))
})
it('should get replace range', async () => {
let filename = await createTmpFile('foo\n\nbar\n')
let fileItem: FileItemDef = {
filepath: filename,
ranges: [{ start: 0, end: 1 }, { start: 2, end: 3 }]
}
let buf = await refactor.createRefactorBuffer()
await buf.addFileItems([fileItem])
let res = buf.getFileRange(4)
let r = buf.getReplaceRange(res)
expect(r).toEqual(Range.create(4, 0, 4, 3))
res = buf.getFileRange(7)
r = buf.getReplaceRange(res)
expect(r).toEqual(Range.create(7, 0, 7, 3))
})
})
describe('fromWorkspaceEdit()', () => {
it('should not create from invalid workspaceEdit', async () => {
let res = await refactor.fromWorkspaceEdit(undefined)
expect(res).toBeUndefined()
res = await refactor.fromWorkspaceEdit({ documentChanges: [] })
expect(res).toBeUndefined()
})
it('should create from document changes', async () => {
let edit = createEdit(URI.file(__filename).toString())
let buf = await refactor.fromWorkspaceEdit(edit)
let shown = await buf.valid
expect(shown).toBe(true)
let items = buf.fileItems
expect(items.length).toBe(1)
await nvim.command(`bd! ${buf.bufnr}`)
await helper.wait(30)
let has = refactor.has(buf.bufnr)
expect(has).toBe(false)
})
it('should create from workspaceEdit', async () => {
let changes = {
[URI.file(__filename).toString()]: [{
range: Range.create(0, 0, 0, 6),
newText: ''
}, {
range: Range.create(1, 0, 1, 6),
newText: ''
}, {
range: Range.create(50, 0, 50, 1),
newText: ' '
}, {
range: Range.create(60, 0, 60, 1),
newText: ' '
}]
}
let edit: WorkspaceEdit = { changes }
let buf = await refactor.fromWorkspaceEdit(edit)
let shown = await buf.valid
expect(shown).toBe(true)
let items = buf.fileItems
expect(items.length).toBe(1)
})
})
describe('fromLocations()', () => {
it('should create from locations', async () => {
let uri = URI.file(__filename).toString()
let locations = [{
uri,
range: Range.create(0, 0, 0, 6),
}, {
uri,
range: Range.create(1, 0, 1, 6),
}]
let buf = await refactor.fromLocations(locations)
let shown = await buf.valid
expect(shown).toBe(true)
let items = buf.fileItems
expect(items.length).toBe(1)
})
it('should not create from empty locations', async () => {
let buf = await refactor.fromLocations([])
expect(buf).toBeUndefined()
})
})
describe('onChange()', () => {
async function setup(): Promise<RefactorBuffer> {
let uri = URI.file(__filename).toString()
let locations = [{
uri,
range: Range.create(0, 0, 0, 6),
}, {
uri,
range: Range.create(1, 0, 1, 6),
}, {
uri,
range: Range.create(10, 0, 10, 6),
}]
return await refactor.fromLocations(locations)
}
it('should refresh on empty text change', async () => {
let buf = await setup()
let line = await nvim.call('getline', [4])
let doc = workspace.getDocument(buf.bufnr)
await nvim.call('setline', [4, line])
doc._forceSync()
let srcId = await nvim.createNamespace('coc-refactor')
let markers = await helper.getMarkers(doc.bufnr, srcId)
expect(markers.length).toBe(2)
})
it('should detect range delete and undo', async () => {
let buf = await setup()
let doc = workspace.getDocument(buf.bufnr)
let r = buf.getFileRange(4)
let end = r.lnum + r.lines.length
await nvim.command(`${r.lnum},${end + 1}d`)
await doc.synchronize()
await assertSynchronized(buf)
await nvim.command('undo')
await doc.synchronize()
await assertSynchronized(buf)
})
it('should detect normal delete', async () => {
let buf = await setup()
let doc = workspace.getDocument(buf.bufnr)
let r = buf.getFileRange(4)
await nvim.command(`${r.lnum + 1},${r.lnum + 1}d`)
await doc.synchronize()
await assertSynchronized(buf)
})
it('should detect insert', async () => {
let buf = await setup()
let doc = workspace.getDocument(buf.bufnr)
let buffer = nvim.createBuffer(buf.bufnr)
await buffer.append(['foo'])
await doc.synchronize()
await assertSynchronized(buf)
await buffer.append(['foo', '\u3000'])
await doc.synchronize()
await assertSynchronized(buf)
})
})
describe('onDocumentChange()', () => {
it('should ignore when change after range', async () => {
let doc = await helper.createDocument()
await doc.buffer.append(['foo', 'bar'])
await doc.synchronize()
let buf = await refactor.fromLocations([{ uri: doc.uri, range: Range.create(0, 0, 0, 3) }])
let lines = await nvim.call('getline', [1, '$'])
await doc.buffer.append(['def'])
await doc.synchronize()
let newLines = await nvim.call('getline', [1, '$'])
expect(lines).toEqual(newLines)
await assertSynchronized(buf)
})
it('should adjust when change before range', async () => {
let doc = await helper.createDocument()
await doc.buffer.append(['', '', '', '', 'foo', 'bar'])
await doc.synchronize()
let buf = await refactor.fromLocations([{ uri: doc.uri, range: Range.create(4, 0, 4, 3) }])
await doc.buffer.setLines(['def'], { start: 0, end: 0, strictIndexing: false })
await doc.synchronize()
let fileRange = buf.getFileRange(4)
expect(fileRange.start).toBe(2)
expect(fileRange.lines.length).toBe(6)
await assertSynchronized(buf)
})
it('should remove ranges when lines empty', async () => {
let doc = await helper.createDocument()
await doc.buffer.append(['', '', '', '', 'foo', 'bar'])
await doc.synchronize()
let buf = await refactor.fromLocations([{ uri: doc.uri, range: Range.create(4, 0, 4, 3) }])
await doc.buffer.setLines([], { start: 0, end: -1, strictIndexing: false })
await doc.synchronize()
let lines = await nvim.call('getline', [1, '$'])
expect(lines.length).toBe(3)
let items = buf.fileItems
expect(items.length).toBe(0)
await assertSynchronized(buf)
})
it('should change when liens changed', async () => {
let doc = await helper.createDocument()
await doc.buffer.append(['', '', '', '', 'foo', 'bar'])
await doc.synchronize()
let buf = await refactor.fromLocations([{ uri: doc.uri, range: Range.create(4, 0, 4, 3) }])
await doc.buffer.setLines(['def', 'def'], { start: 5, end: 6, strictIndexing: false })
await doc.synchronize()
let lines = await nvim.call('getline', [1, '$'])
expect(lines[lines.length - 2]).toBe('def')
await assertSynchronized(buf)
})
})
describe('getFileChanges()', () => {
it('should get changes #1', async () => {
await helper.createDocument()
let lines = `
Save current buffer to make changes
\u3000
\u3000
\u3000/a.ts
})
} `
let buf = await refactor.fromLines(lines.split('\n'))
let changes = await buf.getFileChanges()
expect(changes).toEqual([{ lnum: 5, filepath: '/a.ts', lines: [' })', ' } '] }])
})
it('should get changes #2', async () => {
let lines = `
\u3000/a.ts
})
} `
let buf = await refactor.fromLines(lines.split('\n'))
let changes = await buf.getFileChanges()
expect(changes).toEqual([{ lnum: 2, filepath: '/a.ts', lines: [' })', ' } '] }])
})
it('should get changes #3', async () => {
let lines = `
\u3000/a.ts
})
}
\u3000`
let buf = await refactor.fromLines(lines.split('\n'))
let changes = await buf.getFileChanges()
expect(changes).toEqual([{ lnum: 2, filepath: '/a.ts', lines: [' })', ' }'] }])
})
it('should get changes #4', async () => {
let lines = `
\u3000/a.ts
foo
\u3000/b.ts
bar
\u3000`
let buf = await refactor.fromLines(lines.split('\n'))
let changes = await buf.getFileChanges()
expect(changes).toEqual([
{ filepath: '/a.ts', lnum: 2, lines: ['foo'] },
{ filepath: '/b.ts', lnum: 4, lines: ['bar'] }
])
})
})
describe('createRefactorBuffer()', () => {
it('should create refactor buffer', async () => {
let winid = await nvim.call('win_getid')
let buf = await refactor.createRefactorBuffer()
let curr = await nvim.call('win_getid')
expect(curr).toBeGreaterThan(winid)
let valid = await buf.valid
expect(valid).toBe(true)
buf = await refactor.createRefactorBuffer('vim')
valid = await buf.valid
expect(valid).toBe(true)
})
it('should use conceal for line numbers', async () => {
let buf = await refactor.createRefactorBuffer(undefined, true)
let fileItem: FileItemDef = {
filepath: __filename,
ranges: [{ start: 10, end: 11 }, { start: 15, end: 20 }]
}
await buf.addFileItems([fileItem])
let arr = await nvim.call('getmatches') as any[]
arr = arr.filter(o => o.group == 'Conceal')
expect(arr.length).toBeGreaterThan(0)
await buf.addFileItems([{
filepath: __filename,
ranges: [{ start: 1, end: 3 }]
}])
await nvim.command('normal! ggdG')
let doc = workspace.getDocument(buf.bufnr)
await doc.synchronize()
let b = nvim.createBuffer(buf.bufnr)
let res = await b.getVar('line_infos')
expect(res).toEqual({})
})
})
describe('splitOpen()', () => {
async function setup(): Promise<RefactorBuffer> {
let buf = await refactor.createRefactorBuffer()
let fileItem: FileItemDef = {
filepath: __filename,
ranges: [{ start: 10, end: 11 }, { start: 15, end: 20 }]
}
await buf.addFileItems([fileItem])
await nvim.call('cursor', [5, 1])
return buf
}
it('should jump to position by <CR>', async () => {
let buf = await setup()
await buf.splitOpen()
let line = await nvim.eval('line(".")')
let bufname = await nvim.eval('bufname("%")')
expect(bufname).toMatch('refactor.test.ts')
expect(line).toBe(11)
})
it('should jump split window when original window not valid', async () => {
let win = await nvim.window
let buf = await setup()
await nvim.call('nvim_win_close', [win.id, true])
await buf.splitOpen()
let line = await nvim.eval('line(".")')
let bufname = await nvim.eval('bufname("%")')
expect(bufname).toMatch('refactor.test.ts')
expect(line).toBe(11)
})
})
describe('showMenu()', () => {
async function setup(): Promise<RefactorBuffer> {
let buf = await refactor.createRefactorBuffer()
let fileItem: FileItemDef = {
filepath: __filename,
ranges: [{ start: 10, end: 11 }, { start: 15, end: 20 }]
}
await buf.addFileItems([fileItem])
await nvim.call('cursor', [5, 1])
return buf
}
it('should do nothing when cancelled or range not found', async () => {
let buf = await setup()
let p = buf.showMenu()
await helper.wait(50)
await nvim.input('<esc>')
await p
let bufnr = await nvim.call('bufnr', ['%'])
expect(bufnr).toBe(buf.bufnr)
await nvim.call('cursor', [1, 1])
p = buf.showMenu()
await helper.wait(50)
await nvim.input('1')
await p
bufnr = await nvim.call('bufnr', ['%'])
expect(bufnr).toBe(buf.bufnr)
})
it('should open file in new tab', async () => {
let buf = await setup()
await nvim.call('cursor', [4, 1])
let p = buf.showMenu()
await helper.wait(30)
await nvim.input('1')
await p
let nr = await nvim.call('tabpagenr')
expect(nr).toBe(2)
let lnum = await nvim.call('line', ['.'])
expect(lnum).toBe(11)
})
it('should remove current block', async () => {
let buf = await setup()
await nvim.call('cursor', [4, 1])
let p = buf.showMenu()
await helper.wait(30)
await nvim.input('2')
await p
let items = buf.fileItems
expect(items[0].ranges.length).toBe(1)
await assertSynchronized(buf)
})
})
describe('saveRefactor()', () => {
it('should adjust line ranges after change', async () => {
let filename = await createTmpFile('foo\n\nbar\n')
let fileItem: FileItemDef = {
filepath: filename,
ranges: [{ start: 0, end: 1 }, { start: 2, end: 3 }]
}
let buf = await refactor.createRefactorBuffer()
const getRanges = () => {
let items = buf.fileItems
let item = items.find(o => o.filepath == filename)
return item.ranges.map(o => {
return [o.start, o.start + o.lines.length]
})
}
await buf.addFileItems([fileItem, {
filepath: __filename,
ranges: [{ start: 1, end: 5 }]
}])
expect(getRanges()).toEqual([[0, 1], [2, 3]])
nvim.pauseNotification()
nvim.call('setline', [5, ['xyoo']], true)
nvim.command('undojoin', true)
nvim.call('append', [5, ['de']], true)
nvim.command('undojoin', true)
nvim.call('setline', [9, ['b']], true)
await nvim.resumeNotification()
let doc = workspace.getDocument(buf.bufnr)
await doc.synchronize()
let res = await refactor.save(buf.buffer.id)
expect(res).toBe(true)
expect(getRanges()).toEqual([[0, 2], [3, 4]])
let content = fs.readFileSync(filename, 'utf8')
expect(content).toBe('xyoo\nde\n\nb\n')
})
it('should not save when no change made', async () => {
let buf = await refactor.createRefactorBuffer()
let fileItem: FileItemDef = {
filepath: __filename,
ranges: [{ start: 10, end: 11 }, { start: 15, end: 20 }]
}
await buf.addFileItems([fileItem])
let res = await buf.save()
expect(res).toBe(false)
})
it('should sync buffer change to file', async () => {
let doc = await helper.createDocument()
await doc.buffer.replace(['foo', 'bar', 'line'], 0)
await helper.wait(30)
let filename = URI.parse(doc.uri).fsPath
let fileItem: FileItemDef = {
filepath: filename,
ranges: [{ start: 0, end: 2 }]
}
let buf = await refactor.createRefactorBuffer()
await buf.addFileItems([fileItem])
await nvim.call('setline', [5, 'changed'])
let res = await buf.save()
expect(res).toBe(true)
expect(fs.existsSync(filename)).toBe(true)
let content = fs.readFileSync(filename, 'utf8')
let lines = content.split('\n')
expect(lines).toEqual(['changed', 'bar', 'line', ''])
fs.unlinkSync(filename)
})
})
describe('doRefactor', () => {
let disposable: Disposable
afterEach(() => {
if (disposable) disposable.dispose()
disposable = null
})
it('should throw when rename provider not found', async () => {
await helper.createDocument()
let err
try {
await refactor.doRefactor()
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
it('should show message when prepare failed', async () => {
await helper.createDocument()
disposable = languages.registerRenameProvider(['*'], {
prepareRename: () => {
return undefined
},
provideRenameEdits: () => {
return null
}
})
await refactor.doRefactor()
let res = await helper.getCmdline()
expect(res).toMatch(/unable to rename/)
})
it('should show message when returned edits is null', async () => {
await helper.createDocument()
disposable = languages.registerRenameProvider(['*'], {
provideRenameEdits: () => {
return null
}
})
await refactor.doRefactor()
let res = await helper.getCmdline()
expect(res).toMatch(/returns null/)
})
it('should open refactor window when edits is valid', async () => {
let filepath = __filename
disposable = languages.registerRenameProvider(['*'], {
provideRenameEdits: () => {
let changes = {
[URI.file(filepath).toString()]: [{
range: Range.create(0, 0, 0, 6),
newText: ''
}, {
range: Range.create(1, 0, 1, 6),
newText: ''
}]
}
let edit: WorkspaceEdit = { changes }
return edit
}
})
await helper.createDocument(filepath)
let winid = await nvim.call('win_getid')
await refactor.doRefactor()
let currWin = await nvim.call('win_getid')
expect(currWin - winid).toBeGreaterThan(0)
let bufnr = await nvim.call('bufnr', ['%'])
let b = refactor.getBuffer(bufnr)
expect(b).toBeDefined()
})
})
describe('search', () => {
it('should open refactor buffer from search result', async () => {
let escaped = await nvim.call('fnameescape', [__dirname])
await nvim.command(`cd ${escaped}`)
await helper.createDocument()
await refactor.search(['registerRenameProvider'])
let buf = await nvim.buffer
let name = await buf.name
expect(name).toMatch(/__coc_refactor__/)
let lines = await buf.lines
expect(lines[0]).toMatch(/Save current buffer/)
})
})
})

View file

@ -0,0 +1,262 @@
import { Neovim } from '@chemzqm/neovim'
import { Disposable, Position, Range, TextEdit } from 'vscode-languageserver-protocol'
import { TextDocument } from 'vscode-languageserver-textdocument'
import Rename from '../../handler/rename'
import languages from '../../languages'
import { disposeAll } from '../../util'
import helper from '../helper'
let nvim: Neovim
let disposables: Disposable[] = []
let rename: Rename
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
rename = helper.plugin.getHandler().rename
})
function getWordRangeAtPosition(doc: TextDocument, position: Position): Range | null {
let lines = doc.getText().split(/\r?\n/)
let line = lines[position.line]
if (line.length == 0 || position.character >= line.length) return null
if (!/\w/.test(line[position.character])) return null
let start = position.character
let end = position.character + 1
if (!/\w/.test(line[start])) {
return Range.create(position, { line: position.line, character: position.character + 1 })
}
while (start >= 0) {
let ch = line[start - 1]
if (!ch || !/\w/.test(ch)) break
start = start - 1
}
while (end <= line.length) {
let ch = line[end]
if (!ch || !/\w/.test(ch)) break
end = end + 1
}
return Range.create(position.line, start, position.line, end)
}
function getSymbolRanges(textDocument: TextDocument, word: string): Range[] {
let res: Range[] = []
let str = ''
let content = textDocument.getText()
for (let i = 0, l = content.length; i < l; i++) {
let ch = content[i]
if ('-' == ch && str.length == 0) {
continue
}
let isKeyword = /\w/.test(ch)
if (isKeyword) {
str = str + ch
}
if (str.length > 0 && !isKeyword && str == word) {
res.push(Range.create(textDocument.positionAt(i - str.length), textDocument.positionAt(i)))
}
if (!isKeyword) {
str = ''
}
}
return res
}
beforeEach(() => {
disposables.push(languages.registerRenameProvider([{ language: 'javascript' }], {
provideRenameEdits: (doc, position: Position, newName: string) => {
let range = getWordRangeAtPosition(doc, position)
if (range) {
let word = doc.getText(range)
if (word) {
let ranges = getSymbolRanges(doc, word)
return {
changes: {
[doc.uri]: ranges.map(o => TextEdit.replace(o, newName))
}
}
}
}
return undefined
},
prepareRename: (doc, position) => {
let range = getWordRangeAtPosition(doc, position)
return range ? { range, placeholder: doc.getText(range) } : null
}
}))
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
await helper.reset()
disposeAll(disposables)
disposables = []
})
describe('rename handler', () => {
describe('getWordEdit', () => {
it('should not throw when provider not found', async () => {
await helper.edit()
let res = await rename.getWordEdit()
expect(res).toBe(null)
})
it('should return null when prepare failed', async () => {
let doc = await helper.createDocument('t.js')
await nvim.setLine('你')
await doc.synchronize()
let res = await rename.getWordEdit()
expect(res).toBe(null)
})
it('should return workspace edit', async () => {
let doc = await helper.createDocument('t.js')
await nvim.setLine('foo foo')
await doc.synchronize()
let res = await rename.getWordEdit()
expect(res).toBeDefined()
expect(res.changes[doc.uri].length).toBe(2)
})
it('should extract words from buffer', async () => {
let doc = await helper.createDocument('t')
await nvim.setLine('你 你 你')
await doc.synchronize()
let res = await rename.getWordEdit()
expect(res).toBeDefined()
expect(res.changes[doc.uri].length).toBe(3)
})
})
describe('rename', () => {
it('should throw when provider not found', async () => {
await helper.edit()
let err
try {
await rename.rename('foo')
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
it('should return false for invalid position', async () => {
await helper.createDocument('t.js')
let res = await rename.rename('foo')
expect(res).toBe(false)
})
it('should use newName from placeholder', async () => {
await helper.createDocument('t.js')
await nvim.setLine('foo foo foo')
let p = rename.rename()
await helper.wait(50)
await nvim.input('<C-u>')
await helper.wait(10)
await nvim.input('bar')
await nvim.input('<cr>')
let res = await p
expect(res).toBe(true)
})
it('should return false for empty name', async () => {
await helper.createDocument('t.js')
await nvim.setLine('foo foo foo')
let p = rename.rename()
await helper.wait(50)
await nvim.input('<C-u>')
await helper.wait(20)
await nvim.input('<cr>')
let res = await p
expect(res).toBe(false)
})
it('should use newName from range', async () => {
disposables.push(languages.registerRenameProvider([{ language: '*' }], {
provideRenameEdits: (doc, position: Position, newName: string) => {
let range = getWordRangeAtPosition(doc, position)
if (range) {
let word = doc.getText(range)
if (word) {
let ranges = getSymbolRanges(doc, word)
return {
changes: {
[doc.uri]: ranges.map(o => TextEdit.replace(o, newName))
}
}
}
}
return undefined
},
prepareRename: (doc, position) => {
let range = getWordRangeAtPosition(doc, position)
return range ? range : null
}
}))
await helper.createDocument()
await nvim.setLine('foo foo foo')
let p = rename.rename()
await helper.wait(50)
await nvim.input('<C-u>')
await helper.wait(10)
await nvim.input('bar')
await nvim.input('<cr>')
let res = await p
expect(res).toBe(true)
await helper.waitFor('getline', ['.'], 'bar bar bar')
})
it('should use newName from cword', async () => {
disposables.push(languages.registerRenameProvider([{ language: '*' }], {
provideRenameEdits: (doc, position: Position, newName: string) => {
let range = getWordRangeAtPosition(doc, position)
if (range) {
let word = doc.getText(range)
if (word) {
let ranges = getSymbolRanges(doc, word)
return {
changes: {
[doc.uri]: ranges.map(o => TextEdit.replace(o, newName))
}
}
}
}
return undefined
}
}))
await helper.createDocument()
await nvim.setLine('foo foo foo')
let p = rename.rename()
await helper.wait(50)
await nvim.input('<C-u>')
await helper.wait(50)
await nvim.input('bar')
await nvim.input('<cr>')
let res = await p
expect(res).toBe(true)
let line = await nvim.getLine()
expect(line).toBe('bar bar bar')
})
it('should return false when result is empty', async () => {
disposables.push(languages.registerRenameProvider([{ language: '*' }], {
provideRenameEdits: () => {
return null
}
}))
await helper.createDocument()
await nvim.setLine('foo foo foo')
let p = rename.rename()
await helper.wait(50)
await nvim.input('<C-u>')
await helper.wait(10)
await nvim.input('bar')
await nvim.input('<cr>')
let res = await p
expect(res).toBe(false)
})
})
})

View file

@ -0,0 +1,101 @@
import { Neovim } from '@chemzqm/neovim'
import Refactor from '../../handler/refactor'
import Search, { getPathFromArgs } from '../../handler/refactor/search'
import helper from '../helper'
import path from 'path'
let nvim: Neovim
let refactor: Refactor
// use fake rg command
let cmd = path.resolve(__dirname, '../rg')
let cwd = process.cwd()
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
refactor = helper.plugin.getHandler().refactor
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
refactor.reset()
await helper.reset()
})
describe('getPathFromArgs', () => {
it('should get undefined path', async () => {
let res = getPathFromArgs(['a'])
expect(res).toBeUndefined()
res = getPathFromArgs(['a', 'b', '-c'])
expect(res).toBeUndefined()
res = getPathFromArgs(['a', '-b', 'c'])
expect(res).toBeUndefined()
})
})
describe('search', () => {
it('should open refactor window', async () => {
let search = new Search(nvim, cmd)
let buf = await refactor.createRefactorBuffer()
await search.run([], cwd, buf)
await helper.wait(50)
let fileItems = buf.fileItems
expect(fileItems.length).toBe(2)
expect(fileItems[0].ranges.length).toBe(2)
})
it('should abort task', async () => {
let search = new Search(nvim, cmd)
let buf = await refactor.createRefactorBuffer()
let p = search.run(['--sleep', '1000'], cwd, buf)
search.abort()
await p
let fileItems = buf.fileItems
expect(fileItems.length).toBe(0)
})
it('should work with CocAction search', async () => {
await helper.doAction('search', ['CocAction'])
let bufnr = await nvim.call('bufnr', ['%'])
let buf = refactor.getBuffer(bufnr)
expect(buf).toBeDefined()
})
it('should fail on invalid command', async () => {
let search = new Search(nvim, 'rrg')
let buf = await refactor.createRefactorBuffer()
let err
try {
await search.run([], cwd, buf)
} catch (e) {
err = e
}
expect(err).toBeDefined()
let msg = await helper.getCmdline()
expect(msg).toMatch(/Error on command "rrg"/)
})
it('should show empty result when no result found', async () => {
await helper.doAction('search', ['should found ' + ' no result'])
let bufnr = await nvim.call('bufnr', ['%'])
let buf = refactor.getBuffer(bufnr)
expect(buf).toBeDefined()
let buffer = await nvim.buffer
let lines = await buffer.lines
expect(lines[1]).toMatch(/No match found/)
})
it('should use corrent search folder for rg', async () => {
let search = new Search(nvim, 'rg')
await helper.createDocument()
let buf = await refactor.createRefactorBuffer()
await search.run(['-w', 'createRefactorBuffer', 'src/__tests__'], cwd, buf)
let buffer = await nvim.buffer
let lines = await buffer.lines
expect(lines[1].startsWith('Files: ')).toBe(true)
})
})

View file

@ -0,0 +1,144 @@
import { Neovim } from '@chemzqm/neovim'
import { Disposable, Position, Range, TextEdit } from 'vscode-languageserver-protocol'
import SelectionRange from '../../handler/selectionRange'
import languages from '../../languages'
import workspace from '../../workspace'
import window from '../../window'
import { disposeAll } from '../../util'
import helper from '../helper'
let nvim: Neovim
let disposables: Disposable[] = []
let selection: SelectionRange
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
selection = helper.plugin.getHandler().selectionRange
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
await helper.reset()
disposeAll(disposables)
disposables = []
})
describe('selectionRange', () => {
describe('getSelectionRanges()', () => {
it('should throw error when selectionRange provider does not exist', async () => {
let doc = await helper.createDocument()
await doc.synchronize()
let err
try {
await selection.getSelectionRanges()
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
it('should return ranges', async () => {
await helper.createDocument()
disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], {
provideSelectionRanges: _doc => {
return [{
range: Range.create(0, 0, 0, 1)
}]
}
}))
let res = await selection.getSelectionRanges()
expect(res).toBeDefined()
expect(Array.isArray(res)).toBe(true)
})
})
describe('selectRange()', () => {
async function getSelectedRange(): Promise<Range> {
let m = await nvim.mode
expect(m.mode).toBe('v')
let bufnr = await nvim.call('bufnr', ['%'])
await nvim.input('<esc>')
let doc = workspace.getDocument(bufnr)
let res = await window.getSelectedRange('v')
return res
}
it('should select ranges forward', async () => {
let doc = await helper.createDocument()
let called = 0
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar\ntest\n')])
await nvim.call('cursor', [1, 1])
disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], {
provideSelectionRanges: _doc => {
called += 1
let arr = [{
range: Range.create(0, 0, 0, 1)
}, {
range: Range.create(0, 0, 0, 3)
}, {
range: Range.create(0, 0, 1, 3)
}]
return arr
}
}))
await doc.synchronize()
await selection.selectRange('', false)
await selection.selectRange('', true)
expect(called).toBe(1)
let res = await getSelectedRange()
expect(res).toEqual(Range.create(0, 0, 0, 1))
await selection.selectRange('v', true)
expect(called).toBe(2)
res = await getSelectedRange()
expect(res).toEqual(Range.create(0, 0, 0, 3))
await selection.selectRange('v', true)
expect(called).toBe(3)
res = await getSelectedRange()
expect(res).toEqual(Range.create(0, 0, 1, 3))
await selection.selectRange('v', true)
expect(called).toBe(4)
let m = await nvim.mode
expect(m.mode).toBe('n')
})
it('should select ranges backward', async () => {
let doc = await helper.createDocument()
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar\ntest\n')])
await nvim.call('cursor', [1, 1])
disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], {
provideSelectionRanges: _doc => {
let arr = [{
range: Range.create(0, 0, 0, 1)
}, {
range: Range.create(0, 0, 0, 3)
}, {
range: Range.create(0, 0, 1, 3)
}]
return arr
}
}))
await doc.synchronize()
await selection.selectRange('', true)
let mode = await nvim.call('mode')
expect(mode).toBe('v')
await nvim.input('<esc>')
await window.selectRange(Range.create(0, 0, 1, 3))
await nvim.input('<esc>')
await selection.selectRange('v', false)
let r = await getSelectedRange()
expect(r).toEqual(Range.create(0, 0, 0, 3))
await nvim.input('<esc>')
await selection.selectRange('v', false)
r = await getSelectedRange()
expect(r).toEqual(Range.create(0, 0, 0, 1))
await nvim.input('<esc>')
await selection.selectRange('v', false)
mode = await nvim.call('mode')
expect(mode).toBe('n')
})
})
})

View file

@ -0,0 +1,584 @@
import { Buffer, Neovim } from '@chemzqm/neovim'
import fs from 'fs'
import os from 'os'
import path from 'path'
import { Disposable, Range, SemanticTokensLegend } from 'vscode-languageserver-protocol'
import { URI } from 'vscode-uri'
import commandManager from '../../commands'
import SemanticTokens from '../../handler/semanticTokens/index'
import languages from '../../languages'
import { disposeAll } from '../../util'
import window from '../../window'
import workspace from '../../workspace'
import helper, { createTmpFile } from '../helper'
let nvim: Neovim
let ns: number
let disposables: Disposable[] = []
let highlighter: SemanticTokens
let legend: SemanticTokensLegend = {
tokenTypes: [
"comment",
"keyword",
"string",
"number",
"regexp",
"operator",
"namespace",
"type",
"struct",
"class",
"interface",
"enum",
"enumMember",
"typeParameter",
"function",
"method",
"property",
"macro",
"variable",
"parameter",
"angle",
"arithmetic",
"attribute",
"bitwise",
"boolean",
"brace",
"bracket",
"builtinType",
"character",
"colon",
"comma",
"comparison",
"constParameter",
"dot",
"escapeSequence",
"formatSpecifier",
"generic",
"label",
"lifetime",
"logical",
"operator",
"parenthesis",
"punctuation",
"selfKeyword",
"semicolon",
"typeAlias",
"union",
"unresolvedReference"
],
tokenModifiers: [
"documentation",
"declaration",
"definition",
"static",
"abstract",
"deprecated",
"readonly",
"constant",
"controlFlow",
"injected",
"mutable",
"consuming",
"async",
"library",
"public",
"unsafe",
"attribute",
"trait",
"callable",
"intraDocLink"
]
}
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
ns = await nvim.call('coc#highlight#create_namespace', ['semanticTokens'])
highlighter = helper.plugin.getHandler().semanticHighlighter
})
afterAll(async () => {
await helper.shutdown()
})
const defaultResult = {
resultId: '1',
data: [
0, 0, 2, 1, 0,
0, 3, 4, 14, 2,
0, 4, 1, 41, 0,
0, 1, 1, 41, 3,
0, 2, 1, 25, 0,
1, 4, 8, 17, 0,
0, 8, 1, 41, 0,
0, 1, 3, 2, 0,
0, 3, 1, 41, 0,
0, 1, 1, 44, 0,
1, 0, 1, 25, 0,
]
}
function registerRangeProvider(filetype: string, fn: (range: Range) => number[]): Disposable {
return languages.registerDocumentRangeSemanticTokensProvider([{ language: filetype }], {
provideDocumentRangeSemanticTokens: (_, range) => {
return {
data: fn(range)
}
}
}, legend)
}
function registerProvider(): void {
disposables.push(languages.registerDocumentSemanticTokensProvider([{ language: 'rust' }], {
provideDocumentSemanticTokens: () => {
return defaultResult
},
provideDocumentSemanticTokensEdits: (_, previousResultId) => {
if (previousResultId !== '1') return undefined
return {
resultId: '2',
edits: [{
start: 0,
deleteCount: 0,
data: [0, 0, 3, 1, 0]
}]
}
}
}, legend))
}
async function createRustBuffer(): Promise<Buffer> {
helper.updateConfiguration('semanticTokens.filetypes', ['rust'])
registerProvider()
let code = `fn main() {
println!("H");
}`
let buf = await nvim.buffer
await nvim.command('setf rust')
await buf.setLines(code.split('\n'), { start: 0, end: -1, strictIndexing: false })
let doc = await workspace.document
await doc.patchChange()
return buf
}
afterEach(async () => {
helper.updateConfiguration('semanticTokens.filetypes', [])
await helper.reset()
disposeAll(disposables)
})
describe('semanticTokens', () => {
describe('showHighlightInfo()', () => {
it('should show error when buffer not attached', async () => {
await nvim.command('h')
await highlighter.showHighlightInfo()
let line = await helper.getCmdline()
expect(line).toMatch('not attached')
await highlighter.inspectSemanticToken()
})
it('should show message when not enabled', async () => {
await helper.edit('t.txt')
await highlighter.showHighlightInfo()
let buf = await nvim.buffer
let lines = await buf.lines
expect(lines[2]).toMatch('not enabled for current filetype')
})
it('should show semantic tokens info', async () => {
await createRustBuffer()
await highlighter.highlightCurrent()
await commandManager.executeCommand('semanticTokens.checkCurrent')
let buf = await nvim.buffer
let lines = await buf.lines
let content = lines.join('\n')
expect(content).toMatch('Semantic highlight groups used by current buffer')
})
it('should show highlight info for empty legend', async () => {
helper.updateConfiguration('semanticTokens.filetypes', ['*'])
disposables.push(languages.registerDocumentRangeSemanticTokensProvider([{ language: '*' }], {
provideDocumentRangeSemanticTokens: (_, range) => {
return {
data: []
}
}
}, { tokenModifiers: [], tokenTypes: [] }))
await highlighter.showHighlightInfo()
await highlighter.showHighlightInfo()
let buf = await nvim.buffer
let lines = await buf.lines
let content = lines.join('\n')
expect(content).toMatch('No token')
})
})
describe('highlightCurrent()', () => {
it('should refresh highlights', async () => {
await createRustBuffer()
await nvim.command('hi link CocSemDeclarationFunction MoreMsg')
await nvim.command('hi link CocSemDocumentation Statement')
await window.moveTo({ line: 0, character: 4 })
await highlighter.highlightCurrent()
await commandManager.executeCommand('semanticTokens.inspect')
let win = await helper.getFloat()
let buf = await win.buffer
let lines = await buf.lines
let content = lines.join('\n')
expect(content).toMatch('CocSemDeclarationFunction')
await window.moveTo({ line: 1, character: 0 })
await commandManager.executeCommand('semanticTokens.inspect')
win = await helper.getFloat()
expect(win).toBeUndefined()
})
it('should refresh highlights by command', async () => {
await helper.edit()
let err
try {
await commandManager.executeCommand('semanticTokens.refreshCurrent')
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
it('should refresh when buffer visible', async () => {
helper.updateConfiguration('semanticTokens.filetypes', ['rust'])
let code = `fn main() {
println!("H");
}`
let buf = await nvim.buffer
await nvim.command('setf rust')
await buf.setLines(code.split('\n'), { start: 0, end: -1, strictIndexing: false })
await helper.wait(10)
let doc = await workspace.document
await doc.synchronize()
let item = await highlighter.getCurrentItem()
expect(item.enabled).toBe(false)
await nvim.command('edit bar')
registerProvider()
expect(item.enabled).toBe(true)
await helper.wait(20)
await nvim.command(`b ${buf.id}`)
await item.waitRefresh()
expect(item.highlights).toBeDefined()
})
it('should reuse exists tokens when version not changed', async () => {
let doc = await helper.createDocument('t.vim')
await doc.applyEdits([{ range: Range.create(0, 0, 0, 0), newText: 'let' }])
let fn = jest.fn()
helper.updateConfiguration('semanticTokens.filetypes', ['vim'])
disposables.push(languages.registerDocumentSemanticTokensProvider([{ language: 'vim' }], {
provideDocumentSemanticTokens: () => {
fn()
return new Promise(resolve => {
resolve({
resultId: '1',
data: [0, 0, 3, 1, 0]
})
})
}
}, legend))
let item = await highlighter.getCurrentItem()
item.cancel()
await item.doHighlight()
await item.doHighlight()
expect(fn).toBeCalledTimes(1)
})
it('should only highlight limited range on update', async () => {
let doc = await helper.createDocument('t.vim')
let fn = jest.fn()
helper.updateConfiguration('semanticTokens.filetypes', ['vim'])
disposables.push(languages.registerDocumentSemanticTokensProvider([{ language: 'vim' }], {
provideDocumentSemanticTokens: (doc, token) => {
let text = doc.getText()
if (!text.trim()) {
return Promise.resolve({ resultId: '1', data: [] })
}
fn()
let lines = text.split('\n')
let data = [0, 0, 1, 1, 0]
for (let i = 0; i < lines.length; i++) {
data.push(1, 0, 1, 1, 0)
}
return new Promise(resolve => {
token.onCancellationRequested(() => {
clearTimeout(timer)
resolve(undefined)
})
let timer = setTimeout(() => {
resolve({ resultId: '1', data })
}, 50)
})
}
}, legend))
let item = await highlighter.getCurrentItem()
await item.doHighlight()
let newLine = 'l\n'
await doc.applyEdits([{ range: Range.create(0, 0, 0, 0), newText: `${newLine.repeat(2000)}` }])
await item.doHighlight()
await item.waitRefresh()
expect(fn).toBeCalled()
let buf = nvim.createBuffer(doc.bufnr)
let markers = await buf.getExtMarks(ns, 0, -1, { details: true })
let len = markers.length
expect(len).toBeLessThan(400)
await nvim.command('normal! gg')
await helper.wait(50)
await nvim.command('normal! 200G')
await helper.wait(50)
markers = await buf.getExtMarks(ns, 0, -1, { details: true })
expect(markers.length).toBeGreaterThan(len)
})
it('should highlight hidden buffer on shown', async () => {
helper.updateConfiguration('semanticTokens.filetypes', ['rust'])
registerProvider()
let code = 'fn main() {\n println!("H"); \n}'
let filepath = path.join(os.tmpdir(), 'a.rs')
fs.writeFileSync(filepath, code, 'utf8')
let uri = URI.file(filepath).toString()
await workspace.loadFile(uri)
let doc = workspace.getDocument(uri)
let item = highlighter.getItem(doc.bufnr)
let fn = jest.fn()
item.onDidRefresh(() => {
fn()
})
let buf = doc.buffer
await helper.wait(10)
expect(doc.filetype).toBe('rust')
expect(fn).toBeCalledTimes(0)
await nvim.command(`b ${buf.id}`)
await helper.wait(50)
expect(fn).toBeCalledTimes(1)
})
it('should not highlight on shown when document not changed', async () => {
let fn = jest.fn()
let buf = await createRustBuffer()
let item = await highlighter.getCurrentItem()
await item.waitRefresh()
await nvim.command('enew')
item.doHighlight = async () => {
fn()
}
await nvim.command(`b ${buf.id}`)
await helper.wait(100)
expect(fn).toBeCalledTimes(0)
})
})
describe('clear highlights', () => {
it('should clear highlights of current buffer', async () => {
await createRustBuffer()
await highlighter.highlightCurrent()
let buf = await nvim.buffer
let markers = await buf.getExtMarks(ns, 0, -1)
expect(markers.length).toBeGreaterThan(0)
await commandManager.executeCommand('semanticTokens.clearCurrent')
markers = await buf.getExtMarks(ns, 0, -1)
expect(markers.length).toBe(0)
})
it('should clear all highlights', async () => {
await createRustBuffer()
await highlighter.highlightCurrent()
let buf = await nvim.buffer
await commandManager.executeCommand('semanticTokens.clearAll')
let markers = await buf.getExtMarks(ns, 0, -1)
expect(markers.length).toBe(0)
})
})
describe('rangeProvider', () => {
it('should invoke range provider first time when both kinds exist', async () => {
let fn = jest.fn()
disposables.push(registerRangeProvider('rust', () => {
fn()
return []
}))
let buf = await createRustBuffer()
let item = highlighter.getItem(buf.id)
await item.waitRefresh()
await helper.wait(50)
expect(fn).toBeCalled()
})
it('should do range highlight first time', async () => {
helper.updateConfiguration('semanticTokens.filetypes', ['vim'])
let r: Range
disposables.push(registerRangeProvider('vim', range => {
r = range
return [0, 0, 3, 1, 0]
}))
let filepath = await createTmpFile('let')
fs.renameSync(filepath, filepath + '.vim')
let doc = await helper.createDocument(filepath + '.vim')
expect(doc.filetype).toBe('vim')
await helper.waitValue(() => {
return typeof r !== 'undefined'
}, true)
})
it('should do range highlight after cursor moved', async () => {
helper.updateConfiguration('semanticTokens.filetypes', ['vim'])
let doc = await helper.createDocument('t.vim')
let r: Range
expect(doc.filetype).toBe('vim')
await nvim.call('setline', [2, (new Array(200).fill(''))])
await doc.applyEdits([{ range: Range.create(0, 0, 0, 0), newText: 'let' }])
await helper.wait(50)
disposables.push(registerRangeProvider('vim', range => {
r = range
return []
}))
await nvim.command('normal! G')
await helper.wait(100)
expect(r).toBeDefined()
expect(r.end).toEqual({ line: 201, character: 0 })
})
it('should only cancel range highlight request', async () => {
let rangeCancelled = false
disposables.push(languages.registerDocumentRangeSemanticTokensProvider([{ language: 'vim' }], {
provideDocumentRangeSemanticTokens: (_, range, token) => {
return new Promise(resolve => {
token.onCancellationRequested(() => {
clearTimeout(timeout)
rangeCancelled = true
resolve(null)
})
let timeout = setTimeout(() => {
resolve({ data: [] })
}, 500)
})
}
}, legend))
disposables.push(languages.registerDocumentSemanticTokensProvider([{ language: 'vim' }], {
provideDocumentSemanticTokens: (_, token) => {
return new Promise(resolve => {
resolve({
resultId: '1',
data: [0, 0, 3, 1, 0]
})
})
}
}, legend))
let doc = await helper.createDocument('t.vim')
await doc.applyEdits([{ range: Range.create(0, 0, 0, 0), newText: 'let' }])
let item = await highlighter.getCurrentItem()
helper.updateConfiguration('semanticTokens.filetypes', ['vim'])
item.cancel()
let p = item.doHighlight()
await helper.wait(10)
item.cancel(true)
await p
})
})
describe('triggerSemanticTokens', () => {
it('should be disabled by default', async () => {
helper.updateConfiguration('semanticTokens.filetypes', [])
await workspace.document
const curr = await highlighter.getCurrentItem()
expect(curr.enabled).toBe(false)
})
it('should be enabled', async () => {
await createRustBuffer()
const curr = await highlighter.getCurrentItem()
expect(curr.enabled).toBe(true)
})
it('should get legend by API', async () => {
await createRustBuffer()
const doc = await workspace.document
const l = languages.getLegend(doc.textDocument)
expect(l).toEqual(legend)
})
it('should doHighlight', async () => {
await createRustBuffer()
const doc = await workspace.document
await nvim.call('CocAction', 'semanticHighlight')
const highlights = await nvim.call("coc#highlight#get_highlights", [doc.bufnr, 'semanticTokens'])
expect(highlights.length).toBeGreaterThan(0)
expect(highlights[0][0]).toBe('CocSemKeyword')
})
})
describe('delta update', () => {
it('should perform highlight update', async () => {
await createRustBuffer()
let buf = await nvim.buffer
await highlighter.highlightCurrent()
await window.moveTo({ line: 0, character: 0 })
let doc = await workspace.document
await nvim.input('if')
await helper.wait(50)
await doc.synchronize()
let curr = await highlighter.getCurrentItem()
await curr.forceHighlight()
let markers = await buf.getExtMarks(ns, 0, -1, { details: true })
expect(markers.length).toBeGreaterThan(0)
})
})
describe('checkState', () => {
it('should throw for invalid state', async () => {
let doc = await workspace.document
const toThrow = (cb: () => void) => {
expect(cb).toThrow(Error)
}
let item = highlighter.getItem(doc.bufnr)
toThrow(() => {
item.checkState()
})
helper.updateConfiguration('semanticTokens.filetypes', ['*'])
toThrow(() => {
item.checkState()
})
toThrow(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
workspace._env.updateHighlight = false
item.checkState()
})
let enabled = item.enabled
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
workspace._env.updateHighlight = true
expect(enabled).toBe(false)
doc.detach()
toThrow(() => {
item.checkState()
})
})
})
describe('enabled', () => {
it('should check if buffer enabled for semanticTokens', async () => {
let doc = await workspace.document
let item = highlighter.getItem(doc.bufnr)
disposables.push(languages.registerDocumentRangeSemanticTokensProvider([{ language: '*' }], {
provideDocumentRangeSemanticTokens: (_, range) => {
return {
data: []
}
}
}, { tokenModifiers: [], tokenTypes: [] }))
expect(item.enabled).toBe(false)
helper.updateConfiguration('semanticTokens.filetypes', ['vim'])
expect(item.enabled).toBe(false)
helper.updateConfiguration('semanticTokens.filetypes', ['*'])
expect(item.enabled).toBe(true)
doc.detach()
expect(item.enabled).toBe(false)
})
})
})

View file

@ -0,0 +1,369 @@
import { Neovim } from '@chemzqm/neovim'
import { Disposable, ParameterInformation, SignatureInformation } from 'vscode-languageserver-protocol'
import Signature from '../../handler/signature'
import languages from '../../languages'
import { disposeAll } from '../../util'
import workspace from '../../workspace'
import helper from '../helper'
let nvim: Neovim
let signature: Signature
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
signature = helper.plugin.getHandler().signature
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
await helper.reset()
disposeAll(disposables)
disposables = []
})
describe('signatureHelp', () => {
describe('triggerSignatureHelp', () => {
it('should show signature by api', async () => {
disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], {
provideSignatureHelp: (_doc, _position) => {
return {
signatures: [SignatureInformation.create('foo()', 'my signature')],
activeParameter: null,
activeSignature: null
}
}
}, []))
await helper.createDocument()
await nvim.input('foo')
await signature.triggerSignatureHelp()
let win = await helper.getFloat()
expect(win).toBeDefined()
let lines = await helper.getWinLines(win.id)
expect(lines[2]).toMatch('my signature')
})
it('should use 0 when activeParameter is undefined', async () => {
disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], {
provideSignatureHelp: (_doc, _position) => {
return {
signatures: [SignatureInformation.create('foo(a)', 'my signature', { label: 'a' })],
activeParameter: undefined,
activeSignature: null
}
}
}, []))
await helper.createDocument()
await nvim.input('foo')
await signature.triggerSignatureHelp()
let win = await helper.getFloat()
expect(win).toBeDefined()
let highlights = await win.getVar('highlights')
expect(highlights).toBeDefined()
expect(highlights[0].hlGroup).toBe('CocUnderline')
})
it('should trigger by space', async () => {
let promise = new Promise(resolve => {
disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], {
provideSignatureHelp: (_doc, _position) => {
resolve(undefined)
return {
signatures: [SignatureInformation.create('foo()', 'my signature')],
activeParameter: null,
activeSignature: null
}
}
}, [' ']))
})
await helper.createDocument()
await nvim.input('i')
await helper.wait(30)
await nvim.input(' ')
await promise
})
it('should show signature help with param label as string', async () => {
disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], {
provideSignatureHelp: (_doc, _position) => {
return {
signatures: [
SignatureInformation.create('foo()', 'my signature'),
SignatureInformation.create('foo(a, b)', 'my signature', ParameterInformation.create('a', 'description')),
],
activeParameter: 0,
activeSignature: 1
}
}
}, []))
await helper.createDocument()
await nvim.input('foo')
await signature.triggerSignatureHelp()
let win = await helper.getFloat()
expect(win).toBeDefined()
let lines = await helper.getWinLines(win.id)
expect(lines.join('\n')).toMatch(/description/)
})
})
describe('events', () => {
it('should trigger signature help', async () => {
disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], {
provideSignatureHelp: (_doc, _position) => {
return {
signatures: [SignatureInformation.create('foo(x, y)', 'my signature')],
activeParameter: 0,
activeSignature: 0
}
}
}, ['(', ',']))
await helper.createDocument()
await nvim.input('foo')
await nvim.input('(')
await helper.wait(100)
let win = await helper.getFloat()
expect(win).toBeDefined()
let lines = await helper.getWinLines(win.id)
expect(lines[2]).toMatch('my signature')
})
it('should cancel trigger on InsertLeave', async () => {
disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], {
provideSignatureHelp: async (_doc, _position, token) => {
return new Promise(resolve => {
let timer = setTimeout(() => {
resolve({
signatures: [SignatureInformation.create('foo()', 'my signature')],
activeParameter: null,
activeSignature: null
})
}, 1000)
token.onCancellationRequested(() => {
clearTimeout(timer)
resolve(undefined)
})
})
}
}, ['(', ',']))
await helper.createDocument()
await nvim.input('foo')
let p = signature.triggerSignatureHelp()
await helper.wait(10)
await nvim.command('stopinsert')
await nvim.call('feedkeys', [String.fromCharCode(27), 'in'])
let res = await p
expect(res).toBe(false)
})
it('should not close signature on type', async () => {
disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], {
provideSignatureHelp: (_doc, _position) => {
return {
signatures: [SignatureInformation.create('foo()', 'my signature')],
activeParameter: null,
activeSignature: null
}
}
}, ['(', ',']))
await helper.createDocument()
await nvim.input('foo(')
await helper.wait(100)
await nvim.input('bar')
await helper.wait(100)
let win = await helper.getFloat()
expect(win).toBeDefined()
let lines = await helper.getWinLines(win.id)
expect(lines[2]).toMatch('my signature')
})
it('should close signature float when empty signatures returned', async () => {
let empty = false
disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], {
provideSignatureHelp: (_doc, _position) => {
if (empty) return undefined
return {
signatures: [SignatureInformation.create('foo()', 'my signature')],
activeParameter: null,
activeSignature: null
}
}
}, ['(', ',']))
await helper.createDocument()
await nvim.input('foo(')
await helper.wait(100)
let win = await helper.getFloat()
expect(win).toBeDefined()
empty = true
await signature.triggerSignatureHelp()
await helper.wait(50)
let res = await nvim.call('coc#float#valid', [win.id])
expect(res).toBe(0)
})
})
describe('float window', () => {
it('should align signature window to top', async () => {
disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], {
provideSignatureHelp: (_doc, _position) => {
return {
signatures: [SignatureInformation.create('foo()', 'my signature')],
activeParameter: null,
activeSignature: null
}
}
}, ['(', ',']))
await helper.createDocument()
let buf = await nvim.buffer
await buf.setLines(['', '', '', '', ''], { start: 0, end: -1, strictIndexing: true })
await nvim.call('cursor', [5, 1])
await nvim.input('foo(')
await helper.wait(100)
let win = await helper.getFloat()
expect(win).toBeDefined()
let lines = await helper.getWinLines(win.id)
expect(lines[2]).toMatch('my signature')
let res = await nvim.call('GetFloatCursorRelative', [win.id]) as any
expect(res.row).toBeLessThan(0)
})
it('should show parameter docs', async () => {
disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], {
provideSignatureHelp: (_doc, _position) => {
return {
signatures: [SignatureInformation.create('foo(a, b)', 'my signature',
ParameterInformation.create('a', 'foo'),
ParameterInformation.create([7, 8], 'bar'))],
activeParameter: 1,
activeSignature: null
}
}
}, ['(', ',']))
await helper.createDocument()
let buf = await nvim.buffer
await buf.setLines(['', '', '', '', ''], { start: 0, end: -1, strictIndexing: true })
await nvim.call('cursor', [5, 1])
await nvim.input('foo(a,')
await helper.wait(100)
let win = await helper.getFloat()
expect(win).toBeDefined()
let lines = await helper.getWinLines(win.id)
expect(lines.join('\n')).toMatch('bar')
})
})
describe('configurations', () => {
let { configurations } = workspace
afterEach(() => {
configurations.updateUserConfig({
'signature.target': 'float',
'signature.hideOnTextChange': false,
'signature.enable': true,
'signature.triggerSignatureWait': 500
})
})
it('should cancel signature on timeout', async () => {
configurations.updateUserConfig({ 'signature.triggerSignatureWait': 50 })
disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], {
provideSignatureHelp: (_doc, _position, token) => {
return new Promise(resolve => {
token.onCancellationRequested(() => {
clearTimeout(timer)
resolve(undefined)
})
let timer = setTimeout(() => {
resolve({
signatures: [SignatureInformation.create('foo()', 'my signature')],
activeParameter: null,
activeSignature: null
})
}, 200)
})
}
}, ['(', ',']))
await helper.createDocument()
await signature.triggerSignatureHelp()
let win = await helper.getFloat()
expect(win).toBeUndefined()
configurations.updateUserConfig({ 'signature.triggerSignatureWait': 100 })
})
it('should hide signature window on text change', async () => {
configurations.updateUserConfig({ 'signature.hideOnTextChange': true })
disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], {
provideSignatureHelp: (_doc, _position) => {
return {
signatures: [SignatureInformation.create('foo()', 'my signature')],
activeParameter: null,
activeSignature: null
}
}
}, ['(', ',']))
await helper.createDocument()
await nvim.input('ifoo(')
let winid = await helper.waitFloat()
await nvim.input('x')
await helper.wait(100)
let res = await nvim.call('coc#float#valid', [winid])
expect(res).toBe(0)
configurations.updateUserConfig({ 'signature.hideOnTextChange': false })
})
it('should disable signature help trigger', async () => {
configurations.updateUserConfig({ 'signature.enable': false })
disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], {
provideSignatureHelp: (_doc, _position) => {
return {
signatures: [SignatureInformation.create('foo()', 'my signature')],
activeParameter: null,
activeSignature: null
}
}
}, ['(', ',']))
await helper.createDocument()
await nvim.input('foo')
await nvim.input('(')
await helper.wait(100)
let win = await helper.getFloat()
expect(win).toBeUndefined()
})
it('should echo simple signature help', async () => {
let idx = 0
let activeSignature = null
configurations.updateUserConfig({ 'signature.target': 'echo' })
disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], {
provideSignatureHelp: (_doc, _position) => {
return {
signatures: [SignatureInformation.create('foo(a, b)', 'my signature',
ParameterInformation.create('a', 'foo'),
ParameterInformation.create([7, 8], 'bar')),
SignatureInformation.create('a'.repeat(workspace.env.columns + 10))
],
activeParameter: idx,
activeSignature
}
}
}, []))
await helper.createDocument()
await nvim.input('foo(')
await signature.triggerSignatureHelp()
let line = await helper.getCmdline()
expect(line).toMatch('(a, b)')
await nvim.input('a,')
idx = 1
await signature.triggerSignatureHelp()
line = await helper.getCmdline()
expect(line).toMatch('foo(a, b)')
activeSignature = 1
await signature.triggerSignatureHelp()
line = await helper.getCmdline()
expect(line).toMatch('aaaaaa')
})
})
})

View file

@ -0,0 +1,280 @@
import { Buffer, Neovim } from '@chemzqm/neovim'
import { Disposable, SymbolInformation, SymbolKind, Range } from 'vscode-languageserver-protocol'
import Symbols from '../../handler/symbols/index'
import languages from '../../languages'
import workspace from '../../workspace'
import window from '../../window'
import events from '../../events'
import { disposeAll } from '../../util'
import helper from '../helper'
import Parser from './parser'
let nvim: Neovim
let symbols: Symbols
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
symbols = helper.plugin.getHandler().symbols
})
beforeEach(() => {
disposables.push(languages.registerDocumentSymbolProvider([{ language: 'javascript' }], {
provideDocumentSymbols: document => {
let text = document.getText()
let parser = new Parser(text, text.includes('detail'))
let res = parser.parse()
return Promise.resolve(res)
}
}))
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
disposeAll(disposables)
disposables = []
await helper.reset()
})
describe('Parser', () => {
it('should parse content', async () => {
let code = `class myClass {
fun1() { }
}`
let parser = new Parser(code)
let res = parser.parse()
expect(res.length).toBeGreaterThan(0)
})
})
describe('symbols handler', () => {
async function createBuffer(code: string): Promise<Buffer> {
let buf = await nvim.buffer
await nvim.command('setf javascript')
await buf.setLines(code.split('\n'), { start: 0, end: -1, strictIndexing: false })
let doc = await workspace.document
doc.forceSync()
return buf
}
describe('configuration', () => {
it('should get configuration', async () => {
let functionUpdate = symbols.functionUpdate
expect(functionUpdate).toBe(false)
helper.updateConfiguration('coc.preferences.currentFunctionSymbolAutoUpdate', true)
functionUpdate = symbols.functionUpdate
expect(functionUpdate).toBe(true)
})
it('should update symbols automatically', async () => {
helper.updateConfiguration('coc.preferences.currentFunctionSymbolAutoUpdate', true)
let code = `class myClass {
fun1() {
}
}`
let buf = await createBuffer(code)
await nvim.call('cursor', [2, 8])
await events.fire('CursorHold', [buf.id])
let val = await buf.getVar('coc_current_function')
expect(val).toBe('fun1')
await nvim.call('cursor', [1, 8])
await events.fire('CursorHold', [buf.id])
val = await buf.getVar('coc_current_function')
expect(val).toBe('myClass')
})
})
describe('documentSymbols', () => {
it('should get symbols of current buffer', async () => {
let code = `class detail {
fun1() { }
}`
await createBuffer(code)
let res = await helper.plugin.cocAction('documentSymbols')
expect(res.length).toBe(2)
expect(res[1].detail).toBeDefined()
})
it('should get current function symbols', async () => {
let code = `class myClass {
fun1() {
}
fun2() {
}
}
`
await createBuffer(code)
await nvim.call('cursor', [3, 0])
let res = await helper.doAction('getCurrentFunctionSymbol')
expect(res).toBe('fun1')
await nvim.command('normal! G')
res = await helper.doAction('getCurrentFunctionSymbol')
expect(res).toBe('')
})
it('should reset coc_current_function when symbols do not exist', async () => {
let code = `class myClass {
fun1() {
}
}`
await createBuffer(code)
await nvim.call('cursor', [3, 0])
let res = await helper.doAction('getCurrentFunctionSymbol')
expect(res).toBe('fun1')
await nvim.command('normal! ggdG')
res = await symbols.getCurrentFunctionSymbol()
expect(res).toBe('')
})
it('should support SymbolInformation', async () => {
disposables.push(languages.registerDocumentSymbolProvider(['*'], {
provideDocumentSymbols: () => {
return [
SymbolInformation.create('root', SymbolKind.Function, Range.create(0, 0, 0, 10)),
SymbolInformation.create('child', SymbolKind.Function, Range.create(0, 0, 0, 10), '', 'root')
]
}
}))
await helper.createDocument()
let res = await symbols.getDocumentSymbols()
expect(res.length).toBe(2)
expect(res[0].text).toBe('root')
expect(res[1].text).toBe('child')
})
})
describe('selectSymbolRange', () => {
it('should show warning when no symbols exist', async () => {
disposables.push(languages.registerDocumentSymbolProvider(['*'], {
provideDocumentSymbols: () => {
return []
}
}))
await helper.createDocument()
await nvim.call('cursor', [3, 0])
await symbols.selectSymbolRange(false, '', ['Function'])
let msg = await helper.getCmdline()
expect(msg).toMatch(/No symbols found/)
})
it('should select symbol range at cursor position', async () => {
let code = `class myClass {
fun1() {
}
}`
await createBuffer(code)
await nvim.call('cursor', [3, 0])
await helper.doAction('selectSymbolRange', false, '', ['Function', 'Method'])
let mode = await nvim.mode
expect(mode.mode).toBe('v')
await nvim.input('<esc>')
let res = await window.getSelectedRange('v')
expect(res).toEqual({ start: { line: 1, character: 6 }, end: { line: 2, character: 6 } })
})
it('should select inner range', async () => {
let code = `class myClass {
fun1() {
let foo;
}
}`
let buf = await createBuffer(code)
await nvim.call('cursor', [3, 3])
await symbols.selectSymbolRange(true, '', ['Method'])
let mode = await nvim.mode
expect(mode.mode).toBe('v')
await nvim.input('<esc>')
let res = await window.getSelectedRange('v')
expect(res).toEqual({
start: { line: 2, character: 8 }, end: { line: 2, character: 16 }
})
})
it('should reset visualmode when selection not found', async () => {
let code = `class myClass {}`
await createBuffer(code)
await nvim.call('cursor', [1, 1])
await nvim.command('normal! gg0v$')
let mode = await nvim.mode
expect(mode.mode).toBe('v')
await nvim.input('<esc>')
await symbols.selectSymbolRange(true, 'v', ['Method'])
mode = await nvim.mode
expect(mode.mode).toBe('v')
})
it('should select symbol range from select range', async () => {
let code = `class myClass {
fun1() {
}
}`
let buf = await createBuffer(code)
await nvim.call('cursor', [2, 8])
await nvim.command('normal! viw')
await nvim.input('<esc>')
await helper.doAction('selectSymbolRange', false, 'v', ['Class'])
let mode = await nvim.mode
expect(mode.mode).toBe('v')
let doc = workspace.getDocument(buf.id)
await nvim.input('<esc>')
let res = await window.getSelectedRange('v')
expect(res).toEqual({ start: { line: 0, character: 0 }, end: { line: 3, character: 4 } })
})
})
describe('cancel', () => {
it('should cancel symbols request on insert', async () => {
let cancelled = false
disposables.push(languages.registerDocumentSymbolProvider([{ language: 'text' }], {
provideDocumentSymbols: (_doc, token) => {
return new Promise(s => {
token.onCancellationRequested(() => {
if (timer) clearTimeout(timer)
cancelled = true
s(undefined)
})
let timer = setTimeout(() => {
s(undefined)
}, 3000)
})
}
}))
let doc = await helper.createDocument('t.txt')
let p = symbols.getDocumentSymbols(doc.bufnr)
setTimeout(async () => {
await nvim.input('i')
}, 500)
await p
expect(cancelled).toBe(true)
})
})
describe('workspaceSymbols', () => {
it('should get workspace symbols', async () => {
disposables.push(languages.registerWorkspaceSymbolProvider({
provideWorkspaceSymbols: (_query, _token) => {
return [SymbolInformation.create('far', SymbolKind.Class, Range.create(0, 0, 0, 0))]
},
resolveWorkspaceSymbol: sym => {
let res = Object.assign({}, sym)
res.location.uri = 'test:///foo'
return res
}
}))
disposables.push(languages.registerWorkspaceSymbolProvider({
provideWorkspaceSymbols: (_query, _token) => {
return [SymbolInformation.create('bar', SymbolKind.Function, Range.create(0, 0, 0, 0))]
}
}))
let res = await symbols.getWorkspaceSymbols('a')
expect(res.length).toBe(2)
let resolved = await symbols.resolveWorkspaceSymbol(res[0])
expect(resolved?.location?.uri).toBe('test:///foo')
})
})
})

View file

@ -0,0 +1,99 @@
import { Neovim } from '@chemzqm/neovim'
import { Disposable } from 'vscode-languageserver-protocol'
import WorkspaceHandler from '../../handler/workspace'
import { disposeAll } from '../../util'
import workspace from '../../workspace'
import extensions from '../../extensions'
import helper from '../helper'
let nvim: Neovim
let handler: WorkspaceHandler
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
handler = helper.plugin.getHandler().workspace
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
disposeAll(disposables)
await helper.reset()
})
describe('Workspace handler', () => {
describe('methods', () => {
it('should open log', async () => {
await handler.openLog()
let bufname = await nvim.call('bufname', ['%']) as string
expect(bufname.endsWith('coc-nvim.log')).toBe(true)
})
it('should get configuration of current document', async () => {
let config = await handler.getConfiguration('suggest')
let wait = config.get<number>('triggerCompletionWait')
expect(wait).toBe(0)
})
it('should get root patterns', async () => {
let doc = await helper.createDocument()
let patterns = handler.getRootPatterns(doc.bufnr)
expect(patterns).toBeDefined()
})
})
describe('doKeymap()', () => {
it('should return default value when key mapping does not exist', async () => {
let res = await handler.doKeymap('not_exists', '', '<C-a')
expect(res).toBe('')
})
it('should support repeat key mapping', async () => {
let called = false
await nvim.command('nmap do <Plug>(coc-test)')
disposables.push(workspace.registerKeymap(['n'], 'test', () => {
called = true
}, { repeat: true, silent: true, sync: false }))
await helper.wait(100)
await nvim.call('feedkeys', ['do', 'i'])
await helper.wait(30)
expect(called).toBe(true)
})
})
describe('snippetCheck()', () => {
it('should return false when coc-snippets not found', async () => {
expect(await handler.snippetCheck(true, false)).toBe(false)
})
it('should check jump', async () => {
expect(await handler.snippetCheck(false, true)).toBe(false)
})
it('should check expand by coc-snippets', async () => {
let has = extensions.has
let getExtensionApi = extensions.getExtensionApi
extensions.has = () => {
return true
}
extensions.getExtensionApi = () => {
return {
expandable: () => {
return true
}
}
}
disposables.push({
dispose: () => {
extensions.has = has
extensions.getExtensionApi = getExtensionApi
}
})
let res = await handler.snippetCheck(true, false)
expect(res).toBe(true)
})
})
})

View file

@ -0,0 +1,22 @@
import { Neovim } from '@chemzqm/neovim'
import Plugin from '../plugin'
import helper from './helper'
let nvim: Neovim
let plugin: Plugin
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
plugin = helper.plugin
})
describe('Helper', () => {
it('should setup', () => {
expect(nvim).toBeTruthy()
expect(plugin.isReady).toBeTruthy()
})
})
afterAll(async () => {
await helper.shutdown()
})

View file

@ -0,0 +1,360 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { Buffer, Neovim, Window } from '@chemzqm/neovim'
import * as cp from 'child_process'
import { EventEmitter } from 'events'
import fs from 'fs'
import os from 'os'
import path from 'path'
import util from 'util'
import { v4 as uuid } from 'uuid'
import { Disposable } from 'vscode-languageserver-protocol'
import attach from '../attach'
import completion from '../completion'
import events from '../events'
import Document from '../model/document'
import Plugin from '../plugin'
import { OutputChannel, VimCompleteItem } from '../types'
import { terminate } from '../util/processes'
import workspace from '../workspace'
export interface CursorPosition {
bufnum: number
lnum: number
col: number
}
const nullChannel: OutputChannel = {
content: '',
show: () => {},
dispose: () => {},
name: 'null',
append: () => {},
appendLine: () => {},
clear: () => {},
hide: () => {}
}
process.on('uncaughtException', err => {
let msg = 'Uncaught exception: ' + err.stack
console.error(msg)
})
export class Helper extends EventEmitter {
public nvim: Neovim
public proc: cp.ChildProcess
public plugin: Plugin
constructor() {
super()
this.setMaxListeners(99)
}
public setupNvim(): void {
const vimrc = path.resolve(__dirname, 'vimrc')
let proc = this.proc = cp.spawn(process.env.COC_TEST_NVIM ?? 'nvim', ['-u', vimrc, '-i', 'NONE', '--embed'], {
cwd: __dirname
})
let plugin = attach({ proc })
this.nvim = plugin.nvim
}
public setup(): Promise<void> {
const vimrc = path.resolve(__dirname, 'vimrc')
let proc = this.proc = cp.spawn('nvim', ['-u', vimrc, '-i', 'NONE', '--embed'], {
cwd: __dirname
})
let plugin = this.plugin = attach({ proc })
this.nvim = plugin.nvim
this.nvim.uiAttach(160, 80, {}).catch(e => {
console.error(e)
})
this.nvim.on('notification', (method, args) => {
if (method == 'redraw') {
for (let arg of args) {
let event = arg[0]
this.emit(event, arg.slice(1))
if (event == 'put') {
let arr = arg.slice(1).map(o => o[0])
let line = arr.join('').trim()
if (line.length > 3) {
// console.log(line)
}
}
}
}
})
return new Promise(resolve => {
plugin.once('ready', resolve)
})
}
public async shutdown(): Promise<void> {
if (this.plugin) this.plugin.dispose()
this.nvim.removeAllListeners()
this.nvim = null
if (this.proc) {
this.proc.unref()
terminate(this.proc)
this.proc = null
}
await this.wait(60)
}
public async waitPopup(): Promise<void> {
let visible = await this.nvim.call('pumvisible')
if (visible) return
let res = await events.race(['MenuPopupChanged'], 5000)
if (!res) throw new Error('wait pum timeout after 5s')
}
public async waitPreviewWindow(): Promise<void> {
for (let i = 0; i < 40; i++) {
await this.wait(50)
let has = await this.nvim.call('coc#list#has_preview')
if (has > 0) return
}
throw new Error('timeout after 2s')
}
public async waitPrompt(): Promise<void> {
for (let i = 0; i < 40; i++) {
await this.wait(50)
let prompt = await this.nvim.call('coc#prompt#activated')
if (prompt) return
}
throw new Error('Wait prompt timeout after 2s')
}
public async waitFloat(): Promise<number> {
for (let i = 0; i < 50; i++) {
await this.wait(20)
let winid = await this.nvim.call('GetFloatWin')
if (winid) return winid
}
throw new Error('timeout after 2s')
}
public async selectCompleteItem(idx: number): Promise<void> {
await this.nvim.call('nvim_select_popupmenu_item', [idx, true, true, {}])
}
public async doAction(method: string, ...args: any[]): Promise<any> {
return await this.plugin.cocAction(method, ...args)
}
public async synchronize(): Promise<void> {
let doc = await workspace.document
doc.forceSync()
}
public async reset(): Promise<void> {
let mode = await this.nvim.mode
if (mode.blocking && mode.mode == 'r') {
await this.nvim.input('<cr>')
} else if (mode.mode != 'n' || mode.blocking) {
await this.nvim.call('feedkeys', [String.fromCharCode(27), 'in'])
}
completion.stop()
workspace.reset()
await this.nvim.command('silent! %bwipeout!')
await this.nvim.command('setl nopreviewwindow')
await this.wait(30)
await workspace.document
}
public async pumvisible(): Promise<boolean> {
let res = await this.nvim.call('pumvisible', []) as number
return res == 1
}
public wait(ms = 30): Promise<void> {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, ms)
})
}
public async visible(word: string, source?: string): Promise<boolean> {
await this.waitPopup()
let context = await this.nvim.getVar('coc#_context') as any
let items = context.candidates
if (!items) return false
let item = items.find(o => o.word == word)
if (!item || !item.user_data) return false
try {
let arr = item.user_data.split(':', 2)
if (source && arr[0] !== source) {
return false
}
} catch (e) {
return false
}
return true
}
public async notVisible(word: string): Promise<boolean> {
let items = await this.getItems()
return items.findIndex(o => o.word == word) == -1
}
public async getItems(): Promise<VimCompleteItem[]> {
let visible = await this.pumvisible()
if (!visible) return []
let context = await this.nvim.getVar('coc#_context') as any
let items = context.candidates
return items || []
}
public async edit(file?: string): Promise<Buffer> {
if (!file || !path.isAbsolute(file)) {
file = path.join(__dirname, file ? file : `${uuid()}`)
}
let escaped = await this.nvim.call('fnameescape', file) as string
await this.nvim.command(`edit ${escaped}`)
let doc = await workspace.document
return doc.buffer
}
public async createDocument(name?: string): Promise<Document> {
let buf = await this.edit(name)
let doc = workspace.getDocument(buf.id)
if (!doc) return await workspace.document
return doc
}
public async listInput(input: string): Promise<void> {
await events.fire('InputChar', ['list', input, 0])
}
public async getMarkers(bufnr: number, ns: number): Promise<[number, number, number][]> {
return await this.nvim.call('nvim_buf_get_extmarks', [bufnr, ns, 0, -1, {}]) as [number, number, number][]
}
public async getCmdline(): Promise<string> {
let str = ''
for (let i = 1, l = 70; i < l; i++) {
let ch = await this.nvim.call('screenchar', [79, i])
if (ch == -1) break
str += String.fromCharCode(ch)
}
return str.trim()
}
public updateConfiguration(key: string, value: any): () => void {
let { configurations } = workspace
let curr = workspace.getConfiguration(key)
configurations.updateUserConfig({ [key]: value })
return () => {
configurations.updateUserConfig({ [key]: curr })
}
}
public async mockFunction(name: string, result: string | number | any): Promise<void> {
let content = `
function! ${name}(...)
return ${typeof result == 'number' ? result : JSON.stringify(result)}
endfunction`
await this.nvim.exec(content)
}
public async items(): Promise<VimCompleteItem[]> {
let context = await this.nvim.getVar('coc#_context')
return context['candidates'] || []
}
public async screenLine(line: number): Promise<string> {
let res = ''
for (let i = 1; i <= 80; i++) {
let ch = await this.nvim.call('screenchar', [line, i])
res = res + String.fromCharCode(ch)
}
return res
}
public async getWinLines(winid: number): Promise<string[]> {
return await this.nvim.eval(`getbufline(winbufnr(${winid}), 1, '$')`) as string[]
}
public async getFloat(): Promise<Window> {
let wins = await this.nvim.windows
let floatWin: Window
for (let win of wins) {
let f = await win.getVar('float')
if (f) floatWin = win
}
return floatWin
}
public async getFloats(): Promise<Window[]> {
let ids = await this.nvim.call('coc#float#get_float_win_list', [])
if (!ids) return []
return ids.map(id => this.nvim.createWindow(id))
}
public async getExtmarkers(bufnr: number, ns: number): Promise<[number, number, number, number, string][]> {
let res = await this.nvim.call('nvim_buf_get_extmarks', [bufnr, ns, 0, -1, { details: true }]) as any
return res.map(o => {
return [o[1], o[2], o[3].end_row, o[3].end_col, o[3].hl_group]
})
}
public async waitFor<T>(method: string, args: any[], value: T): Promise<void> {
let find = false
for (let i = 0; i < 40; i++) {
await this.wait(50)
let res = await this.nvim.call(method, args) as T
if (res == value || (value instanceof RegExp && value.test(res.toString()))) {
find = true
break
}
}
if (!find) {
throw new Error(`waitFor ${value} timeout`)
}
}
public async waitValue<T>(fn: () => T, value: T): Promise<void> {
let find = false
for (let i = 0; i < 40; i++) {
await this.wait(50)
let res = fn()
if (res == value) {
find = true
break
}
}
if (!find) {
throw new Error(`waitValue ${value} timeout`)
}
}
public createNullChannel(): OutputChannel {
return nullChannel
}
}
export function rmdir(dir: string): void {
if (typeof fs['rm'] === 'function') {
fs['rmSync'](dir, { recursive: true })
} else {
fs.rmdirSync(dir, { recursive: true })
}
}
export async function createTmpFile(content: string, disposables?: Disposable[]): Promise<string> {
let tmpFolder = path.join(os.tmpdir(), `coc-${process.pid}`)
if (!fs.existsSync(tmpFolder)) {
fs.mkdirSync(tmpFolder)
}
let fsPath = path.join(tmpFolder, uuid())
await util.promisify(fs.writeFile)(fsPath, content, 'utf8')
if (disposables) {
disposables.push(Disposable.create(() => {
if (fs.existsSync(fsPath)) fs.unlinkSync(fsPath)
}))
}
return fsPath
}
export default new Helper()

View file

@ -0,0 +1,136 @@
import { Neovim } from '@chemzqm/neovim'
import path from 'path'
import { ListContext, ListTask } from '../../types'
import manager from '../../list/manager'
import helper, { createTmpFile } from '../helper'
import BasicList from '../../list/basic'
import { Disposable } from 'vscode-languageserver-protocol'
import { disposeAll } from '../../util'
class DataList extends BasicList {
public name = 'data'
public async loadItems(_context: ListContext): Promise<ListTask> {
let fsPath = await createTmpFile(`console.log('foo');console.log('');console.log('bar');`)
return this.createCommandTask({
cmd: 'node',
args: [fsPath],
cwd: path.dirname(fsPath),
onLine: line => {
if (!line) return undefined
return {
label: line
}
}
})
}
}
class SleepList extends BasicList {
public name = 'sleep'
public loadItems(_context: ListContext): Promise<ListTask> {
return Promise.resolve(this.createCommandTask({
cmd: 'sleep',
args: ['10'],
onLine: line => {
return {
label: line
}
}
}))
}
}
class StderrList extends BasicList {
public name = 'stderr'
public async loadItems(_context: ListContext): Promise<ListTask> {
let fsPath = await createTmpFile(`console.error('stderr');console.log('stdout')`)
return Promise.resolve(this.createCommandTask({
cmd: 'node',
args: [fsPath],
cwd: path.dirname(fsPath),
onLine: line => {
return {
label: line
}
}
}))
}
}
class ErrorTask extends BasicList {
public name = 'error'
public async loadItems(_context: ListContext): Promise<ListTask> {
return Promise.resolve(this.createCommandTask({
cmd: 'NOT_EXISTS',
args: [],
cwd: __dirname,
onLine: line => {
return {
label: line
}
}
}))
}
}
let nvim: Neovim
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
disposeAll(disposables)
manager.reset()
await helper.reset()
})
describe('Command task', () => {
it('should not show stderr', async () => {
disposables.push(manager.registerList(new StderrList(nvim)))
await manager.start(['stderr'])
await manager.session.ui.ready
let lines = await nvim.call('getline', [1, '$']) as string[]
expect(lines).toEqual(['stdout'])
})
it('should show error for bad key', async () => {
let list = new DataList(nvim)
list.config.fixKey('<X-a>')
await helper.wait(200)
await nvim.command('redraw')
let msg = await helper.getCmdline()
expect(msg).toMatch('not supported')
})
it('should not show error', async () => {
disposables.push(manager.registerList(new ErrorTask(nvim)))
await manager.start(['error'])
await helper.wait(300)
await nvim.command('redraw')
let len = manager.session.ui.length
expect(len).toBe(0)
})
it('should create command task', async () => {
let list = new DataList(nvim)
disposables.push(manager.registerList(list))
await manager.start(['data'])
await manager.session.ui.ready
await helper.wait(100)
let lines = await nvim.call('getline', [1, '$']) as string[]
expect(lines).toEqual(['foo', 'bar'])
})
it('should stop command task', async () => {
let list = new SleepList(nvim)
disposables.push(manager.registerList(list))
await manager.start(['sleep'])
manager.session.stop()
})
})

View file

@ -0,0 +1,508 @@
import { Neovim } from '@chemzqm/neovim'
import path from 'path'
import manager from '../../list/manager'
import events from '../../events'
import { QuickfixItem, IList, ListItem } from '../../types'
import helper from '../helper'
let nvim: Neovim
const locations: ReadonlyArray<QuickfixItem> = [{
filename: __filename,
col: 2,
lnum: 1,
text: 'foo'
}, {
filename: __filename,
col: 1,
lnum: 2,
text: 'Bar'
}, {
filename: __filename,
col: 1,
lnum: 3,
text: 'option'
}]
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
await nvim.setVar('coc_jump_locations', locations)
})
afterEach(async () => {
manager.reset()
await helper.reset()
})
afterAll(async () => {
await helper.shutdown()
})
describe('list', () => {
describe('events', () => {
it('should cancel and enable prompt', async () => {
let winid = await nvim.call('win_getid')
await manager.start(['location'])
await manager.session.ui.ready
await nvim.call('win_gotoid', [winid])
await helper.wait(50)
let res = await nvim.call('coc#prompt#activated')
expect(res).toBe(0)
await nvim.command('wincmd p')
await helper.wait(50)
res = await nvim.call('coc#prompt#activated')
expect(res).toBe(1)
})
})
describe('list commands', () => {
it('should be activated', async () => {
await manager.start(['location'])
await manager.session.ui.ready
await helper.wait(50)
expect(manager.isActivated).toBe(true)
let line = await nvim.getLine()
expect(line).toMatch(/manager.test.ts/)
})
it('should get list names', () => {
let names = manager.names
expect(names.length > 0).toBe(true)
})
it('should resume list', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await helper.wait(30)
await nvim.eval('feedkeys("j", "in")')
await helper.wait(30)
let line = await nvim.call('line', '.')
expect(line).toBe(2)
await manager.cancel()
await helper.wait(30)
await manager.resume()
await helper.wait(30)
line = await nvim.call('line', '.')
expect(line).toBe(2)
})
it('should not quit list with --no-quit', async () => {
await manager.start(['--normal', '--no-quit', 'location'])
await manager.session.ui.ready
let winnr = await nvim.eval('win_getid()') as number
await manager.doAction()
await helper.wait(100)
let wins = await nvim.windows
let ids = wins.map(o => o.id)
expect(ids).toContain(winnr)
})
it('should do default action for first item', async () => {
await manager.start(['--normal', '--first', 'location'])
await helper.wait(300)
let name = await nvim.eval('bufname("%")') as string
let filename = path.basename(__filename)
expect(name.includes(filename)).toBe(true)
let pos = await nvim.eval('getcurpos()')
expect(pos[1]).toBe(1)
expect(pos[2]).toBe(2)
})
it('should goto next & previous', async () => {
await manager.start(['location'])
await manager.session?.ui.ready
await helper.wait(60)
await manager.doAction()
await manager.cancel()
let bufname = await nvim.eval('expand("%:p")')
expect(bufname).toMatch('manager.test.ts')
await manager.next()
let line = await nvim.call('line', '.')
expect(line).toBe(2)
await helper.wait(60)
await manager.previous()
line = await nvim.call('line', '.')
expect(line).toBe(1)
})
it('should parse arguments', async () => {
await manager.start(['--input=test', '--reverse', '--normal', '--no-sort', '--ignore-case', '--top', '--number-select', '--auto-preview', '--strict', 'location'])
await helper.wait(30)
let opts = manager.session?.listOptions
expect(opts).toEqual({
reverse: true,
numberSelect: true,
autoPreview: true,
first: false,
input: 'test',
interactive: false,
matcher: 'strict',
ignorecase: true,
position: 'top',
mode: 'normal',
noQuit: false,
sort: false
})
})
})
describe('list configuration', () => {
it('should change indicator', async () => {
helper.updateConfiguration('list.indicator', '>>')
await manager.start(['location'])
await manager.session.ui.ready
await helper.wait(200)
let line = await helper.getCmdline()
expect(line).toMatch('>>')
})
it('should split right for preview window', async () => {
helper.updateConfiguration('list.previewSplitRight', true)
let win = await nvim.window
await manager.start(['location'])
await helper.wait(100)
await manager.doAction('preview')
await helper.wait(100)
manager.prompt.cancel()
await helper.wait(10)
await nvim.call('win_gotoid', [win.id])
await nvim.command('wincmd l')
let curr = await nvim.window
let isPreview = await curr.getVar('previewwindow')
expect(isPreview).toBe(1)
})
it('should toggle selection mode', async () => {
await manager.start(['--normal', 'location'])
await manager.session?.ui.ready
await nvim.input('V')
await helper.wait(30)
await nvim.input('1')
await helper.wait(30)
await nvim.input('j')
await helper.wait(100)
await manager.session?.ui.toggleSelection()
let items = await manager.session?.ui.getItems()
expect(items.length).toBe(2)
})
it('should change next/previous keymap', async () => {
helper.updateConfiguration('list.nextKeymap', '<tab>')
helper.updateConfiguration('list.previousKeymap', '<s-tab>')
await manager.start(['location'])
await manager.session.ui.ready
await helper.wait(100)
await nvim.eval('feedkeys("\\<tab>", "in")')
await helper.wait(100)
let line = await nvim.line
expect(line).toMatch('Bar')
await nvim.eval('feedkeys("\\<s-tab>", "in")')
await helper.wait(100)
line = await nvim.line
expect(line).toMatch('foo')
})
it('should respect mouse events', async () => {
async function setMouseEvent(line: number): Promise<void> {
let winid = manager.session?.ui.winid
await nvim.command(`let v:mouse_winid = ${winid}`)
await nvim.command(`let v:mouse_lnum = ${line}`)
await nvim.command(`let v:mouse_col = 1`)
}
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await helper.wait(100)
await setMouseEvent(1)
await manager.onNormalInput('<LeftMouse>')
await setMouseEvent(2)
await manager.onNormalInput('<LeftDrag>')
await setMouseEvent(3)
await manager.onNormalInput('<LeftRelease>')
await helper.wait(100)
let items = await manager.session?.ui.getItems()
expect(items.length).toBe(3)
})
it('should toggle preview', async () => {
await manager.start(['--normal', '--auto-preview', 'location'])
await manager.session.ui.ready
await helper.wait(100)
await manager.togglePreview()
await helper.wait(100)
await manager.togglePreview()
await helper.wait(100)
let has = await nvim.call('coc#list#has_preview')
expect(has).toBeGreaterThan(0)
})
it('should show help of current list', async () => {
await manager.start(['--normal', '--auto-preview', 'location'])
await helper.wait(200)
await manager.session?.showHelp()
let bufname = await nvim.call('bufname', '%')
expect(bufname).toBe('[LIST HELP]')
})
it('should resolve list item', async () => {
let list: IList = {
name: 'test',
actions: [{
name: 'open', execute: _item => {
// noop
}
}],
defaultAction: 'open',
loadItems: () => Promise.resolve([{ label: 'foo' }, { label: 'bar' }]),
resolveItem: item => {
item.label = item.label.slice(0, 1)
return Promise.resolve(item)
}
}
let disposable = manager.registerList(list)
await manager.start(['--normal', 'test'])
await manager.session.ui.ready
await helper.wait(50)
let line = await nvim.line
expect(line).toBe('f')
disposable.dispose()
})
})
describe('descriptions', () => {
it('should get descriptions', async () => {
let res = manager.descriptions
expect(res).toBeDefined()
expect(res.location).toBeDefined()
})
})
describe('loadItems()', () => {
it('should load items for list', async () => {
let res = await manager.loadItems('location')
expect(res.length).toBeGreaterThan(0)
; (manager as any).lastSession = undefined
manager.toggleMode()
manager.stop()
})
})
describe('onInsertInput()', () => {
it('should handle insert input', async () => {
await manager.onInsertInput('k')
await manager.onInsertInput('<LeftMouse>')
await manager.start(['--number-select', 'location'])
await manager.session.ui.ready
await manager.onInsertInput('1')
await helper.wait(300)
let bufname = await nvim.call('expand', ['%:p'])
expect(bufname).toMatch('manager.test.ts')
})
it('should ignore invalid input', async () => {
await manager.start(['location'])
await manager.session.ui.ready
await manager.onInsertInput('<X-y>')
await manager.onInsertInput(String.fromCharCode(65533))
await manager.onInsertInput(String.fromCharCode(30))
expect(manager.isActivated).toBe(true)
})
it('should ignore <plug> insert', async () => {
await manager.start(['location'])
await manager.session.ui.ready
await nvim.eval('feedkeys("\\<plug>x", "in")')
await helper.wait(50)
expect(manager.isActivated).toBe(true)
})
})
describe('parseArgs()', () => {
it('should show error for bad option', async () => {
manager.parseArgs(['$x', 'location'])
let msg = await helper.getCmdline()
expect(msg).toMatch('Invalid list option')
})
it('should show error for option that does not exist', async () => {
manager.parseArgs(['-xyz', 'location'])
let msg = await helper.getCmdline()
expect(msg).toMatch('Invalid option')
})
it('should show error for interactive with list not support interactive', async () => {
manager.parseArgs(['--interactive', 'location'])
let msg = await helper.getCmdline()
expect(msg).toMatch('not supported')
})
})
describe('resume()', () => {
it('should resume by name', async () => {
await events.fire('FocusGained', [])
await manager.start(['location'])
await manager.session.ui.ready
await manager.session.hide()
await helper.wait(100)
await manager.resume('location')
expect(manager.isActivated).toBe(true)
})
})
describe('first(), last()', () => {
it('should get session by name', async () => {
let last: string
let list: IList = {
name: 'test',
actions: [{
name: 'open',
execute: (item: ListItem) => {
last = item.label
}
}],
defaultAction: 'open',
loadItems: () => Promise.resolve([{ label: 'foo' }, { label: 'bar' }])
}
manager.registerList(list)
await manager.start(['test'])
await manager.session.ui.ready
await manager.first('a')
await manager.last('a')
await manager.first('test')
expect(last).toBe('foo')
await manager.last('test')
expect(last).toBe('bar')
})
})
describe('registerList()', () => {
it('should recreat list', async () => {
let list: IList = {
name: 'test',
actions: [{
name: 'open', execute: _item => {
// noop
}
}],
defaultAction: 'open',
loadItems: () => Promise.resolve([{ label: 'foo' }, { label: 'bar' }])
}
manager.registerList(list)
helper.updateConfiguration('list.source.test.defaultAction', 'open')
let disposable = manager.registerList(list)
disposable.dispose()
await helper.wait(30)
let msg = await helper.getCmdline()
expect(msg).toMatch('recreated')
})
})
describe('start()', () => {
it('should show error when loadItems throws', async () => {
let list: IList = {
name: 'test',
actions: [{
name: 'open',
execute: (_item: ListItem) => {
}
}],
defaultAction: 'open',
loadItems: () => {
throw new Error('test error')
}
}
manager.registerList(list)
await manager.start(['test'])
await helper.wait(100)
})
})
describe('list options', () => {
it('should respect auto preview option', async () => {
await manager.start(['--auto-preview', 'location'])
await manager.session.ui.ready
await helper.waitFor('winnr', ['$'], 3)
let previewWinnr = await nvim.call('coc#list#has_preview')
expect(previewWinnr).toBe(2)
let bufnr = await nvim.call('winbufnr', previewWinnr)
let buf = nvim.createBuffer(bufnr)
let name = await buf.name
expect(name).toMatch('manager.test.ts')
await nvim.eval('feedkeys("j", "in")')
await helper.wait(100)
let winnr = await nvim.call('coc#list#has_preview')
expect(winnr).toBe(previewWinnr)
})
it('should respect input option', async () => {
await manager.start(['--input=foo', 'location'])
await manager.session.ui.ready
await helper.wait(30)
let line = await helper.getCmdline()
expect(line).toMatch('foo')
expect(manager.isActivated).toBe(true)
})
it('should respect regex filter', async () => {
await manager.start(['--input=f.o', '--regex', 'location'])
await helper.wait(200)
let item = await manager.session?.ui.item
expect(item.label).toMatch('foo')
})
it('should respect normal option', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
let line = await helper.getCmdline()
expect(line).toBe('')
})
it('should respect nosort option', async () => {
await manager.start(['--ignore-case', '--no-sort', 'location'])
await manager.session.ui.ready
expect(manager.isActivated).toBe(true)
await nvim.input('oo')
await helper.wait(100)
let line = await nvim.call('getline', ['.'])
expect(line).toMatch('foo')
})
it('should respect ignorecase option', async () => {
await manager.start(['--ignore-case', '--strict', 'location'])
await manager.session.ui.ready
expect(manager.isActivated).toBe(true)
await nvim.input('bar')
await helper.wait(100)
let n = manager.session?.ui.length
expect(n).toBe(1)
let line = await nvim.line
expect(line).toMatch('Bar')
})
it('should respect top option', async () => {
await manager.start(['--top', 'location'])
await manager.session.ui.ready
let nr = await nvim.call('winnr')
expect(nr).toBe(1)
})
it('should respect number select option', async () => {
await manager.start(['--number-select', 'location'])
await manager.session.ui.ready
await helper.wait(100)
await nvim.eval('feedkeys("2", "in")')
await helper.wait(100)
let lnum = locations[1].lnum
let curr = await nvim.call('line', '.')
expect(lnum).toBe(curr)
})
it('should respect tab option', async () => {
await manager.start(['--tab', '--auto-preview', 'location'])
await manager.session.ui.ready
await helper.wait(100)
await nvim.command('wincmd l')
let previewwindow = await nvim.eval('w:previewwindow')
expect(previewwindow).toBe(1)
})
})
})

View file

@ -0,0 +1,764 @@
import { Neovim } from '@chemzqm/neovim'
import path from 'path'
import { CancellationToken, Disposable } from 'vscode-languageserver-protocol'
import BasicList from '../../list/basic'
import manager from '../../list/manager'
import { IList, ListContext, ListItem, QuickfixItem } from '../../types'
import { disposeAll } from '../../util/index'
import window from '../../window'
import helper from '../helper'
class TestList extends BasicList {
public name = 'test'
public timeout = 3000
public text = 'test'
public detail = 'detail'
public loadItems(_context: ListContext, token: CancellationToken): Promise<ListItem[]> {
return new Promise(resolve => {
let timer = setTimeout(() => {
resolve([{ label: this.text }])
}, this.timeout)
token.onCancellationRequested(() => {
if (timer) {
clearTimeout(timer)
resolve([])
}
})
})
}
}
let nvim: Neovim
let disposables: Disposable[] = []
const locations: ReadonlyArray<QuickfixItem> = [{
filename: __filename,
col: 2,
lnum: 1,
text: 'foo'
}, {
filename: __filename,
col: 1,
lnum: 2,
text: 'Bar'
}, {
filename: __filename,
col: 1,
lnum: 3,
text: 'option'
}]
const lineList: IList = {
name: 'lines',
actions: [{
name: 'open',
execute: async item => {
await window.moveTo({
line: (item as ListItem).data.line,
character: 0
})
// noop
}
}],
defaultAction: 'open',
async loadItems(_context, _token): Promise<ListItem[]> {
let lines = []
for (let i = 0; i < 100; i++) {
lines.push(i.toString())
}
return lines.map((line, idx) => ({
label: line,
data: { line: idx }
}))
}
}
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
await nvim.setVar('coc_jump_locations', locations)
})
afterAll(async () => {
disposeAll(disposables)
await helper.shutdown()
})
afterEach(async () => {
manager.reset()
await helper.reset()
})
describe('isValidAction()', () => {
it('should check invalid action', async () => {
let mappings = manager.mappings
expect(mappings.isValidAction('foo')).toBe(false)
expect(mappings.isValidAction('do:switch')).toBe(true)
expect(mappings.isValidAction('eval:@*')).toBe(true)
expect(mappings.isValidAction('undefined:undefined')).toBe(false)
})
})
describe('User mappings', () => {
it('should show warning for invalid key', async () => {
let revert = helper.updateConfiguration('list.insertMappings', {
xy: 'action:tabe',
})
await helper.wait(30)
let msg = await helper.getCmdline()
revert()
await nvim.command('echo ""')
expect(msg).toMatch('Invalid configuration')
revert = helper.updateConfiguration('list.insertMappings', {
'<M-x>': 'action:tabe',
})
await helper.wait(30)
msg = await helper.getCmdline()
revert()
expect(msg).toMatch('Invalid configuration')
revert = helper.updateConfiguration('list.insertMappings', {
'<C-a>': 'foo:bar',
})
await helper.wait(30)
msg = await helper.getCmdline()
revert()
expect(msg).toMatch('Invalid configuration')
})
it('should execute action keymap', async () => {
let revert = helper.updateConfiguration('list.insertMappings', {
'<C-d>': 'action:quickfix',
})
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-d>')
let buftype = await nvim.eval('&buftype')
expect(buftype).toBe('quickfix')
revert()
})
it('should execute expr keymap', async () => {
await helper.mockFunction('TabOpen', 'quickfix')
helper.updateConfiguration('list.insertMappings', {
'<C-t>': 'expr:TabOpen',
})
helper.updateConfiguration('list.normalMappings', {
t: 'expr:TabOpen',
})
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-t>')
let buftype = await nvim.eval('&buftype')
expect(buftype).toBe('quickfix')
await nvim.command('close')
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await helper.listInput('t')
buftype = await nvim.eval('&buftype')
expect(buftype).toBe('quickfix')
})
it('should execute do mappings', async () => {
helper.updateConfiguration('list.previousKeymap', '<C-j>')
helper.updateConfiguration('list.nextKeymap', '<C-k>')
helper.updateConfiguration('list.insertMappings', {
'<C-n>': 'do:next',
'<C-p>': 'do:previous',
'<C-d>': 'do:exit',
})
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-n>')
let item = await manager.session?.ui.item
expect(item.label).toMatch(locations[1].text)
await helper.listInput('<C-p>')
item = await manager.session?.ui.item
expect(item.label).toMatch(locations[0].text)
await helper.listInput('<C-k>')
item = await manager.session?.ui.item
expect(item.label).toMatch(locations[1].text)
await helper.listInput('<C-j>')
item = await manager.session?.ui.item
expect(item.label).toMatch(locations[0].text)
await helper.listInput('<C-d>')
expect(manager.isActivated).toBe(false)
})
it('should execute prompt mappings', async () => {
helper.updateConfiguration('list.insertMappings', {
'<C-p>': 'prompt:previous',
'<C-n>': 'prompt:next',
'<C-a>': 'prompt:start',
'<C-e>': 'prompt:end',
'<Left>': 'prompt:left',
'<Right>': 'prompt:right',
'<backspace>': 'prompt:deleteforward',
'<C-x>': 'prompt:deletebackward',
'<C-k>': 'prompt:removetail',
'<C-u>': 'prompt:removeahead',
})
await manager.start(['location'])
await manager.session.ui.ready
for (let key of ['<C-p>', '<C-n>', '<C-a>', '<C-e>', '<Left>', '<Right>', '<backspace>', '<C-x>', '<C-k>', '<C-u>']) {
await helper.listInput(key)
}
expect(manager.isActivated).toBe(true)
})
it('should execute feedkeys keymap', async () => {
helper.updateConfiguration('list.insertMappings', {
'<C-f>': 'feedkeys:\\<C-f>',
})
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-f>')
let line = await nvim.call('line', '.')
expect(line).toBe(locations.length)
})
it('should execute normal keymap', async () => {
helper.updateConfiguration('list.insertMappings', {
'<C-g>': 'normal:G',
})
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-g>')
let line = await nvim.call('line', '.')
expect(line).toBe(locations.length)
})
it('should execute command keymap', async () => {
helper.updateConfiguration('list.insertMappings', {
'<C-w>': 'command:wincmd p',
})
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-w>')
expect(manager.isActivated).toBe(true)
let winnr = await nvim.call('winnr')
expect(winnr).toBe(1)
})
it('should execute call keymap', async () => {
await helper.mockFunction('Test', 1)
helper.updateConfiguration('list.insertMappings', {
'<C-t>': 'call:Test',
})
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-t>')
expect(manager.isActivated).toBe(true)
})
it('should insert clipboard register to prompt', async () => {
helper.updateConfiguration('list.insertMappings', {
'<C-r>': 'prompt:paste',
})
await nvim.command('let @* = "foobar"')
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-r>')
let { input } = manager.prompt
expect(input).toMatch('foobar')
await nvim.command('let @* = ""')
await helper.listInput('<C-r>')
expect(manager.prompt.input).toMatch('foobar')
})
it('should insert text from default register to prompt', async () => {
helper.updateConfiguration('list.insertMappings', {
'<C-v>': 'eval:@@',
})
await nvim.command('let @@ = "bar"')
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-v>')
let { input } = manager.prompt
expect(input).toMatch('bar')
})
})
describe('doAction()', () => {
it('should throw when action not found', async () => {
let mappings = manager.mappings
let fn = async () => {
await mappings.doAction('foo:bar')
}
await expect(fn()).rejects.toThrow(/doesn't exist/)
})
it('should not throw when session does not exist', async () => {
let mappings = manager.mappings
await mappings.doAction('do:selectall')
await mappings.doAction('do:help')
await mappings.doAction('do:refresh')
await mappings.doAction('do:toggle')
await mappings.doAction('do:jumpback')
await mappings.doAction('prompt:previous')
await mappings.doAction('prompt:next')
await mappings.doAction('do:refresh')
})
it('should not throw when action name does not exist', async () => {
await helper.mockFunction('MyExpr', '')
let mappings = manager.mappings
await mappings.doAction('expr', 'MyExpr')
})
})
describe('getAction()', () => {
it('should throw for invalid action', async () => {
let mappings = manager.mappings
let fn = () => {
mappings.getAction('foo')
}
expect(fn).toThrow(Error)
fn = () => {
mappings.getAction('do:bar')
}
expect(fn).toThrow(Error)
})
})
describe('Default normal mappings', () => {
it('should invoke action', async () => {
await manager.start(['--normal', '--no-quit', 'location'])
await manager.session.ui.ready
let winid = manager.session.ui.winid
await helper.listInput('t')
let nr = await nvim.call('tabpagenr')
expect(nr).toBe(2)
await nvim.call('win_gotoid', [winid])
await helper.listInput('s')
let winnr = await nvim.call('winnr', ['$'])
expect(winnr).toBe(3)
await nvim.call('win_gotoid', [winid])
await helper.listInput('d')
let filename = await nvim.call('expand', ['%'])
expect(filename).toMatch(path.basename(__filename))
await nvim.call('win_gotoid', [winid])
await helper.listInput('<cr>')
filename = await nvim.call('expand', ['%'])
expect(filename).toMatch(path.basename(__filename))
})
it('should select all items by <C-a>', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await helper.listInput('<C-a>')
let selected = manager.session?.ui.selectedItems
expect(selected.length).toBe(locations.length)
})
it('should stop by <C-c>', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await helper.listInput('<C-c>')
let loading = manager.session?.worker.isLoading
expect(loading).toBe(false)
})
it('should jump back by <C-o>', async () => {
let doc = await helper.createDocument()
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await helper.listInput('<C-o>')
let bufnr = await nvim.call('bufnr', ['%'])
expect(bufnr).toBe(doc.bufnr)
})
it('should scroll preview window by <C-e>, <C-y>', async () => {
await helper.createDocument()
await manager.start(['--auto-preview', '--normal', 'location'])
await manager.session.ui.ready
await helper.waitPreviewWindow()
let winnr = await nvim.call('coc#list#has_preview') as number
let winid = await nvim.call('win_getid', [winnr])
await helper.listInput('<C-e>')
let res = await nvim.call('getwininfo', [winid])
expect(res[0].topline).toBeGreaterThan(1)
await helper.listInput('<C-y>')
res = await nvim.call('getwininfo', [winid])
expect(res[0].topline).toBeLessThan(7)
})
it('should insert command by :', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await helper.listInput(':')
await nvim.eval('feedkeys("let g:x = 1\\<cr>", "in")')
let res = await nvim.getVar('x')
expect(res).toBe(1)
})
it('should select action by <tab>', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
let p = helper.listInput('<tab>')
await helper.wait(50)
await nvim.input('t')
await p
let nr = await nvim.call('tabpagenr')
expect(nr).toBe(2)
})
it('should preview by p', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await helper.listInput('p')
let winnr = await nvim.call('coc#list#has_preview')
expect(winnr).toBe(2)
})
it('should stop task by <C-c>', async () => {
disposables.push(manager.registerList(new TestList(nvim)))
let p = manager.start(['--normal', 'test'])
await helper.wait(50)
await nvim.input('<C-c>')
await p
let len = manager.session?.ui.length
expect(len).toBe(0)
})
it('should cancel list by <esc>', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await nvim.eval('feedkeys("\\<esc>", "in")')
await helper.waitValue(() => {
return manager.isActivated
}, false)
})
it('should reload list by <C-l>', async () => {
let list = new TestList(nvim)
list.timeout = 0
disposables.push(manager.registerList(list))
await manager.start(['--normal', 'test'])
await manager.session.ui.ready
list.text = 'new'
await helper.listInput('<C-l>')
await helper.wait(30)
let line = await nvim.line
expect(line).toMatch('new')
})
it('should toggle selection <space>', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await helper.listInput(' ')
let selected = manager.session?.ui.selectedItems
expect(selected.length).toBe(1)
await helper.listInput('k')
await helper.listInput(' ')
selected = manager.session?.ui.selectedItems
expect(selected.length).toBe(0)
})
it('should change to insert mode by i, o, a', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
let keys = ['i', 'I', 'o', 'O', 'a', 'A']
for (let key of keys) {
await helper.listInput(key)
let mode = manager.prompt.mode
expect(mode).toBe('insert')
await helper.listInput('<C-o>')
mode = manager.prompt.mode
expect(mode).toBe('normal')
}
})
it('should show help by ?', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await helper.listInput('?')
let bufname = await nvim.call('bufname', '%')
expect(bufname).toBe('[LIST HELP]')
})
})
describe('list insert mappings', () => {
it('should open by <cr>', async () => {
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<cr>')
let bufname = await nvim.call('expand', ['%:p'])
expect(bufname).toMatch('mappings.test.ts')
})
it('should paste input by <C-v>', async () => {
await nvim.command('let @* = "foo"')
await nvim.command('let @@ = "foo"')
await nvim.call('setreg', ['*', 'foo'])
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-v>')
let input = manager.prompt.input
expect(input).toBe('foo')
})
it('should insert register content by <C-r>', async () => {
await nvim.command('let @* = "foo"')
await nvim.command('let @@ = "foo"')
await nvim.call('setreg', ['*', 'foo'])
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-r>')
await helper.listInput('*')
let input = manager.prompt.input
expect(input).toBe('foo')
await helper.listInput('<C-r>')
await helper.listInput('<')
input = manager.prompt.input
expect(input).toBe('foo')
manager.prompt.reset()
})
it('should cancel by <esc>', async () => {
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<esc>')
expect(manager.isActivated).toBe(false)
})
it('should select action by insert <tab>', async () => {
await manager.start(['location'])
await manager.session.ui.ready
let p = helper.listInput('<tab>')
await helper.wait(50)
await nvim.input('d')
await p
let bufname = await nvim.call('bufname', ['%'])
expect(bufname).toMatch(path.basename(__filename))
})
it('should select action for visual selected items', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await helper.wait(50)
await nvim.input('V')
await helper.wait(30)
await nvim.input('2')
await helper.wait(30)
await nvim.input('j')
await helper.wait(30)
await manager.doAction('quickfix')
let buftype = await nvim.eval('&buftype')
expect(buftype).toBe('quickfix')
})
it('should stop loading by <C-c>', async () => {
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-c>')
expect(manager.isActivated).toBe(true)
})
it('should reload by <C-l>', async () => {
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-l>')
expect(manager.isActivated).toBe(true)
})
it('should change to normal mode by <C-o>', async () => {
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-o>')
expect(manager.isActivated).toBe(true)
})
it('should select line by <down> and <up>', async () => {
await manager.start(['location'])
await manager.session.ui.ready
await nvim.eval('feedkeys("\\<down>", "in")')
await nvim.eval('feedkeys("\\<up>", "in")')
expect(manager.isActivated).toBe(true)
let line = await nvim.line
expect(line).toMatch('foo')
})
it('should move cursor by <left> and <right>', async () => {
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('f')
await helper.listInput('<left>')
await helper.listInput('<left>')
await helper.listInput('a')
await helper.listInput('<right>')
await helper.listInput('<right>')
await helper.listInput('c')
let input = manager.prompt.input
expect(input).toBe('afc')
})
it('should move cursor by <end> and <home>', async () => {
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<home>')
await helper.listInput('<end>')
await helper.listInput('a')
let input = manager.prompt.input
expect(input).toBe('a')
})
it('should move cursor by <PageUp> <PageDown> <C-d>', async () => {
disposables.push(manager.registerList(lineList))
await manager.start(['lines'])
await manager.session.ui.ready
await helper.listInput('<PageDown>')
await helper.listInput('<PageUp>')
await helper.listInput('<C-d>')
})
it('should scroll window by <C-f> and <C-b>', async () => {
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-f>')
await helper.listInput('<C-b>')
})
it('should change input by <Backspace>', async () => {
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('f')
await helper.listInput('<backspace>')
let input = manager.prompt.input
expect(input).toBe('')
})
it('should change input by <C-b>', async () => {
let revert = helper.updateConfiguration('list.insertMappings', {
'<C-b>': 'prompt:removetail',
})
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('f')
await helper.listInput('o')
await helper.listInput('o')
await helper.listInput('<C-a>')
await helper.listInput('<C-b>')
expect(manager.mappings.hasUserMapping('insert', '<C-b>')).toBe(true)
let input = manager.prompt.input
revert()
expect(input).toBe('')
})
it('should change input by <C-h>', async () => {
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('f')
await helper.listInput('<C-h>')
let input = manager.prompt.input
expect(input).toBe('')
})
it('should change input by <C-w>', async () => {
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('f')
await helper.listInput('a')
await helper.listInput('<C-w>')
let input = manager.prompt.input
expect(input).toBe('')
})
it('should change input by <C-u>', async () => {
await manager.start(['--input=a', 'location'])
await manager.session.ui.ready
await helper.listInput('<C-u>')
let input = manager.prompt.input
expect(input).toBe('')
})
it('should change input by <C-n> and <C-p>', async () => {
async function session(input: string): Promise<void> {
await manager.start(['location'])
await manager.session.ui.ready
for (let ch of input) {
await helper.listInput(ch)
}
await manager.cancel()
}
await session('foo')
await session('bar')
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-n>')
let input = manager.prompt.input
expect(input.length).toBeGreaterThan(0)
await helper.listInput('<C-p>')
input = manager.prompt.input
expect(input.length).toBeGreaterThan(0)
})
it('should change matcher by <C-s>', async () => {
await manager.start(['location'])
await manager.session.ui.ready
await helper.listInput('<C-s>')
let matcher = manager.session?.listOptions.matcher
expect(matcher).toBe('strict')
await helper.listInput('<C-s>')
matcher = manager.session?.listOptions.matcher
expect(matcher).toBe('regex')
await helper.listInput('f')
let len = manager.session?.ui.length
expect(len).toBeGreaterThan(0)
})
})
describe('evalExpression', () => {
it('should exit list', async () => {
helper.updateConfiguration('list.normalMappings', {
t: 'do:exit',
})
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
expect(manager.mappings.hasUserMapping('normal', 't')).toBe(true)
await helper.listInput('t')
expect(manager.isActivated).toBe(false)
})
it('should cancel prompt', async () => {
helper.updateConfiguration('list.normalMappings', {
t: 'do:cancel',
})
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await helper.listInput('t')
let res = await nvim.call('coc#prompt#activated')
expect(res).toBe(0)
})
it('should invoke normal command', async () => {
let revert = helper.updateConfiguration('list.normalMappings', {
x: 'normal!:G'
})
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await helper.listInput('x')
revert()
let lnum = await nvim.call('line', ['.'])
expect(lnum).toBeGreaterThan(1)
})
it('should toggle, scroll preview', async () => {
let revert = helper.updateConfiguration('list.normalMappings', {
'<space>': 'do:toggle',
a: 'do:toggle',
b: 'do:previewtoggle',
c: 'do:previewup',
d: 'do:previewdown',
e: 'prompt:insertregister',
f: 'do:stop',
g: 'do:togglemode',
})
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await helper.listInput(' ')
for (let key of ['a', 'b', 'c', 'd', 'e', 'f', 'g']) {
await helper.listInput(key)
}
revert()
expect(manager.isActivated).toBe(true)
})
})

View file

@ -0,0 +1,304 @@
import { Neovim } from '@chemzqm/neovim'
import { Disposable } from 'vscode-languageserver-protocol'
import BasicList from '../../list/basic'
import manager from '../../list/manager'
import ListSession from '../../list/session'
import { ListItem, IList } from '../../types'
import { disposeAll } from '../../util'
import helper from '../helper'
let labels: string[] = []
let lastItem: string
let lastItems: ListItem[]
class SimpleList extends BasicList {
public name = 'simple'
public detail = 'detail'
public options = [{
name: 'foo',
description: 'foo'
}]
constructor(nvim: Neovim) {
super(nvim)
this.addAction('open', item => {
lastItem = item.label
})
this.addMultipleAction('multiple', items => {
lastItems = items
})
this.addAction('parallel', async () => {
await helper.wait(100)
}, { parallel: true })
this.addAction('reload', item => {
lastItem = item.label
}, { persist: true, reload: true })
}
public loadItems(): Promise<ListItem[]> {
return Promise.resolve(labels.map(s => {
return { label: s } as ListItem
}))
}
}
let nvim: Neovim
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
disposeAll(disposables)
manager.reset()
await helper.reset()
})
describe('list session', () => {
describe('doDefaultAction()', () => {
it('should throw error when default action does not exist', async () => {
labels = ['a', 'b', 'c']
let list = new SimpleList(nvim)
list.defaultAction = 'foo'
let len = list.actions.length
list.actions.splice(0, len)
disposables.push(manager.registerList(list))
await manager.start(['--normal', 'simple'])
let ui = manager.session.ui
await ui.ready
let err
try {
await manager.session.first()
} catch (e) {
err = e
}
expect(err).toBeDefined()
err = null
try {
await manager.session.last()
} catch (e) {
err = e
}
expect(err).toBeDefined()
})
})
describe('doItemAction()', () => {
it('should invoke multiple action', async () => {
labels = ['a', 'b', 'c']
let list = new SimpleList(nvim)
disposables.push(manager.registerList(list))
await manager.start(['--normal', 'simple'])
let ui = manager.session.ui
await ui.ready
await ui.selectAll()
await manager.doAction('multiple')
expect(lastItems.length).toBe(3)
lastItems = undefined
})
it('should invoke parallel action', async () => {
labels = ['a', 'b', 'c']
let list = new SimpleList(nvim)
disposables.push(manager.registerList(list))
await manager.start(['--normal', 'simple'])
let ui = manager.session.ui
await ui.ready
await ui.selectAll()
let d = Date.now()
await manager.doAction('parallel')
expect(Date.now() - d).toBeLessThan(300)
})
it('should invoke reload action', async () => {
labels = ['a', 'b', 'c']
let list = new SimpleList(nvim)
disposables.push(manager.registerList(list))
await manager.start(['--normal', 'simple'])
let ui = manager.session.ui
await ui.ready
labels = ['d', 'e']
await manager.doAction('reload')
await helper.wait(50)
let buf = await nvim.buffer
let lines = await buf.lines
expect(lines).toEqual(['d', 'e'])
})
})
describe('reloadItems()', () => {
it('should not reload items when window is hidden', async () => {
let fn = jest.fn()
let list: IList = {
name: 'reload',
defaultAction: 'open',
actions: [{
name: 'open',
execute: () => {}
}],
loadItems: () => {
fn()
return Promise.resolve([])
}
}
disposables.push(manager.registerList(list))
await manager.start(['--normal', 'reload'])
let ui = manager.session.ui
await ui.ready
await manager.cancel(true)
let ses = manager.getSession('reload')
await ses.reloadItems()
expect(fn).toBeCalledTimes(1)
})
})
describe('resume()', () => {
it('should do preview on resume', async () => {
labels = ['a', 'b', 'c']
let lastItem
let list = new SimpleList(nvim)
list.actions.push({
name: 'preview',
execute: item => {
lastItem = item
}
})
disposables.push(manager.registerList(list))
await manager.start(['--normal', '--auto-preview', 'simple'])
let ui = manager.session.ui
await ui.ready
await ui.selectLines(1, 2)
await helper.wait(50)
await nvim.call('coc#window#close', [ui.winid])
await helper.wait(100)
await manager.session.resume()
await helper.wait(100)
expect(lastItem).toBeDefined()
})
})
describe('jumpBack()', () => {
it('should jump back', async () => {
let win = await nvim.window
labels = ['a', 'b', 'c']
let list = new SimpleList(nvim)
disposables.push(manager.registerList(list))
await manager.start(['--normal', 'simple'])
let ui = manager.session.ui
await ui.ready
manager.session.jumpBack()
await helper.wait(50)
let winid = await nvim.call('win_getid')
expect(winid).toBe(win.id)
})
})
describe('doNumberSelect()', () => {
async function create(len: number): Promise<ListSession> {
labels = []
for (let i = 0; i < len; i++) {
let code = 'a'.charCodeAt(0) + i
labels.push(String.fromCharCode(code))
}
let list = new SimpleList(nvim)
disposables.push(manager.registerList(list))
await manager.start(['--normal', '--number-select', 'simple'])
let ui = manager.session.ui
await ui.ready
return manager.session
}
it('should return false for invalid number', async () => {
let session = await create(5)
let res = await session.doNumberSelect('a')
expect(res).toBe(false)
res = await session.doNumberSelect('8')
expect(res).toBe(false)
})
it('should consider 0 as 10', async () => {
let session = await create(15)
let res = await session.doNumberSelect('0')
expect(res).toBe(true)
expect(lastItem).toBe('j')
})
})
})
describe('showHelp()', () => {
it('should show description and options in help', async () => {
labels = ['a', 'b', 'c']
let list = new SimpleList(nvim)
disposables.push(manager.registerList(list))
await manager.start(['--normal', 'simple'])
let ui = manager.session.ui
await ui.ready
await manager.session.showHelp()
let lines = await nvim.call('getline', [1, '$'])
expect(lines.indexOf('DESCRIPTION')).toBeGreaterThan(0)
expect(lines.indexOf('ARGUMENTS')).toBeGreaterThan(0)
})
})
describe('chooseAction()', () => {
it('should filter actions not have shortcuts', async () => {
labels = ['a', 'b', 'c']
let fn = jest.fn()
let list = new SimpleList(nvim)
list.actions.push({
name: 'a',
execute: () => {
fn()
}
})
list.actions.push({
name: 'b',
execute: () => {
}
})
list.actions.push({
name: 'ab',
execute: () => {
}
})
disposables.push(manager.registerList(list))
await manager.start(['--normal', 'simple'])
await manager.session.ui.ready
let p = manager.session.chooseAction()
await helper.wait(50)
await nvim.input('a')
await p
expect(fn).toBeCalled()
})
it('should choose action by menu picker', async () => {
helper.updateConfiguration('list.menuAction', true)
labels = ['a', 'b', 'c']
let fn = jest.fn()
let list = new SimpleList(nvim)
let len = list.actions.length
list.actions.splice(0, len)
list.actions.push({
name: 'a',
execute: () => {
fn()
}
})
list.actions.push({
name: 'b',
execute: () => {
fn()
}
})
disposables.push(manager.registerList(list))
await manager.start(['--normal', 'simple'])
await manager.session.ui.ready
let p = manager.session.chooseAction()
await helper.wait(100)
await nvim.input('<cr>')
await p
})
})

View file

@ -0,0 +1,666 @@
import { Neovim } from '@chemzqm/neovim'
import { CancellationToken, Diagnostic, DiagnosticSeverity, Disposable, Emitter, Location, Range } from 'vscode-languageserver-protocol'
import { URI } from 'vscode-uri'
import diagnosticManager from '../../diagnostic/manager'
import events from '../../events'
import languages from '../../languages'
import BasicList, { PreviewOptions, toVimFiletype } from '../../list/basic'
import { formatListItems, formatPath, UnformattedListItem } from '../../list/formatting'
import manager from '../../list/manager'
import Document from '../../model/document'
import services, { IServiceProvider } from '../../services'
import { ListArgument, ListContext, ListItem, ServiceStat } from '../../types'
import { disposeAll } from '../../util'
import workspace from '../../workspace'
import helper from '../helper'
let listItems: ListItem[] = []
class OptionList extends BasicList {
public name = 'option'
public options: ListArgument[] = [{
name: '-w, -word',
description: 'word'
}, {
name: '-i, -input INPUT',
hasValue: true,
description: 'input'
}]
constructor(nvim) {
super(nvim)
this.addLocationActions()
}
public loadItems(_context: ListContext, _token: CancellationToken): Promise<ListItem[]> {
return Promise.resolve(listItems)
}
}
let previewOptions: PreviewOptions
class SimpleList extends BasicList {
public name = 'simple'
public defaultAction: 'preview'
constructor(nvim: Neovim) {
super(nvim)
this.addAction('preview', async (_item, context) => {
await this.preview(previewOptions, context)
})
}
public loadItems(): Promise<ListItem[]> {
return Promise.resolve(['a', 'b', 'c'].map((s, idx) => {
return { label: s, location: Location.create('test:///a', Range.create(idx, 0, idx + 1, 0)) } as ListItem
}))
}
}
let disposables: Disposable[] = []
let nvim: Neovim
const locations: any[] = [{
filename: __filename,
range: Range.create(0, 0, 0, 6),
text: 'foo'
}, {
filename: __filename,
range: Range.create(2, 0, 2, 6),
text: 'Bar'
}, {
filename: __filename,
range: Range.create(3, 0, 4, 6),
text: 'multiple'
}]
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
})
afterAll(async () => {
manager.dispose()
await helper.shutdown()
})
afterEach(async () => {
listItems = []
disposeAll(disposables)
manager.reset()
await helper.reset()
})
describe('formatting', () => {
describe('formatPath()', () => {
it('should format path', async () => {
expect(formatPath('hidden', 'path')).toBe('')
expect(formatPath('full', __filename)).toMatch('sources.test.ts')
expect(formatPath('short', __filename)).toMatch('sources.test.ts')
expect(formatPath('filename', __filename)).toMatch('sources.test.ts')
})
})
describe('formatListItems', () => {
it('should format list items', async () => {
expect(formatListItems(false, [])).toEqual([])
let items: UnformattedListItem[] = [{
label: ['a', 'b', 'c']
}]
expect(formatListItems(false, items)).toEqual([{
label: 'a\tb\tc'
}])
items = [{
label: ['a', 'b', 'c']
}, {
label: ['foo', 'bar', 'go']
}]
expect(formatListItems(true, items)).toEqual([{
label: 'a \tb \tc '
}, {
label: 'foo\tbar\tgo'
}])
})
})
})
describe('configuration', () => {
beforeEach(() => {
let list = new OptionList(nvim)
manager.registerList(list)
})
it('should change default options', async () => {
helper.updateConfiguration('list.source.option.defaultOptions', ['--normal'])
await manager.start(['option'])
await manager.session.ui.ready
const mode = manager.prompt.mode
expect(mode).toBe('normal')
})
it('should change default action', async () => {
helper.updateConfiguration('list.source.option.defaultAction', 'split')
await manager.start(['option'])
await manager.session.ui.ready
const action = manager.session.defaultAction
expect(action.name).toBe('split')
await manager.session.doAction()
let tab = await nvim.tabpage
let wins = await tab.windows
expect(wins.length).toBeGreaterThan(1)
})
it('should change default arguments', async () => {
helper.updateConfiguration('list.source.option.defaultArgs', ['-word'])
await manager.start(['option'])
await manager.session.ui.ready
const context = manager.session.context
expect(context.args).toEqual(['-word'])
})
})
describe('BasicList', () => {
describe('getFiletype()', () => {
it('should get filetype', async () => {
expect(toVimFiletype('latex')).toBe('tex')
expect(toVimFiletype('foo')).toBe('foo')
})
})
describe('parse arguments', () => {
it('should parse args #1', () => {
let list = new OptionList(nvim)
let res = list.parseArguments(['-w'])
expect(res).toEqual({ word: true })
})
it('should parse args #2', () => {
let list = new OptionList(nvim)
let res = list.parseArguments(['-word'])
expect(res).toEqual({ word: true })
})
it('should parse args #3', () => {
let list = new OptionList(nvim)
let res = list.parseArguments(['-input', 'foo'])
expect(res).toEqual({ input: 'foo' })
})
})
describe('jumpTo()', () => {
let list: OptionList
beforeAll(() => {
list = new OptionList(nvim)
})
it('should jump to uri', async () => {
let uri = URI.file(__filename).toString()
await list.jumpTo(uri, 'edit')
let bufname = await nvim.call('bufname', ['%'])
expect(bufname).toMatch('sources.test.ts')
})
it('should jump to location', async () => {
let uri = URI.file(__filename).toString()
let loc = Location.create(uri, Range.create(0, 0, 1, 0))
await list.jumpTo(loc, 'edit')
let bufname = await nvim.call('bufname', ['%'])
expect(bufname).toMatch('sources.test.ts')
})
it('should jump to location with empty range', async () => {
let uri = URI.file(__filename).toString()
let loc = Location.create(uri, Range.create(0, 0, 0, 0))
await list.jumpTo(loc, 'edit')
let bufname = await nvim.call('bufname', ['%'])
expect(bufname).toMatch('sources.test.ts')
})
})
describe('convertLocation()', () => {
let list: OptionList
beforeAll(() => {
list = new OptionList(nvim)
})
it('should convert uri', async () => {
let uri = URI.file(__filename).toString()
let res = await list.convertLocation(uri)
expect(res.uri).toBe(uri)
})
it('should convert location with line', async () => {
let uri = URI.file(__filename).toString()
let res = await list.convertLocation({ uri, line: 'convertLocation()', text: 'convertLocation' })
expect(res.uri).toBe(uri)
res = await list.convertLocation({ uri, line: 'convertLocation()' })
expect(res.uri).toBe(uri)
})
it('should convert location with custom schema', async () => {
let uri = 'test:///foo'
let res = await list.convertLocation({ uri, line: 'convertLocation()' })
expect(res.uri).toBe(uri)
})
})
describe('createAction()', () => {
it('should overwrite action', async () => {
let idx: number
let list = new OptionList(nvim)
listItems.push({
label: 'foo',
location: Location.create('untitled:///1', Range.create(0, 0, 0, 0))
})
list.createAction({
name: 'foo',
execute: () => { idx = 0 }
})
list.createAction({
name: 'foo',
execute: () => { idx = 1 }
})
disposables.push(manager.registerList(list))
await manager.start(['--normal', 'option'])
await manager.session.ui.ready
await manager.doAction('foo')
expect(idx).toBe(1)
})
})
describe('preview()', () => {
beforeEach(() => {
let list = new SimpleList(nvim)
disposables.push(manager.registerList(list))
})
async function doPreview(opts: PreviewOptions): Promise<number> {
previewOptions = opts
await manager.start(['--normal', 'simple'])
await manager.session.ui.ready
await manager.doAction('preview')
let res = await nvim.call('coc#list#has_preview') as number
expect(res).toBeGreaterThan(0)
let winid = await nvim.call('win_getid', [res])
return winid
}
it('should preview lines', async () => {
await doPreview({ filetype: '', lines: ['foo', 'bar'] })
})
it('should preview with bufname', async () => {
await doPreview({
bufname: 't.js',
filetype: 'typescript',
lines: ['foo', 'bar']
})
})
it('should preview with range highlight', async () => {
let winid = await doPreview({
bufname: 't.js',
filetype: 'typescript',
lines: ['foo', 'bar'],
range: Range.create(0, 0, 0, 3)
})
let res = await nvim.call('getmatches', [winid])
expect(res.length).toBeGreaterThan(0)
})
})
describe('previewLocation()', () => {
it('should preview sketch buffer', async () => {
await nvim.command('new')
await nvim.setLine('foo')
let doc = await workspace.document
expect(doc.uri).toMatch('untitled')
let list = new OptionList(nvim)
listItems.push({
label: 'foo',
location: Location.create(doc.uri, Range.create(0, 0, 0, 0))
})
disposables.push(manager.registerList(list))
await manager.start(['option'])
await manager.session.ui.ready
await helper.wait(30)
await manager.doAction('preview')
await nvim.command('wincmd p')
let win = await nvim.window
let isPreview = await win.getVar('previewwindow')
expect(isPreview).toBe(1)
let line = await nvim.line
expect(line).toBe('foo')
})
})
})
describe('list sources', () => {
beforeAll(async () => {
await nvim.setVar('coc_jump_locations', locations)
})
describe('locations', () => {
it('should highlight ranges', async () => {
await manager.start(['--normal', '--auto-preview', 'location'])
await manager.session.ui.ready
await helper.wait(200)
manager.prompt.cancel()
await nvim.command('wincmd k')
let name = await nvim.eval('bufname("%")')
expect(name).toMatch('sources.test.ts')
let res = await nvim.call('getmatches')
expect(res.length).toBe(1)
})
it('should change highlight on cursor move', async () => {
await manager.start(['--normal', '--auto-preview', 'location'])
await manager.session.ui.ready
await nvim.command('exe 2')
let bufnr = await nvim.eval('bufnr("%")')
await events.fire('CursorMoved', [bufnr, [2, 1]])
await helper.waitFor('winnr', ['$'], 3)
await nvim.command('wincmd k')
let res = await nvim.call('getmatches')
expect(res.length).toBe(1)
expect(res[0]['pos1']).toEqual([3, 1, 6])
})
it('should highlight multiple line range', async () => {
await manager.start(['--normal', '--auto-preview', 'location'])
await manager.session.ui.ready
await nvim.command('exe 3')
let bufnr = await nvim.eval('bufnr("%")')
await events.fire('CursorMoved', [bufnr, [2, 1]])
await helper.waitFor('winnr', ['$'], 3)
await nvim.command('wincmd k')
let res = await nvim.call('getmatches')
expect(res.length).toBe(1)
expect(res[0]['pos1']).toBeDefined()
expect(res[0]['pos2']).toBeDefined()
})
it('should do open action', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await manager.doAction('open')
let name = await nvim.eval('bufname("%")')
expect(name).toMatch('sources.test.ts')
})
it('should do quickfix action', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await manager.session.ui.selectAll()
await manager.doAction('quickfix')
let buftype = await nvim.eval('&buftype')
expect(buftype).toBe('quickfix')
})
it('should do refactor action', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await manager.session.ui.selectAll()
await manager.doAction('refactor')
let name = await nvim.eval('bufname("%")')
expect(name).toMatch('coc_refactor')
})
it('should do tabe action', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await manager.doAction('tabe')
let tabs = await nvim.tabpages
expect(tabs.length).toBe(2)
})
it('should do drop action', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await manager.doAction('drop')
let name = await nvim.eval('bufname("%")')
expect(name).toMatch('sources.test.ts')
})
it('should do vsplit action', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await manager.doAction('vsplit')
let name = await nvim.eval('bufname("%")')
expect(name).toMatch('sources.test.ts')
})
it('should do split action', async () => {
await manager.start(['--normal', 'location'])
await manager.session.ui.ready
await manager.doAction('split')
let name = await nvim.eval('bufname("%")')
expect(name).toMatch('sources.test.ts')
})
})
describe('commands', () => {
it('should load commands source', async () => {
await manager.start(['commands'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
})
it('should do run action', async () => {
await manager.start(['commands'])
await manager.session?.ui.ready
await manager.doAction()
})
})
describe('diagnostics', () => {
function createDiagnostic(msg: string, range?: Range, severity?: DiagnosticSeverity, code?: number): Diagnostic {
range = range ? range : Range.create(0, 0, 0, 1)
return Diagnostic.create(range, msg, severity || DiagnosticSeverity.Error, code)
}
async function createDocument(name?: string): Promise<Document> {
let doc = await helper.createDocument(name)
let collection = diagnosticManager.create('test')
disposables.push({
dispose: () => {
collection.clear()
collection.dispose()
}
})
let diagnostics: Diagnostic[] = []
await doc.buffer.setLines(['foo bar foo bar', 'foo bar', 'foo', 'bar'], {
start: 0,
end: -1,
strictIndexing: false
})
diagnostics.push(createDiagnostic('error', Range.create(0, 2, 0, 4), DiagnosticSeverity.Error, 1001))
diagnostics.push(createDiagnostic('warning', Range.create(0, 5, 0, 6), DiagnosticSeverity.Warning, 1002))
diagnostics.push(createDiagnostic('information', Range.create(1, 0, 1, 1), DiagnosticSeverity.Information, 1003))
diagnostics.push(createDiagnostic('hint', Range.create(1, 2, 1, 3), DiagnosticSeverity.Hint, 1004))
diagnostics.push(createDiagnostic('error', Range.create(2, 0, 2, 2), DiagnosticSeverity.Error, 1005))
collection.set(doc.uri, diagnostics)
await doc.synchronize()
return doc
}
it('should load diagnostics source', async () => {
await createDocument('a')
await manager.start(['diagnostics'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
})
it('should not include code', async () => {
let fn = helper.updateConfiguration('list.source.diagnostics.includeCode', false)
disposables.push({ dispose: fn })
await createDocument('a')
await manager.start(['diagnostics'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
let line = await nvim.line
expect(line.match(/100/)).toBeNull()
})
it('should hide file path', async () => {
helper.updateConfiguration('list.source.diagnostics.pathFormat', 'hidden')
await createDocument('foo')
await manager.start(['diagnostics'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
let line = await nvim.line
expect(line.match(/foo/)).toBeNull()
})
it('should refresh on diagnostics refresh', async () => {
let doc = await createDocument('bar')
await manager.start(['diagnostics'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
let diagnostics: Diagnostic[] = []
let collection = diagnosticManager.create('test')
diagnostics.push(createDiagnostic('error', Range.create(2, 0, 2, 2), DiagnosticSeverity.Error, 1009))
collection.set(doc.uri, diagnostics)
await helper.wait(50)
let buf = await nvim.buffer
let lines = await buf.lines
expect(lines.length).toBeGreaterThan(0)
})
})
describe('extensions', () => {
it('should load extensions source', async () => {
await manager.start(['extensions'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
})
})
describe('folders', () => {
it('should load folders source', async () => {
await manager.start(['folders'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
})
})
describe('lists', () => {
it('should load lists source', async () => {
await manager.start(['lists'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
await helper.listInput('<cr>')
await helper.wait(50)
let s = manager.getSession()
expect(s.name != 'lists').toBe(true)
})
})
describe('outline', () => {
it('should load outline source', async () => {
await manager.start(['outline'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
})
})
describe('services', () => {
function createService(name: string): IServiceProvider {
let _onServcieReady = new Emitter<void>()
// public readonly onServcieReady: Event<void> = this.
let service: IServiceProvider = {
id: name,
name,
selector: [{ language: 'vim' }],
state: ServiceStat.Initial,
start(): Promise<void> {
service.state = ServiceStat.Running
_onServcieReady.fire()
return Promise.resolve()
},
dispose(): void {
service.state = ServiceStat.Stopped
},
stop(): void {
service.state = ServiceStat.Stopped
},
restart(): void {
service.state = ServiceStat.Running
_onServcieReady.fire()
},
onServiceReady: _onServcieReady.event
}
disposables.push(services.regist(service))
return service
}
it('should load services source', async () => {
createService('foo')
createService('bar')
await manager.start(['services'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
let lines = await nvim.call('getline', [1, '$']) as string[]
expect(lines.length).toBe(2)
})
it('should toggle service state', async () => {
let service = createService('foo')
await service.start()
await manager.start(['services'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
let ses = manager.session
expect(ses.name).toBe('services')
await ses.doAction('toggle')
expect(service.state).toBe(ServiceStat.Stopped)
await ses.doAction('toggle')
})
})
describe('sources', () => {
it('should load sources source', async () => {
await manager.start(['sources'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
let session = manager.getSession()
await session.doAction('open')
let bufname = await nvim.call('bufname', '%')
expect(bufname).toMatch(/native/)
})
it('should toggle source state', async () => {
await manager.start(['sources'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
let session = manager.getSession()
await session.doAction('toggle')
await session.doAction('toggle')
})
it('should refresh source', async () => {
await manager.start(['sources'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
let session = manager.getSession()
await session.doAction('refresh')
})
})
describe('symbols', () => {
it('should load symbols source', async () => {
await helper.createDocument()
let disposable = languages.registerWorkspaceSymbolProvider({
provideWorkspaceSymbols: () => []
})
await manager.start(['--interactive', 'symbols'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
disposable.dispose()
})
})
describe('links', () => {
it('should load links source', async () => {
let disposable = languages.registerDocumentLinkProvider([{ scheme: 'file' }, { scheme: 'untitled' }], {
provideDocumentLinks: () => []
})
await manager.start(['links'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
disposable.dispose()
})
})
})

View file

@ -0,0 +1,274 @@
import { Neovim } from '@chemzqm/neovim'
import { EventEmitter } from 'events'
import { Disposable } from 'vscode-languageserver-protocol'
import BasicList from '../../list/basic'
import events from '../../events'
import manager from '../../list/manager'
import { ListItem, IList, ListTask } from '../../types'
import { disposeAll } from '../../util'
import helper from '../helper'
let labels: string[] = []
let lastItem: string
class SimpleList extends BasicList {
public name = 'simple'
constructor(nvim: Neovim) {
super(nvim)
this.addAction('open', item => {
lastItem = item.label
})
}
public loadItems(): Promise<ListItem[]> {
return Promise.resolve(labels.map(s => {
return { label: s, ansiHighlights: [{ span: [0, 1], hlGroup: 'MoreMsg' }] } as ListItem
}))
}
}
class SlowTask extends EventEmitter implements ListTask {
private interval: NodeJS.Timer
constructor() {
super()
let i = 0
let interval = this.interval = setInterval(() => {
i++
this.emit('data', {
label: i.toString(), highlights: {
spans: [[0, 1]],
hlGroup: 'Search'
}
})
if (i == 5) {
this.emit('end')
clearInterval(interval)
}
}, 50)
}
public dispose(): void {
clearInterval(this.interval)
this.removeAllListeners()
}
}
let nvim: Neovim
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
disposeAll(disposables)
manager.reset()
await helper.reset()
})
describe('list ui', () => {
describe('selectLines()', () => {
it('should select lines', async () => {
labels = ['foo', 'bar']
disposables.push(manager.registerList(new SimpleList(nvim)))
await manager.start(['simple'])
let ui = manager.session.ui
await ui.ready
await ui.selectLines(3, 1)
let buf = await nvim.buffer
let res = await buf.getSigns({ group: 'coc-list' })
expect(res.length).toBe(2)
})
})
describe('preselect', () => {
it('should select preselect item', async () => {
let list: IList = {
actions: [{
name: 'open',
execute: () => {}
}],
name: 'preselect',
defaultAction: 'open',
loadItems: () => {
return Promise.resolve([{ label: 'foo' }, { label: 'bar', preselect: true }])
}
}
disposables.push(manager.registerList(list))
await manager.start(['preselect'])
let ui = manager.session.ui
await ui.ready
let line = await nvim.line
expect(line).toBe('bar')
})
})
describe('resume()', () => {
it('should resume with selected lines', async () => {
labels = ['foo', 'bar']
disposables.push(manager.registerList(new SimpleList(nvim)))
await manager.start(['simple'])
let ui = manager.session.ui
await ui.ready
await ui.selectLines(1, 2)
await nvim.call('coc#window#close', [ui.winid])
await helper.wait(100)
await manager.session.resume()
await helper.wait(100)
let buf = await nvim.buffer
let res = await buf.getSigns({ group: 'coc-list' })
expect(res.length).toBe(2)
})
})
describe('events', () => {
async function mockMouse(winid: number, lnum: number): Promise<void> {
await nvim.command(`let v:mouse_winid = ${winid}`)
await nvim.command(`let v:mouse_lnum = ${lnum}`)
await nvim.command('let v:mouse_col = 1')
}
it('should fire action on double click', async () => {
labels = ['foo', 'bar']
disposables.push(manager.registerList(new SimpleList(nvim)))
await manager.start(['simple'])
let ui = manager.session.ui
await ui.ready
await mockMouse(ui.winid, 1)
await manager.session.onMouseEvent('<2-LeftMouse>')
await helper.wait(100)
expect(lastItem).toBe('foo')
})
it('should select clicked line', async () => {
labels = ['foo', 'bar']
disposables.push(manager.registerList(new SimpleList(nvim)))
await manager.start(['simple'])
let ui = manager.session.ui
await ui.ready
await mockMouse(ui.winid, 2)
await ui.onMouse('mouseDown')
await helper.wait(50)
await mockMouse(ui.winid, 2)
await ui.onMouse('mouseUp')
await helper.wait(50)
let item = await ui.item
expect(item.label).toBe('bar')
})
it('should jump to original window on click', async () => {
labels = ['foo', 'bar']
let win = await nvim.window
disposables.push(manager.registerList(new SimpleList(nvim)))
await manager.start(['simple'])
let ui = manager.session.ui
await ui.ready
await mockMouse(win.id, 1)
await ui.onMouse('mouseUp')
await helper.wait(50)
let curr = await nvim.window
expect(curr.id).toBe(win.id)
})
it('should highlights items on CursorMoved', async () => {
labels = (new Array(400)).fill('a')
disposables.push(manager.registerList(new SimpleList(nvim)))
await manager.start(['--normal', 'simple'])
let ui = manager.session.ui
await ui.ready
await nvim.call('cursor', [350, 1])
await events.fire('CursorMoved', [ui.bufnr, [350, 1]])
await helper.wait(100)
let res = await nvim.call('coc#highlight#get_highlights', [ui.bufnr, 'list'])
expect(res.length).toBeGreaterThan(300)
})
})
})
describe('reversed list', () => {
it('should render and add highlights', async () => {
labels = ['a', 'b', 'c', 'd']
disposables.push(manager.registerList(new SimpleList(nvim)))
await manager.start(['--reverse', 'simple'])
let ui = manager.session.ui
await ui.ready
let buf = nvim.createBuffer(ui.bufnr)
let lines = await buf.lines
expect(lines).toEqual(['d', 'c', 'b', 'a'])
await helper.listInput('a')
await helper.wait(50)
lines = await buf.lines
expect(lines).toEqual(['a'])
let res = await nvim.call('coc#highlight#get_highlights', [ui.bufnr, 'list'])
expect(res.length).toBe(2)
let win = nvim.createWindow(ui.winid)
let height = await win.height
expect(height).toBe(1)
})
it('should moveUp and moveDown', async () => {
labels = ['a', 'b', 'c', 'd']
disposables.push(manager.registerList(new SimpleList(nvim)))
await manager.start(['--reverse', 'simple'])
let ui = manager.session.ui
await ui.ready
ui.moveUp()
await helper.waitFor('line', ['.'], 3)
ui.moveDown()
await helper.waitFor('line', ['.'], 4)
})
it('should toggle selection', async () => {
labels = ['a', 'b', 'c', 'd']
disposables.push(manager.registerList(new SimpleList(nvim)))
await manager.start(['--reverse', '--normal', 'simple'])
let ui = manager.session.ui
await ui.ready
await ui.toggleSelection()
let items = ui.selectedItems
expect(items.length).toBeGreaterThan(0)
expect(items[0].label).toBe('a')
let lnum = await nvim.call('line', ['.'])
expect(lnum).toBe(3)
await helper.listInput('j')
await ui.toggleSelection()
items = ui.selectedItems
expect(items.length).toBe(0)
})
it('should prepend list items', async () => {
let o: any
let p = new Promise(resolve => {
let list: IList = {
actions: [{
name: 'open',
execute: item => {
o = item
}
}],
name: 'slow',
defaultAction: 'open',
loadItems: () => {
let task = new SlowTask()
task.on('end', () => {
resolve(undefined)
})
return Promise.resolve(task)
}
}
disposables.push(manager.registerList(list))
void manager.start(['--reverse', '--normal', 'slow'])
})
await p
await helper.wait(50)
let ui = manager.session.ui
let buf = nvim.createBuffer(ui.bufnr)
let lines = await buf.lines
expect(lines).toEqual(['5', '4', '3', '2', '1'])
let lnum = await nvim.call('line', ['.'])
expect(lnum).toBe(5)
})
})

View file

@ -0,0 +1,226 @@
import { Neovim } from '@chemzqm/neovim'
import manager from '../../list/manager'
import { parseInput } from '../../list/worker'
import helper from '../helper'
import { ListContext, ListTask, ListItem } from '../../types'
import { CancellationToken, Disposable } from 'vscode-languageserver-protocol'
import { EventEmitter } from 'events'
import colors from 'colors/safe'
import BasicList from '../../list/basic'
import { disposeAll } from '../../util'
let items: ListItem[] = []
class DataList extends BasicList {
public name = 'data'
public loadItems(): Promise<ListItem[]> {
return Promise.resolve(items)
}
}
class EmptyList extends BasicList {
public name = 'empty'
public loadItems(): Promise<ListItem[]> {
let emitter: any = new EventEmitter()
setTimeout(() => {
emitter.emit('end')
}, 20)
return emitter
}
}
class IntervalTaskList extends BasicList {
public name = 'task'
public timeout = 3000
public loadItems(_context: ListContext, token: CancellationToken): Promise<ListTask> {
let emitter: any = new EventEmitter()
let i = 0
let interval = setInterval(() => {
emitter.emit('data', { label: i.toFixed() })
i++
}, 50)
emitter.dispose = () => {
clearInterval(interval)
emitter.emit('end')
}
token.onCancellationRequested(() => {
emitter.dispose()
})
return emitter
}
}
class DelayTask extends BasicList {
public name = 'delay'
public interactive = true
public loadItems(_context: ListContext, token: CancellationToken): Promise<ListTask> {
let emitter: any = new EventEmitter()
let disposed = false
setTimeout(() => {
if (disposed) return
emitter.emit('data', { label: 'ahead' })
}, 100)
setTimeout(() => {
if (disposed) return
emitter.emit('data', { label: 'abort' })
}, 200)
emitter.dispose = () => {
disposed = true
emitter.emit('end')
}
token.onCancellationRequested(() => {
emitter.dispose()
})
return emitter
}
}
class InteractiveList extends BasicList {
public name = 'test'
public interactive = true
public loadItems(context: ListContext, _token: CancellationToken): Promise<ListItem[]> {
return Promise.resolve([{
label: colors.magenta(context.input || '')
}])
}
}
class ErrorList extends BasicList {
public name = 'error'
public interactive = true
public loadItems(_context: ListContext, _token: CancellationToken): Promise<ListItem[]> {
return Promise.reject(new Error('test error'))
}
}
class ErrorTaskList extends BasicList {
public name = 'task'
public loadItems(_context: ListContext, _token: CancellationToken): Promise<ListTask> {
let emitter: any = new EventEmitter()
let timeout = setTimeout(() => {
emitter.emit('error', new Error('task error'))
}, 100)
emitter.dispose = () => {
clearTimeout(timeout)
}
return emitter
}
}
let nvim: Neovim
let disposables: Disposable[] = []
beforeAll(async () => {
await helper.setup()
nvim = helper.nvim
})
afterAll(async () => {
await helper.shutdown()
})
afterEach(async () => {
disposeAll(disposables)
manager.reset()
await helper.reset()
})
describe('parseInput', () => {
it('should parse input with space', async () => {
let res = parseInput('a b')
expect(res).toEqual(['a', 'b'])
res = parseInput('a b ')
expect(res).toEqual(['a', 'b'])
})
it('should parse input with escaped space', async () => {
let res = parseInput('a\\ b')
expect(res).toEqual(['a b'])
})
})
describe('list worker', () => {
it('should work with long running task', async () => {
disposables.push(manager.registerList(new IntervalTaskList(nvim)))
await manager.start(['task'])
await manager.session.ui.ready
await helper.wait(200)
let len = manager.session?.length
expect(len > 2).toBe(true)
await manager.cancel()
})
it('should sort by sortText', async () => {
items = [{
label: 'abc',
sortText: 'b'
}, {
label: 'ade',
sortText: 'a'
}]
disposables.push(manager.registerList(new DataList(nvim)))
await manager.start(['data'])
await manager.session.ui.ready
await nvim.input('a')
await helper.wait(50)
let buf = await nvim.buffer
let lines = await buf.lines
expect(lines).toEqual(['ade', 'abc'])
await manager.cancel()
})
it('should show empty line for empty task', async () => {
disposables.push(manager.registerList(new EmptyList(nvim)))
await manager.start(['empty'])
await manager.session.ui.ready
let line = await nvim.call('getline', [1])
expect(line).toMatch('No results')
})
it('should cancel task by use CancellationToken', async () => {
disposables.push(manager.registerList(new IntervalTaskList(nvim)))
await manager.start(['task'])
expect(manager.session?.worker.isLoading).toBe(true)
await helper.wait(100)
manager.session?.stop()
expect(manager.session?.worker.isLoading).toBe(false)
})
it('should render slow interactive list', async () => {
disposables.push(manager.registerList(new DelayTask(nvim)))
await manager.start(['delay'])
await nvim.input('a')
await helper.wait(600)
let buf = await nvim.buffer
let lines = await buf.lines
expect(lines).toEqual(['ahead', 'abort'])
})
it('should work with interactive list', async () => {
disposables.push(manager.registerList(new InteractiveList(nvim)))
await manager.start(['-I', 'test'])
await manager.session?.ui.ready
expect(manager.isActivated).toBe(true)
await nvim.eval('feedkeys("f", "in")')
await helper.wait(100)
await nvim.eval('feedkeys("a", "in")')
await helper.wait(100)
await nvim.eval('feedkeys("x", "in")')
await helper.wait(300)
let item = await manager.session?.ui.item
expect(item.label).toBe('fax')
})
it('should not activate on load error', async () => {
disposables.push(manager.registerList(new ErrorList(nvim)))
await manager.start(['test'])
expect(manager.isActivated).toBe(false)
})
it('should deactivate on task error', async () => {
disposables.push(manager.registerList(new ErrorTaskList(nvim)))
await manager.start(['task'])
await helper.wait(300)
expect(manager.isActivated).toBe(false)
})
})

View file

@ -0,0 +1,265 @@
import { getHighlightItems, parseMarkdown, parseDocuments } from '../../markdown/index'
import { Documentation } from '../../types'
describe('getHighlightItems', () => {
it('should get highlights in single line', async () => {
let res = getHighlightItems('this line has highlights', 0, [10, 15])
expect(res).toEqual([{
colStart: 10,
colEnd: 15,
lnum: 0,
hlGroup: 'CocUnderline'
}])
})
it('should get highlights when active end extended', async () => {
let res = getHighlightItems('this line', 0, [5, 30])
expect(res).toEqual([{
colStart: 5,
colEnd: 9,
lnum: 0,
hlGroup: 'CocUnderline'
}])
})
it('should get highlights across line', async () => {
let res = getHighlightItems('this line\nhas highlights', 0, [5, 15])
expect(res).toEqual([{
colStart: 5, colEnd: 9, lnum: 0, hlGroup: 'CocUnderline'
}, {
colStart: 0, colEnd: 5, lnum: 1, hlGroup: 'CocUnderline'
}])
res = getHighlightItems('a\nb\nc\nd', 0, [2, 5])
expect(res).toEqual([
{ colStart: 0, colEnd: 1, lnum: 1, hlGroup: 'CocUnderline' },
{ colStart: 0, colEnd: 1, lnum: 2, hlGroup: 'CocUnderline' },
{ colStart: 0, colEnd: 0, lnum: 3, hlGroup: 'CocUnderline' }
])
})
})
describe('parseMarkdown', () => {
it('should parse code blocks', async () => {
let content = `
\`\`\`js
var global = globalThis
\`\`\`
\`\`\`ts
let str:string
\`\`\`
\`\`\`bash
if
\`\`\`
`
let res = parseMarkdown(content, {})
expect(res.lines).toEqual([
'var global = globalThis',
'',
'let str:string',
'',
'if'
])
expect(res.codes).toEqual([
{ filetype: 'javascript', startLine: 0, endLine: 1 },
{ filetype: 'typescript', startLine: 2, endLine: 3 },
{ filetype: 'sh', startLine: 4, endLine: 5 },
])
})
it('should merge empty lines', async () => {
let content = `
![img](http://img.io)
![img](http://img.io)
[link](http://example.com)
[link](javascript:void(0))
`
let res = parseMarkdown(content, { excludeImages: true })
expect(res.lines).toEqual([
'link',
'',
'link: http://example.com'
])
})
it('should parse html code block', async () => {
let content = `
example:
\`\`\`html
<div>code</div>
\`\`\`
`
let res = parseMarkdown(content, {})
expect(res.lines).toEqual(['example:', '', '<div>code</div>'])
expect(res.codes).toEqual([{ filetype: 'html', startLine: 2, endLine: 3 }])
})
it('should compose empty lines', async () => {
let content = 'foo\n\n\nbar\n\n\n'
let res = parseMarkdown(content, {})
expect(res.lines).toEqual(['foo', '', 'bar'])
})
it('should merge lines', async () => {
let content = 'first\nsecond'
let res = parseMarkdown(content, {})
expect(res.lines).toEqual(['first', 'second'])
})
it('should parse ansi highlights', async () => {
let content = '__foo__\n[link](link)'
let res = parseMarkdown(content, {})
expect(res.lines).toEqual(['foo', 'link'])
expect(res.highlights).toEqual([
{ hlGroup: 'CocBold', lnum: 0, colStart: 0, colEnd: 3 },
{ hlGroup: 'CocUnderline', lnum: 1, colStart: 0, colEnd: 4 }
])
})
it('should exclude images by option', async () => {
let content = 'head\n![img](img)\ncontent ![img](img) ![img](img)'
let res = parseMarkdown(content, { excludeImages: false })
expect(res.lines).toEqual(['head', '![img](img)', 'content ![img](img) ![img](img)'])
content = 'head\n![img](img)\ncontent ![img](img) ![img](img)'
res = parseMarkdown(content, { excludeImages: true })
expect(res.lines).toEqual(['head', 'content'])
})
it('should render hr', async () => {
let content = 'foo\n***\nbar'
let res = parseMarkdown(content, {})
expect(res.lines).toEqual(['foo', '', '───', 'bar'])
})
it('should render deleted text', async () => {
let content = '~foo~'
let res = parseMarkdown(content, {})
expect(res.highlights).toEqual([
{ hlGroup: 'CocStrikeThrough', lnum: 0, colStart: 0, colEnd: 3 }
])
})
it('should render br', async () => {
let content = 'a \nb'
let res = parseMarkdown(content, {})
expect(res.lines).toEqual(['a', 'b'])
})
it('should render code span', async () => {
let content = '`foo`'
let res = parseMarkdown(content, {})
expect(res.highlights).toEqual([
{ hlGroup: 'CocMarkdownCode', lnum: 0, colStart: 0, colEnd: 3 }
])
})
it('should render html', async () => {
let content = '<div>foo</div>'
let res = parseMarkdown(content, {})
expect(res.lines).toEqual(['foo'])
})
it('should render checkbox', async () => {
let content = '- [x] first\n- [ ] second'
let res = parseMarkdown(content, {})
expect(res.lines).toEqual([
' * [X] first', ' * [ ] second'
])
})
it('should render numbered list', async () => {
let content = '1. one\n2. two\n3. three'
let res = parseMarkdown(content, {})
expect(res.lines).toEqual([
' 1. one', ' 2. two', ' 3. three'
])
})
it('should render nested list', async () => {
let content = '- foo\n- bar\n - one\n - two'
let res = parseMarkdown(content, {})
expect(res.lines).toEqual([
' * foo', ' * bar', ' * one', ' * two'
])
})
})
describe('parseDocuments', () => {
it('should parse documents with diagnostic filetypes', async () => {
let docs = [{
filetype: 'Error',
content: 'Error text'
}, {
filetype: 'Warning',
content: 'Warning text'
}]
let res = parseDocuments(docs)
expect(res.lines).toEqual([
'Error text',
'─',
'Warning text'
])
expect(res.codes).toEqual([
{ hlGroup: 'CocErrorFloat', startLine: 0, endLine: 1 },
{ hlGroup: 'CocWarningFloat', startLine: 2, endLine: 3 }
])
})
it('should parse markdown document with filetype document', async () => {
let docs = [{
filetype: 'typescript',
content: 'const workspace'
}, {
filetype: 'markdown',
content: '**header**'
}]
let res = parseDocuments(docs)
expect(res.lines).toEqual([
'const workspace',
'─',
'header'
])
expect(res.highlights).toEqual([{
hlGroup: 'CocBold',
lnum: 2,
colStart: 0,
colEnd: 6
}])
expect(res.codes).toEqual([
{ filetype: 'typescript', startLine: 0, endLine: 1 }
])
})
it('should parse document with highlights', async () => {
let docs: Documentation[] = [{
filetype: 'txt',
content: 'foo'
}, {
filetype: 'txt',
content: 'foo bar',
highlights: [{
lnum: 0,
colStart: 4,
colEnd: 7,
hlGroup: 'String'
}]
}]
let res = parseDocuments(docs)
let { highlights } = res
expect(highlights).toEqual([{ lnum: 2, colStart: 4, colEnd: 7, hlGroup: 'String' }])
})
it('should parse documents with active highlights', async () => {
let docs = [{
filetype: 'javascript',
content: 'func(foo, bar)',
active: [5, 8]
}, {
filetype: 'javascript',
content: 'func()',
active: [15, 20]
}]
let res = parseDocuments(docs as any)
expect(res.highlights).toEqual([{ colStart: 5, colEnd: 8, lnum: 0, hlGroup: 'CocUnderline' }
])
})
})

View file

@ -0,0 +1,119 @@
import { marked } from 'marked'
import Renderer from '../../markdown/renderer'
import * as styles from '../../markdown/styles'
import { parseAnsiHighlights, AnsiResult } from '../../util/ansiparse'
marked.setOptions({
renderer: new Renderer()
})
function parse(text: string): AnsiResult {
let m = marked(text)
let res = parseAnsiHighlights(m.split(/\n/)[0], true)
return res
}
describe('styles', () => {
it('should add styles', async () => {
let keys = ['gray', 'magenta', 'bold', 'underline', 'italic', 'strikethrough', 'yellow', 'green', 'blue']
for (let key of keys) {
let res = styles[key]('text')
expect(res).toContain('text')
}
})
})
describe('Renderer of marked', () => {
it('should create bold highlights', async () => {
let res = parse('**note**.')
expect(res.highlights[0]).toEqual({
span: [0, 4],
hlGroup: 'CocBold'
})
})
it('should create italic highlights', async () => {
let res = parse('_note_.')
expect(res.highlights[0]).toEqual({
span: [0, 4],
hlGroup: 'CocItalic'
})
})
it('should create underline highlights for link', async () => {
let res = parse('[baidu](https://baidu.com)')
expect(res.highlights[0]).toEqual({
span: [0, 5],
hlGroup: 'CocMarkdownLink'
})
res = parse('https://baidu.com')
expect(res.highlights[0]).toEqual({
span: [0, 17],
hlGroup: 'CocUnderline'
})
})
it('should parse link', async () => {
// let res = parse('https://doc.rust-lang.org/nightly/core/iter/traits/iterator/Iterator.t.html#map.v')
// console.log(JSON.stringify(res, null, 2))
let link = 'https://doc.rust-lang.org/nightly/core/iter/traits/iterator/Iterator.t.html#map.v'
let parsed = marked(link)
let res = parseAnsiHighlights(parsed.split(/\n/)[0], true)
expect(res.line).toEqual(link)
expect(res.highlights.length).toBeGreaterThan(0)
expect(res.highlights[0].hlGroup).toBe('CocUnderline')
})
it('should create highlight for code span', async () => {
let res = parse('`let foo = "bar"`')
expect(res.highlights[0]).toEqual({
span: [0, 15],
hlGroup: 'CocMarkdownCode'
})
})
it('should create header highlights', async () => {
let res = parse('# header')
expect(res.highlights[0]).toEqual({
span: [0, 8],
hlGroup: 'CocMarkdownHeader'
})
res = parse('## header')
expect(res.highlights[0]).toEqual({
span: [0, 9],
hlGroup: 'CocMarkdownHeader'
})
res = parse('### header')
expect(res.highlights[0]).toEqual({
span: [0, 10],
hlGroup: 'CocMarkdownHeader'
})
})
it('should indent blockquote', async () => {
let res = parse('> header')
expect(res.line).toBe(' header')
})
it('should preserve code block', async () => {
let text = '``` js\nconsole.log("foo")\n```'
let m = marked(text)
expect(m.split('\n')).toEqual([
'``` js',
'console.log("foo")',
'```',
''
])
})
it('should renderer table', async () => {
let text = `
| Syntax | Description |
| ----------- | ----------- |
| Header | Title |
| Paragraph | Text |
`
let res = marked(text)
expect(res).toContain('Syntax')
})
})

View file

@ -0,0 +1 @@
{}

Some files were not shown because too many files have changed in this diff Show more