├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .npmignore ├── .vim └── coc-settings.json ├── README.md ├── bin ├── build-docs.js └── index.js ├── package.json ├── src ├── common │ ├── constant.ts │ ├── fuzzy.ts │ ├── logger.ts │ ├── patterns.ts │ ├── types.ts │ └── util.ts ├── docs │ └── builtin-docs.json ├── handles │ ├── completion │ │ ├── autocmds.ts │ │ ├── builtinVariable.ts │ │ ├── colorscheme.ts │ │ ├── command.ts │ │ ├── expandEnum.ts │ │ ├── function.ts │ │ ├── hasEnum.ts │ │ ├── highlightArgKeys.ts │ │ ├── highlightArgValues.ts │ │ ├── identifier.ts │ │ ├── index.ts │ │ ├── mapEnum.ts │ │ ├── option.ts │ │ └── provider.ts │ ├── completionResolve.ts │ ├── definition.ts │ ├── diagnostic.ts │ ├── documentHighlight.ts │ ├── documentSymbol.ts │ ├── foldingRange.ts │ ├── hover.ts │ ├── references.ts │ ├── rename.ts │ ├── selectionRange.ts │ └── signatureHelp.ts ├── index.ts ├── lib │ ├── vimparser.d.ts │ └── vimparser.js ├── script │ └── build-docs.ts └── server │ ├── buffer.ts │ ├── builtin.ts │ ├── config.ts │ ├── connection.ts │ ├── documents.ts │ ├── parser.ts │ ├── scan.ts │ ├── snippets.ts │ └── workspaces.ts ├── test ├── fixtures │ └── findProjectRoot │ │ └── upRoot │ │ └── projectRoot │ │ └── autoload │ │ └── plugin │ │ └── foo.vim └── src │ └── util │ └── findProjectRoot.ts ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [12.x] 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v2 23 | - name: Setup ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - name: yarn install, build, and test 28 | run: | 29 | yarn install 30 | yarn build 31 | yarn fix 32 | yarn lint 33 | yarn test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # create by https://github.com/iamcco/gitignore.vim 3 | # gitignore templates from https://github.com/dvcs/gitignore 4 | 5 | ### Node.gitignore ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (http://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Typescript v1 declaration files 45 | typings/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | 65 | out/ 66 | .DS_Store 67 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.vim/coc-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Runtimepath", 4 | "Vimruntime", 5 | "Workdir", 6 | "argpos", 7 | "autocmd", 8 | "cmdpos", 9 | "cond", 10 | "endfor", 11 | "endfunction", 12 | "endwhile", 13 | "fitem", 14 | "fitems", 15 | "flist", 16 | "fname", 17 | "fpath", 18 | "iskeyword", 19 | "linepos", 20 | "lnum", 21 | "msglog", 22 | "neco", 23 | "rlist", 24 | "runtimepath's", 25 | "runtimepaths", 26 | "shvl", 27 | "textdocument", 28 | "vimlparser", 29 | "vimls", 30 | "vimparser", 31 | "vimruntime's", 32 | "vimscript" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VimScript Language Server 2 | 3 | [![CI](https://github.com/iamcco/vim-language-server/workflows/CI/badge.svg?branch=master)](https://github.com/iamcco/vim-language-server/actions?query=workflow%3ACI) 4 | [![Npm](https://img.shields.io/github/package-json/v/iamcco/vim-language-server)](https://www.npmjs.com/package/vim-language-server) 5 | ![Type](https://img.shields.io/npm/types/vim-language-server) 6 | ![download](https://img.shields.io/npm/dm/vim-language-server) 7 | 8 | > language server for VimScript 9 | 10 | **Features:** 11 | 12 | - auto completion 13 | - function signature help 14 | - hover document 15 | - go to definition 16 | - go to references 17 | - document symbols 18 | - document highlight 19 | - folding range 20 | - select range 21 | - rename 22 | - snippets 23 | - diagnostic 24 | 25 | ![autocomplete](https://user-images.githubusercontent.com/5492542/81493984-909c2e80-92d7-11ea-9638-d7be3e18e1d1.gif) 26 | 27 | ## Install 28 | 29 | **For yarn** 30 | 31 | ```sh 32 | yarn global add vim-language-server 33 | ``` 34 | 35 | **For npm** 36 | 37 | ```sh 38 | npm install -g vim-language-server 39 | ``` 40 | 41 | **For coc.nvim user** install coc extension: 42 | 43 | ```vim 44 | :CocInstall coc-vimlsp 45 | ``` 46 | 47 | **For vim-easycomplete user** install lsp server via `:InstallLspServer vim` and config nothing: 48 | 49 | ```vim 50 | :InstallLspServer vim 51 | ``` 52 | 53 | ## Config 54 | 55 | for document highlight 56 | 57 | ```vim 58 | let g:markdown_fenced_languages = [ 59 | \ 'vim', 60 | \ 'help' 61 | \] 62 | ``` 63 | 64 | lsp client config example with coc.nvim 65 | 66 | - Using node ipc 67 | 68 | ```jsonc 69 | "languageserver": { 70 | "vimls": { 71 | "module": "/path/to/vim-language-server/bin/index.js", 72 | "args": ["--node-ipc"], 73 | "initializationOptions": { 74 | "isNeovim": true, // is neovim, default false 75 | "iskeyword": "@,48-57,_,192-255,-#", // vim iskeyword option 76 | "vimruntime": "", // $VIMRUNTIME option 77 | "runtimepath": "", // vim runtime path separate by `,` 78 | "diagnostic": { 79 | "enable": true 80 | }, 81 | "indexes": { 82 | "runtimepath": true, // if index runtimepath's vim files this will effect the suggest 83 | "gap": 100, // index time gap between next file 84 | "count": 3, // count of files index at the same time 85 | "projectRootPatterns" : ["strange-root-pattern", ".git", "autoload", "plugin"] // Names of files used as the mark of project root. If empty, the default value [".git", "autoload", "plugin"] will be used 86 | }, 87 | "suggest": { 88 | "fromVimruntime": true, // completionItems from vimruntime's vim files 89 | "fromRuntimepath": false // completionItems from runtimepath's vim files, if this is true that fromVimruntime is true 90 | } 91 | }, 92 | "filetypes": [ "vim" ], 93 | } 94 | } 95 | ``` 96 | 97 | - Using stdio 98 | 99 | ```jsonc 100 | "languageserver": { 101 | "vimls": { 102 | "command": "vim-language-server", 103 | "args": ["--stdio"], 104 | "initializationOptions": { 105 | "isNeovim": true, // is neovim, default false 106 | "iskeyword": "@,48-57,_,192-255,-#", // vim iskeyword option 107 | "vimruntime": "", // $VIMRUNTIME option 108 | "runtimepath": "", // vim runtime path separate by `,` 109 | "diagnostic": { 110 | "enable": true 111 | }, 112 | "indexes": { 113 | "runtimepath": true, // if index runtimepath's vim files this will effect the suggest 114 | "gap": 100, // index time gap between next file 115 | "count": 3, // count of files index at the same time 116 | "projectRootPatterns" : ["strange-root-pattern", ".git", "autoload", "plugin"] // Names of files used as the mark of project root. If empty, the default value [".git", "autoload", "plugin"] will be used 117 | }, 118 | "suggest": { 119 | "fromVimruntime": true, // completionItems from vimruntime's vim files 120 | "fromRuntimepath": false // completionItems from runtimepath's vim files, if this is true that fromVimruntime is true 121 | } 122 | }, 123 | "filetypes": [ "vim" ] 124 | } 125 | } 126 | ``` 127 | 128 | **Note**: 129 | 130 | - if you set `isNeovim: true`, command like `fixdel` in vimrc which neovim does not support will report error. 131 | - if you want to speed up index, change `gap` to smaller and `count` to greater, this will cause high CPU usage for some time 132 | - if you don't want to index vim's runtimepath files, set `runtimepath` to `false` and you will not get any suggest from those files. 133 | 134 | ## Usage 135 | 136 | > The screen record is using coc.nvim as LSP client. 137 | 138 | **Auto complete and function signature help**: 139 | 140 | ![autocomplete](https://user-images.githubusercontent.com/5492542/81493984-909c2e80-92d7-11ea-9638-d7be3e18e1d1.gif) 141 | 142 | **Hover document**: 143 | 144 | ![hover](https://user-images.githubusercontent.com/5492542/81494066-5aab7a00-92d8-11ea-9ccd-31bd6440e622.gif) 145 | 146 | **Go to definition and references**: 147 | 148 | ![goto](https://user-images.githubusercontent.com/5492542/81494125-c261c500-92d8-11ea-83c0-fecba34ea55e.gif) 149 | 150 | **Document symbols**: 151 | 152 | ![symbols](https://user-images.githubusercontent.com/5492542/81494183-5cc20880-92d9-11ea-9495-a7691420df39.gif) 153 | 154 | **Document highlight**: 155 | 156 | ![highlight](https://user-images.githubusercontent.com/5492542/81494214-b1fe1a00-92d9-11ea-9cc1-0420cddc5cbc.gif) 157 | 158 | **Folding range and selection range**: 159 | 160 | ![fold](https://user-images.githubusercontent.com/5492542/81494276-3bade780-92da-11ea-8c93-bc3d2127a19d.gif) 161 | 162 | **Rename**: 163 | 164 | ![rename](https://user-images.githubusercontent.com/5492542/81494329-aa8b4080-92da-11ea-8a5d-ace5385445e9.gif) 165 | 166 | **Snippets and diagnostic**: 167 | 168 | ![dia](https://user-images.githubusercontent.com/5492542/81494408-503eaf80-92db-11ea-96ac-641d46027623.gif) 169 | 170 | ## References 171 | 172 | - [vim-vimlparser](https://github.com/vim-jp/vim-vimlparser) 173 | - [neco-vim](https://github.com/Shougo/neco-vim) 174 | 175 | ## Similar project 176 | 177 | - [vimscript-language-server](https://github.com/google/vimscript-language-server) 178 | 179 | ### Buy Me A Coffee ☕️ 180 | 181 | ![btc](https://img.shields.io/keybase/btc/iamcco.svg?style=popout-square) 182 | 183 | ![image](https://user-images.githubusercontent.com/5492542/42771079-962216b0-8958-11e8-81c0-520363ce1059.png) 184 | -------------------------------------------------------------------------------- /bin/build-docs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../out/build-docs') 4 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../out') 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vim-language-server", 3 | "version": "2.3.1", 4 | "description": "vim language server", 5 | "keywords": [ 6 | "viml", 7 | "vim", 8 | "lsp", 9 | "language", 10 | "server", 11 | "autocomplete" 12 | ], 13 | "main": "./out/index.js", 14 | "repository": "https://github.com/iamcco/vim-language-server", 15 | "author": "iamcco ", 16 | "license": "MIT", 17 | "scripts": { 18 | "build-docs": "rm ./src/docs/builtin-docs.json && ./bin/build-docs.js", 19 | "build": "rm -rf ./out && webpack", 20 | "watch": "webpack -w", 21 | "test": "mocha test/src/**/*.ts --require ts-node/register", 22 | "lint": "tslint -c tslint.json --format verbose {.,test}/src/**/*.ts src/index.ts", 23 | "fix": "tslint -c tslint.json --fix {.,test}/src/**/*.ts src/index.ts" 24 | }, 25 | "bin": { 26 | "vim-language-server": "./bin/index.js" 27 | }, 28 | "devDependencies": { 29 | "@types/mocha": "^7.0.2", 30 | "@types/node": "^11.13.6", 31 | "chai": "^4.2.0", 32 | "chai-as-promised": "^7.1.1", 33 | "fast-glob": "^3.2.4", 34 | "findup": "^0.1.5", 35 | "mocha": "^7.1.2", 36 | "rxjs": "^6.6.7", 37 | "rxjs-operators": "^1.1.3", 38 | "shvl": "^2.0.0", 39 | "ts-loader": "^8.1.0", 40 | "ts-node": "^9.1.1", 41 | "tslint": "^6.1.3", 42 | "typescript": "^4.2.3", 43 | "vscode-languageserver": "^7.0.0", 44 | "vscode-languageserver-textdocument": "^1.0.1", 45 | "vscode-uri": "^3.0.2", 46 | "webpack": "^5.30.0", 47 | "webpack-cli": "^4.6.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/common/constant.ts: -------------------------------------------------------------------------------- 1 | export const sortTexts = { 2 | one: "00001", 3 | two: "00002", 4 | three: "00003", 5 | four: "00004", 6 | }; 7 | 8 | export const projectRootPatterns = [".git", "autoload", "plugin"]; 9 | -------------------------------------------------------------------------------- /src/common/fuzzy.ts: -------------------------------------------------------------------------------- 1 | export default function fuzzy(origin: string, query: string): number { 2 | let score = 0; 3 | 4 | for (let qIdx = 0, oIdx = 0; qIdx < query.length && oIdx < origin.length; qIdx++) { 5 | const qc = query.charAt(qIdx).toLowerCase(); 6 | 7 | for (; oIdx < origin.length; oIdx++) { 8 | const oc = origin.charAt(oIdx).toLowerCase(); 9 | 10 | if (qc === oc) { 11 | score++; 12 | oIdx++; 13 | break; 14 | } 15 | } 16 | } 17 | 18 | return score; 19 | } 20 | -------------------------------------------------------------------------------- /src/common/logger.ts: -------------------------------------------------------------------------------- 1 | import { connection } from "../server/connection"; 2 | 3 | export default function(name: string) { 4 | return { 5 | log(message: string) { 6 | connection.console.log(`${name}: ${message}`); 7 | }, 8 | info(message: string) { 9 | connection.console.info(`${name}: ${message}`); 10 | }, 11 | warn(message: string) { 12 | connection.console.warn(`${name}: ${message}`); 13 | }, 14 | error(message: string) { 15 | connection.console.error(`${name}: ${message}`); 16 | }, 17 | showErrorMessage(message: string) { 18 | connection.window.showErrorMessage(message) 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/common/patterns.ts: -------------------------------------------------------------------------------- 1 | export type kindPattern = RegExp[]; 2 | 3 | export const errorLinePattern = /[^:]+:\s*(.+?):\s*line\s*([0-9]+)\s*col\s*([0-9]+)/; 4 | 5 | export const commentPattern = /^[ \t]*("|')/; 6 | 7 | export const keywordPattern = /[\w#&$<>.:]/; 8 | 9 | export const builtinFunctionPattern = /^((|\b(v|g|b|s|l|a):)?[\w#&]+)[ \t]*\([^)]*\)/; 10 | 11 | export const wordPrePattern = /^.*?(((|\b(v|g|b|s|l|a):)?[\w#&$.]+)|(||ID>|D>|>||\b(v|g|b|s|l|a):)?[\w#&$.]+|(:[\w#&$.]+)).*?(\r\n|\r|\n)?$/; 14 | 15 | export const colorschemePattern = /\bcolorscheme[ \t]+\w*$/; 16 | 17 | export const mapCommandPattern = /^([ \t]*(\[ \t]*)?)\w*map[ \t]+/; 18 | 19 | export const highlightLinkPattern = /^[ \t]*(hi|highlight)[ \t]+link([ \t]+[^ \t]+)*[ \t]*$/; 20 | 21 | export const highlightPattern = /^[ \t]*(hi|highlight)([ \t]+[^ \t]+)*[ \t]*$/; 22 | 23 | export const highlightValuePattern = /^[ \t]*(hi|highlight)([ \t]+[^ \t]+)*[ \t]+([^ \t=]+)=[^ \t=]*$/; 24 | 25 | export const autocmdPattern = /^[ \t]*(au|autocmd)!?[ \t]+([^ \t,]+,)*[^ \t,]*$/; 26 | 27 | export const globalIdentifierPattern = /^((g|b):\w+(\.\w+)*|(\w+#)+\w*)$/; 28 | 29 | export const normalIdentifierPattern = /^([a-zA-Z_]\w*(\.\w+)*)$/; 30 | 31 | export const scriptIdentifierPattern = /^((s:|)\w+(\.\w+)*)$/; 32 | 33 | export const localIdentifierPattern = /^(l:\w+(\.\w+)*)$/; 34 | 35 | export const funcArgIdentifierPattern = /^(a:\w+(\.\w+)*)$/; 36 | 37 | export const builtinVariablePattern = [ 38 | /\bv:\w*$/, 39 | ]; 40 | 41 | export const optionPattern = [ 42 | /(^|[ \t]+)&\w*$/, 43 | /(^|[ \t]+)set(l|local|g|global)?[ \t]+\w+$/, 44 | ]; 45 | 46 | export const notFunctionPattern = [ 47 | /^[ \t]*\\$/, 48 | /^[ \t]*\w+$/, 49 | /^[ \t]*"/, 50 | /(let|set|colorscheme)[ \t][^ \t]*$/, 51 | /[^([,\\ \t\w#>]\w*$/, 52 | /^[ \t]*(hi|highlight)([ \t]+link)?([ \t]+[^ \t]+)*[ \t]*$/, 53 | autocmdPattern, 54 | ]; 55 | 56 | export const commandPattern = [ 57 | /(^|[ \t]):\w+$/, 58 | /^[ \t]*\w+$/, 59 | /:?silent!?[ \t]\w+/, 60 | ]; 61 | 62 | export const featurePattern = [ 63 | /\bhas\([ \t]*["']\w*/, 64 | ]; 65 | 66 | export const expandPattern = [ 67 | /\bexpand\(['"]<\w*$/, 68 | /\bexpand\([ \t]*['"]\w*$/, 69 | ]; 70 | 71 | export const notIdentifierPattern = [ 72 | commentPattern, 73 | /("|'):\w*$/, 74 | /^[ \t]*\\$/, 75 | /^[ \t]*call[ \t]+[^ \t()]*$/, 76 | /('|"|#|&|\$|<)\w*$/, 77 | ]; 78 | -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- 1 | import { Subscription} from "rxjs"; 2 | import { ClientCapabilities, CompletionItem } from "vscode-languageserver"; 3 | 4 | export interface IParserHandles { 5 | [uri: string]: Subscription | undefined; 6 | } 7 | 8 | export interface IDiagnostic { 9 | enable: boolean; 10 | } 11 | 12 | export interface ISuggest { 13 | fromRuntimepath: boolean; 14 | fromVimruntime: boolean; 15 | } 16 | 17 | export interface IIndexes { 18 | runtimepath: boolean; 19 | gap: number; 20 | count: number; 21 | projectRootPatterns: string[]; 22 | } 23 | 24 | // initialization options 25 | export interface IConfig { 26 | isNeovim: boolean; 27 | iskeyword: string; 28 | vimruntime: string; 29 | runtimepath: string[]; 30 | diagnostic: IDiagnostic; 31 | snippetSupport: boolean; 32 | suggest: ISuggest; 33 | indexes: IIndexes; 34 | capabilities: ClientCapabilities 35 | } 36 | 37 | // builtin-doc 38 | export interface IBuiltinDoc { 39 | completionItems: { 40 | functions: CompletionItem[] 41 | commands: CompletionItem[] 42 | options: CompletionItem[] 43 | variables: CompletionItem[] 44 | features: CompletionItem[] 45 | expandKeywords: CompletionItem[] 46 | autocmds: CompletionItem[], 47 | }; 48 | signatureHelp: Record; 49 | documents: { 50 | functions: Record 51 | commands: Record 52 | options: Record 53 | variables: Record 54 | features: Record 55 | expandKeywords: Record, 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /src/common/util.ts: -------------------------------------------------------------------------------- 1 | import {ErrnoException} from "@nodelib/fs.stat/out/types"; 2 | import {spawn, SpawnOptions} from "child_process"; 3 | import findup from "findup"; 4 | import fs, {Stats} from "fs"; 5 | import path from "path"; 6 | import {Readable} from "stream"; 7 | import {CompletionItem, InsertTextFormat, Position, Range, TextDocument} from "vscode-languageserver"; 8 | import {INode, StringReader, VimLParser} from "../lib/vimparser"; 9 | import { commentPattern, keywordPattern, kindPattern, wordNextPattern, wordPrePattern } from "./patterns"; 10 | import config from '../server/config'; 11 | 12 | // FIXME vimlparser missing update builtin_commands 13 | if (VimLParser.prototype) { 14 | (VimLParser.prototype as any).builtin_commands?.push({ 15 | name: 'balt', 16 | minlen: 4, 17 | flags: 'NEEDARG|FILE1|EDITCMD|TRLBAR|CMDWIN', 18 | parser: 'parse_cmd_common' 19 | }) 20 | } 21 | 22 | export function isSomeMatchPattern(patterns: kindPattern, line: string): boolean { 23 | return patterns.some((p) => p.test(line)); 24 | } 25 | 26 | export function executeFile( 27 | input: Readable, 28 | command: string, 29 | args?: any[], 30 | option?: SpawnOptions, 31 | ): Promise<{ 32 | code: number, 33 | stdout: string, 34 | stderr: string, 35 | }> { 36 | return new Promise((resolve, reject) => { 37 | let stdout = ""; 38 | let stderr = ""; 39 | let error: Error; 40 | let isPassAsText = false; 41 | 42 | args = (args || []).map((arg) => { 43 | if (/%text/.test(arg)) { 44 | isPassAsText = true; 45 | return arg.replace(/%text/g, input.toString()); 46 | } 47 | return arg; 48 | }); 49 | 50 | const cp = spawn(command, args, option); 51 | 52 | cp.stdout.on("data", (data) => { 53 | stdout += data; 54 | }); 55 | 56 | cp.stderr.on("data", (data) => { 57 | stderr += data; 58 | }); 59 | 60 | cp.on("error", (err: Error) => { 61 | error = err; 62 | reject(error); 63 | }); 64 | 65 | cp.on("close", (code) => { 66 | if (!error) { 67 | resolve({ code, stdout, stderr }); 68 | } 69 | }); 70 | 71 | // error will occur when cp get error 72 | if (!isPassAsText) { 73 | input.pipe(cp.stdin).on("error", () => { return; }); 74 | } 75 | 76 | }); 77 | } 78 | 79 | // cover cb type async function to promise 80 | export function pcb( 81 | cb: (...args: any[]) => void, 82 | ): (...args: any[]) => Promise { 83 | return (...args: any[]): Promise => { 84 | return new Promise((resolve) => { 85 | cb(...args, (...params: any[]) => { 86 | resolve(params); 87 | }); 88 | }); 89 | }; 90 | } 91 | 92 | // find work dirname by root patterns 93 | export async function findProjectRoot( 94 | filePath: string, 95 | rootPatterns: string | string[], 96 | ): Promise { 97 | const dirname = path.dirname(filePath); 98 | const patterns = [].concat(rootPatterns); 99 | let dirCandidate = ""; 100 | for (const pattern of patterns) { 101 | const [err, dir] = await pcb(findup)(dirname, pattern); 102 | if (!err && dir && dir !== "/" && dir.length > dirCandidate.length) { 103 | dirCandidate = dir; 104 | } 105 | } 106 | if (dirCandidate.length) { 107 | return dirCandidate; 108 | } 109 | return dirname; 110 | } 111 | 112 | export function markupSnippets(snippets: string): string { 113 | return [ 114 | "```vim", 115 | snippets.replace(/\$\{[0-9]+(:([^}]+))?\}/g, "$2"), 116 | "```", 117 | ].join("\n"); 118 | } 119 | 120 | export function getWordFromPosition( 121 | doc: TextDocument, 122 | position: Position, 123 | ): { 124 | word: string 125 | wordLeft: string 126 | wordRight: string 127 | left: string 128 | right: string, 129 | } | undefined { 130 | if (!doc) { 131 | return; 132 | } 133 | 134 | // invalid character which less than 0 135 | if (position.character < 0) { 136 | return 137 | } 138 | 139 | const character = doc.getText( 140 | Range.create( 141 | Position.create(position.line, position.character), 142 | Position.create(position.line, position.character + 1), 143 | ), 144 | ); 145 | 146 | // not keyword position 147 | if (!character || !keywordPattern.test(character)) { 148 | return; 149 | } 150 | 151 | const currentLine = doc.getText( 152 | Range.create( 153 | Position.create(position.line, 0), 154 | Position.create(position.line + 1, 0), 155 | ), 156 | ); 157 | 158 | // comment line 159 | if (commentPattern.test(currentLine)) { 160 | return; 161 | } 162 | 163 | const preSegment = currentLine.slice(0, position.character); 164 | const nextSegment = currentLine.slice(position.character); 165 | const wordLeft = preSegment.match(wordPrePattern); 166 | const wordRight = nextSegment.match(wordNextPattern); 167 | const word = `${wordLeft && wordLeft[1] || ""}${wordRight && wordRight[1] || ""}`; 168 | 169 | return { 170 | word, 171 | left: wordLeft && wordLeft[1] || "", 172 | right: wordRight && wordRight[1] || "", 173 | wordLeft: wordLeft && wordLeft[1] 174 | ? preSegment.replace(new RegExp(`${wordLeft[1]}$`), word) 175 | : `${preSegment}${word}`, 176 | wordRight: wordRight && wordRight[1] 177 | ? nextSegment.replace(new RegExp(`^${wordRight[1]}`), word) 178 | : `${word}${nextSegment}`, 179 | }; 180 | } 181 | 182 | // parse vim buffer 183 | export async function handleParse(textDoc: TextDocument | string): Promise<[INode | null, string]> { 184 | const text = textDoc instanceof Object ? textDoc.getText() : textDoc; 185 | const tokens = new StringReader(text.split(/\r\n|\r|\n/)); 186 | try { 187 | const node: INode = new VimLParser(config.isNeovim).parse(tokens); 188 | return [node, ""]; 189 | } catch (error) { 190 | return [null, error]; 191 | } 192 | } 193 | 194 | // remove snippets of completionItem 195 | export function removeSnippets(completionItems: CompletionItem[] = []): CompletionItem[] { 196 | return completionItems.map((item) => { 197 | if (item.insertTextFormat === InsertTextFormat.Snippet) { 198 | return { 199 | ...item, 200 | insertText: item.label, 201 | insertTextFormat: InsertTextFormat.PlainText, 202 | }; 203 | } 204 | return item; 205 | }); 206 | } 207 | 208 | export const isSymbolLink = async (filePath: string): Promise<{ err: ErrnoException | null; stats: boolean }> => { 209 | return new Promise((resolve) => { 210 | fs.lstat(filePath, (err: ErrnoException | null, stats: Stats) => { 211 | resolve({ 212 | err, 213 | stats: stats && stats.isSymbolicLink(), 214 | }); 215 | }); 216 | }); 217 | }; 218 | 219 | export const getRealPath = async (filePath: string): Promise => { 220 | const { err, stats } = await isSymbolLink(filePath); 221 | if (!err && stats) { 222 | return new Promise((resolve) => { 223 | fs.realpath(filePath, (error: ErrnoException | null, realPath: string) => { 224 | if (error) { 225 | return resolve(filePath); 226 | } 227 | resolve(realPath); 228 | }); 229 | }); 230 | } 231 | return filePath; 232 | }; 233 | 234 | export const delay = async (ms: number) => { 235 | await new Promise((res) => { 236 | setTimeout(() => { 237 | res() 238 | }, ms); 239 | }) 240 | } 241 | -------------------------------------------------------------------------------- /src/handles/completion/autocmds.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * autocmds 3 | * 4 | */ 5 | import { CompletionItem } from "vscode-languageserver"; 6 | import { autocmdPattern } from "../../common/patterns"; 7 | import { builtinDocs } from "../../server/builtin"; 8 | import { useProvider } from "./provider"; 9 | 10 | function provider(line: string): CompletionItem[] { 11 | if (autocmdPattern.test(line)) { 12 | return builtinDocs.getVimAutocmds().filter((item) => { 13 | return line.indexOf(item.label) === -1; 14 | }); 15 | } 16 | return []; 17 | } 18 | 19 | useProvider(provider); 20 | -------------------------------------------------------------------------------- /src/handles/completion/builtinVariable.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * vim builtin variable 3 | * 4 | * - v:xxx 5 | */ 6 | import { CompletionItem } from "vscode-languageserver"; 7 | import { builtinVariablePattern } from "../../common/patterns"; 8 | import { isSomeMatchPattern } from "../../common/util"; 9 | import { builtinDocs } from "../../server/builtin"; 10 | import { useProvider } from "./provider"; 11 | 12 | function provider(line: string): CompletionItem[] { 13 | if (isSomeMatchPattern(builtinVariablePattern, line)) { 14 | return builtinDocs.getPredefinedVimVariables(); 15 | } 16 | return []; 17 | } 18 | 19 | useProvider(provider); 20 | -------------------------------------------------------------------------------- /src/handles/completion/colorscheme.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * vim color scheme 3 | */ 4 | import { CompletionItem } from "vscode-languageserver"; 5 | import { colorschemePattern } from "../../common/patterns"; 6 | import { builtinDocs } from "../../server/builtin"; 7 | import { useProvider } from "./provider"; 8 | 9 | function provider(line: string): CompletionItem[] { 10 | if (colorschemePattern.test(line)) { 11 | return builtinDocs.getColorschemes(); 12 | } 13 | return []; 14 | } 15 | 16 | useProvider(provider); 17 | -------------------------------------------------------------------------------- /src/handles/completion/command.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * vim builtin command 3 | * 4 | * - xxx 5 | */ 6 | import { CompletionItem } from "vscode-languageserver"; 7 | import { commandPattern } from "../../common/patterns"; 8 | import { isSomeMatchPattern } from "../../common/util"; 9 | import { builtinDocs } from "../../server/builtin"; 10 | import config from "../../server/config"; 11 | import { commandSnippets } from "../../server/snippets"; 12 | import { useProvider } from "./provider"; 13 | 14 | function provider(line: string): CompletionItem[] { 15 | if (isSomeMatchPattern(commandPattern, line)) { 16 | // only return snippets when snippetSupport is true 17 | if (config.snippetSupport) { 18 | return builtinDocs.getVimCommands().concat(commandSnippets); 19 | } 20 | return builtinDocs.getVimCommands(); 21 | } 22 | return []; 23 | } 24 | 25 | useProvider(provider); 26 | -------------------------------------------------------------------------------- /src/handles/completion/expandEnum.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * expand args enum 3 | * 4 | * - 5 | * - 6 | * - 7 | * - 8 | * - 9 | * - 10 | * - 11 | * - 12 | */ 13 | import { CompletionItem } from "vscode-languageserver"; 14 | import { expandPattern } from "../../common/patterns"; 15 | import { builtinDocs } from "../../server/builtin"; 16 | import { useProvider } from "./provider"; 17 | 18 | function provider(line: string): CompletionItem[] { 19 | if (expandPattern[0].test(line)) { 20 | return builtinDocs.getExpandKeywords().map((item) => { 21 | return { 22 | ...item, 23 | insertText: item.insertText.slice(1), 24 | }; 25 | }); 26 | } else if (expandPattern[1].test(line)) { 27 | return builtinDocs.getExpandKeywords(); 28 | } 29 | return []; 30 | } 31 | 32 | useProvider(provider); 33 | -------------------------------------------------------------------------------- /src/handles/completion/function.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * vim function provider 3 | * 4 | * - builtin vim function 5 | * - builtin neovim api function 6 | * - g:xxx 7 | * - s:xxx 8 | * - xx#xxx 9 | * - Xxx 10 | */ 11 | import { CompletionItem, Position } from "vscode-languageserver"; 12 | import { notFunctionPattern } from "../../common/patterns"; 13 | import { isSomeMatchPattern } from "../../common/util"; 14 | import { builtinDocs } from "../../server/builtin"; 15 | import config from "../../server/config"; 16 | import { workspace } from "../../server/workspaces"; 17 | import { useProvider } from "./provider"; 18 | 19 | function provider(line: string, uri: string, position: Position): CompletionItem[] { 20 | if (/\b(g:|s:|)\w*$/.test(line)) { 21 | let list: CompletionItem[] = []; 22 | if (/\bg:\w*$/.test(line)) { 23 | list = workspace.getFunctionItems(uri) 24 | .filter((item) => /^g:/.test(item.label)); 25 | } else if (/\b(s:|)\w*$/i.test(line)) { 26 | list = workspace.getFunctionItems(uri) 27 | .filter((item) => /^s:/.test(item.label)); 28 | } 29 | return list.map((item) => ({ 30 | ...item, 31 | insertText: !/:/.test(config.iskeyword) ? item.insertText.slice(2) : item.insertText, 32 | })); 33 | } else if (/\B:\w*$/.test(line)) { 34 | return workspace.getFunctionItems(uri) 35 | .filter((item) => /:/.test(item.label)) 36 | .map((item) => { 37 | const m = line.match(/:[^:]*$/); 38 | return { 39 | ...item, 40 | // delete the `:` symbol 41 | textEdit: { 42 | range: { 43 | start: { 44 | line: position.line, 45 | character: line.length - m[0].length, 46 | }, 47 | end: { 48 | line: position.line, 49 | character: line.length - m[0].length + 1, 50 | }, 51 | }, 52 | newText: item.insertText, 53 | }, 54 | }; 55 | }); 56 | } else if (isSomeMatchPattern(notFunctionPattern, line)) { 57 | return []; 58 | } 59 | return workspace.getFunctionItems(uri) 60 | .filter((item) => { 61 | return !builtinDocs.isBuiltinFunction(item.label); 62 | }) 63 | .concat( 64 | builtinDocs.getBuiltinVimFunctions(), 65 | ); 66 | } 67 | 68 | useProvider(provider); 69 | -------------------------------------------------------------------------------- /src/handles/completion/hasEnum.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * has features enum 3 | * 4 | * - mac 5 | * - win32 6 | * - win64 7 | * ... 8 | */ 9 | import { CompletionItem } from "vscode-languageserver"; 10 | import { featurePattern } from "../../common/patterns"; 11 | import { isSomeMatchPattern } from "../../common/util"; 12 | import { builtinDocs } from "../../server/builtin"; 13 | import { useProvider } from "./provider"; 14 | 15 | function provider(line: string): CompletionItem[] { 16 | if (isSomeMatchPattern(featurePattern, line)) { 17 | return builtinDocs.getVimFeatures(); 18 | } 19 | return []; 20 | } 21 | 22 | useProvider(provider); 23 | -------------------------------------------------------------------------------- /src/handles/completion/highlightArgKeys.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * highlight arg keys 3 | * 4 | */ 5 | import { CompletionItem } from "vscode-languageserver"; 6 | import { highlightLinkPattern, highlightPattern, highlightValuePattern } from "../../common/patterns"; 7 | import { builtinDocs } from "../../server/builtin"; 8 | import { useProvider } from "./provider"; 9 | 10 | function provider(line: string): CompletionItem[] { 11 | if ( 12 | !highlightLinkPattern.test(line) && 13 | !highlightValuePattern.test(line) && 14 | highlightPattern.test(line) 15 | ) { 16 | return builtinDocs.getHighlightArgKeys().filter((item) => { 17 | return line.indexOf(item.label) === -1; 18 | }); 19 | } 20 | return []; 21 | } 22 | 23 | useProvider(provider); 24 | -------------------------------------------------------------------------------- /src/handles/completion/highlightArgValues.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * highlight arg values 3 | * 4 | */ 5 | import { CompletionItem } from "vscode-languageserver"; 6 | import { highlightLinkPattern, highlightValuePattern } from "../../common/patterns"; 7 | import { builtinDocs } from "../../server/builtin"; 8 | import { useProvider } from "./provider"; 9 | 10 | function provider(line: string): CompletionItem[] { 11 | const m = line.match(highlightValuePattern); 12 | if (!highlightLinkPattern.test(line) && m) { 13 | const values = builtinDocs.getHighlightArgValues(); 14 | const keyName = m[3]; 15 | if (values[keyName]) { 16 | return values[keyName]; 17 | } 18 | } 19 | return []; 20 | } 21 | 22 | useProvider(provider); 23 | -------------------------------------------------------------------------------- /src/handles/completion/identifier.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * vim identifier 3 | * 4 | * - xxx 5 | * - g:xxx 6 | * - b:xxx 7 | * - s:xxx 8 | * - l:xxx 9 | * - a:xxx 10 | */ 11 | import { CompletionItem, Position } from "vscode-languageserver"; 12 | 13 | import { notIdentifierPattern } from "../../common/patterns"; 14 | import { isSomeMatchPattern } from "../../common/util"; 15 | import config from "../../server/config"; 16 | import { workspace } from "../../server/workspaces"; 17 | import { useProvider } from "./provider"; 18 | 19 | function provider(line: string, uri: string, position: Position): CompletionItem[] { 20 | if (isSomeMatchPattern(notIdentifierPattern, line)) { 21 | return []; 22 | } else if (/\b[gbsla]:\w*$/.test(line)) { 23 | let list = []; 24 | if (/\bg:\w*$/.test(line)) { 25 | list = workspace.getIdentifierItems(uri, position.line) 26 | .filter((item) => /^g:/.test(item.label)); 27 | } else if (/\bb:\w*$/.test(line)) { 28 | list = workspace.getIdentifierItems(uri, position.line) 29 | .filter((item) => /^b:/.test(item.label)); 30 | } else if (/\bs:\w*$/.test(line)) { 31 | list = workspace.getIdentifierItems(uri, position.line) 32 | .filter((item) => /^s:/.test(item.label)); 33 | } else if (/\bl:\w*$/.test(line)) { 34 | list = workspace.getIdentifierItems(uri, position.line) 35 | .filter((item) => /^l:/.test(item.label)); 36 | } else if (/\ba:\w*$/.test(line)) { 37 | list = workspace.getIdentifierItems(uri, position.line) 38 | .filter((item) => /^a:/.test(item.label)); 39 | } 40 | return list.map((item) => ({ 41 | ...item, 42 | insertText: !/:/.test(config.iskeyword) ? item.insertText.slice(2) : item.insertText, 43 | })); 44 | } else if (/\B:\w*$/.test(line)) { 45 | return workspace.getIdentifierItems(uri, position.line) 46 | .filter((item) => /:/.test(item.label)) 47 | .map((item) => { 48 | const m = line.match(/:[^:]*$/); 49 | return { 50 | ...item, 51 | // delete the `:` symbol 52 | textEdit: { 53 | range: { 54 | start: { 55 | line: position.line, 56 | character: line.length - m[0].length, 57 | }, 58 | end: { 59 | line: position.line, 60 | character: line.length - m[0].length + 1, 61 | }, 62 | }, 63 | newText: item.insertText, 64 | }, 65 | }; 66 | }); 67 | } 68 | return workspace.getIdentifierItems(uri, position.line); 69 | } 70 | 71 | useProvider(provider); 72 | -------------------------------------------------------------------------------- /src/handles/completion/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompletionItem, 3 | CompletionParams, 4 | Position, 5 | Range, 6 | CompletionList, 7 | } from "vscode-languageserver"; 8 | 9 | import fuzzy from "../../common/fuzzy"; 10 | import { getWordFromPosition, removeSnippets } from "../../common/util"; 11 | import config from "../../server/config"; 12 | import { documents } from "../../server/documents"; 13 | import "./autocmds"; 14 | import "./builtinVariable"; 15 | import "./colorscheme"; 16 | import "./command"; 17 | import "./expandEnum"; 18 | import "./function"; 19 | import "./hasEnum"; 20 | import "./highlightArgKeys"; 21 | import "./highlightArgValues"; 22 | import "./identifier"; 23 | import "./mapEnum"; 24 | import "./option"; 25 | import { getProvider } from "./provider"; 26 | 27 | const provider = getProvider(); 28 | 29 | export const completionProvider = (params: CompletionParams): CompletionList | CompletionItem[] => { 30 | 31 | const { textDocument, position } = params; 32 | const textDoc = documents.get(textDocument.uri); 33 | if (textDoc) { 34 | const line = textDoc.getText(Range.create( 35 | Position.create(position.line, 0), 36 | position, 37 | )); 38 | const words = getWordFromPosition(textDoc, { line: position.line, character: position.character - 1 }); 39 | let word = words && words.word || ""; 40 | if (word === "" && words && words.wordRight.trim() === ":") { 41 | word = ":"; 42 | } 43 | // options items start with & 44 | const invalidLength = word.replace(/^&/, "").length; 45 | const completionItems = provider(line, textDoc.uri, position, word, invalidLength, []); 46 | if (!config.snippetSupport) { 47 | return { 48 | isIncomplete: true, 49 | items: removeSnippets(completionItems) 50 | } 51 | } 52 | return { 53 | isIncomplete: true, 54 | items: completionItems 55 | } 56 | } 57 | return []; 58 | }; 59 | -------------------------------------------------------------------------------- /src/handles/completion/mapEnum.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * map defined args 3 | * 4 | * 5 | * ... 6 | */ 7 | import { CompletionItem } from "vscode-languageserver"; 8 | import { mapCommandPattern } from "../../common/patterns"; 9 | import { builtinDocs } from "../../server/builtin"; 10 | import { useProvider } from "./provider"; 11 | 12 | function provider(line: string): CompletionItem[] { 13 | if (mapCommandPattern.test(line)) { 14 | if (/<$/.test(line)) { 15 | return builtinDocs.getVimMapArgs().map((item) => ({ 16 | ...item, 17 | insertText: item.insertText!.slice(1), 18 | })); 19 | } 20 | return builtinDocs.getVimMapArgs(); 21 | } 22 | return []; 23 | } 24 | 25 | useProvider(provider); 26 | -------------------------------------------------------------------------------- /src/handles/completion/option.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * vim options 3 | * 4 | * - &xxxx 5 | */ 6 | import { CompletionItem } from "vscode-languageserver"; 7 | import { optionPattern } from "../../common/patterns"; 8 | import { isSomeMatchPattern } from "../../common/util"; 9 | import { builtinDocs } from "../../server/builtin"; 10 | import { useProvider } from "./provider"; 11 | 12 | function provider(line: string): CompletionItem[] { 13 | if (isSomeMatchPattern(optionPattern, line)) { 14 | return builtinDocs.getVimOptions(); 15 | } 16 | return []; 17 | } 18 | 19 | useProvider(provider); 20 | -------------------------------------------------------------------------------- /src/handles/completion/provider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompletionItem, 3 | Position, 4 | } from "vscode-languageserver"; 5 | import fuzzy from "../../common/fuzzy"; 6 | 7 | type Provider = (line: string, uri?: string, position?: Position) => CompletionItem[]; 8 | 9 | const providers: Provider[] = []; 10 | 11 | export function useProvider(p: Provider) { 12 | providers.push(p); 13 | } 14 | 15 | export function getProvider() { 16 | return providers.reduce((pre, next) => { 17 | return ( 18 | line: string, 19 | uri: string, 20 | position: Position, 21 | word: string, 22 | invalidLength: number, 23 | items: CompletionItem[], 24 | ): CompletionItem[] => { 25 | // 200 items is enough 26 | if (items.length > 200) { 27 | return items.slice(0, 200) 28 | } 29 | const newItems = next(line, uri, position) 30 | .filter((item) => fuzzy(item.label, word) >= invalidLength) 31 | return pre( 32 | line, 33 | uri, 34 | position, 35 | word, 36 | invalidLength, 37 | items.concat(newItems), 38 | ) 39 | }; 40 | 41 | }, ( 42 | _line: string, 43 | _uri: string, 44 | _position: Position, 45 | _word: string, 46 | _invalidLength: number, 47 | items: CompletionItem[], 48 | ): CompletionItem[] => items, 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/handles/completionResolve.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem } from "vscode-languageserver"; 2 | import { builtinDocs } from "../server/builtin"; 3 | 4 | export const completionResolveProvider = (params: CompletionItem): CompletionItem => { 5 | return builtinDocs.getDocumentByCompletionItem(params); 6 | }; 7 | -------------------------------------------------------------------------------- /src/handles/definition.ts: -------------------------------------------------------------------------------- 1 | import { Location, TextDocumentPositionParams } from "vscode-languageserver"; 2 | import { getWordFromPosition } from "../common/util"; 3 | import { documents } from "../server/documents"; 4 | import { workspace } from "../server/workspaces"; 5 | 6 | export const definitionProvider = (params: TextDocumentPositionParams): Location[] | null => { 7 | const { textDocument, position } = params; 8 | const doc = documents.get(textDocument.uri); 9 | if (!doc) { 10 | return null; 11 | } 12 | const words = getWordFromPosition(doc, position); 13 | if (!words) { 14 | return null; 15 | } 16 | let currentName = words.word; 17 | if (/\./.test(words.right)) { 18 | const tail = words.right.replace(/^[^.]*(\.)/, "$1"); 19 | currentName = words.word.replace(tail, ""); 20 | } 21 | return workspace.getLocations(currentName, doc.uri, position, "definition").locations; 22 | }; 23 | -------------------------------------------------------------------------------- /src/handles/diagnostic.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DiagnosticSeverity, 3 | Position, 4 | Range, 5 | TextDocument, 6 | } from "vscode-languageserver"; 7 | import { errorLinePattern } from "../common/patterns"; 8 | import { connection } from "../server/connection"; 9 | 10 | const fixNegativeNum = (num: number): number => { 11 | if (num < 0) { 12 | return 0; 13 | } 14 | return num; 15 | }; 16 | 17 | export async function handleDiagnostic( 18 | textDoc: TextDocument, 19 | error: string, 20 | ) { 21 | const m = (error || "").match(errorLinePattern); 22 | if (m) { 23 | const lines = textDoc.lineCount; 24 | const line = fixNegativeNum(parseFloat(m[2]) - 1); 25 | const col = fixNegativeNum(parseFloat(m[3]) - 1); 26 | return connection.sendDiagnostics({ 27 | uri: textDoc.uri, 28 | diagnostics: [{ 29 | source: "vimlsp", 30 | message: m[1], 31 | range: Range.create( 32 | Position.create(line > lines ? lines : line, col), 33 | Position.create(line > lines ? lines : line, col + 1), 34 | ), 35 | severity: DiagnosticSeverity.Error, 36 | }], 37 | }); 38 | } 39 | 40 | // clear diagnostics 41 | connection.sendDiagnostics({ 42 | uri: textDoc.uri, 43 | diagnostics: [], 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /src/handles/documentHighlight.ts: -------------------------------------------------------------------------------- 1 | import {DocumentHighlight, TextDocumentPositionParams} from "vscode-languageserver"; 2 | 3 | import {getWordFromPosition} from "../common/util"; 4 | import { documents } from "../server/documents"; 5 | import {workspace} from "../server/workspaces"; 6 | 7 | export const documentHighlightProvider = ((params: TextDocumentPositionParams): DocumentHighlight[] => { 8 | const { textDocument, position } = params; 9 | const doc = documents.get(textDocument.uri); 10 | if (!doc) { 11 | return []; 12 | } 13 | const words = getWordFromPosition(doc, position); 14 | if (!words) { 15 | return []; 16 | } 17 | 18 | let currentName = words.word; 19 | if (/\./.test(words.right)) { 20 | const tail = words.right.replace(/^[^.]*(\.)/, "$1"); 21 | currentName = words.word.replace(tail, ""); 22 | } 23 | 24 | const defs = workspace.getLocationsByUri(currentName, doc.uri, position, "definition"); 25 | const refs = workspace.getLocationsByUri(currentName, doc.uri, position, "references"); 26 | 27 | return defs.locations.concat(refs.locations) 28 | .map((location) => { 29 | return { 30 | range: location.range, 31 | }; 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/handles/documentSymbol.ts: -------------------------------------------------------------------------------- 1 | import {DocumentSymbolParams, DocumentSymbol, SymbolKind, Range, Position, SymbolInformation} from "vscode-languageserver"; 2 | import * as shvl from "shvl"; 3 | 4 | import {workspace} from "../server/workspaces"; 5 | import {IFunction, IIdentifier} from "../server/buffer"; 6 | import {documents} from "../server/documents"; 7 | import config from "../server/config"; 8 | 9 | export const documentSymbolProvider = async (params: DocumentSymbolParams): Promise => { 10 | const documentSymbols: DocumentSymbol[] = [] 11 | const { textDocument } = params 12 | const buffer = await workspace.getBufferByUri(textDocument.uri) 13 | const document = documents.get(textDocument.uri) 14 | if (!buffer || !document) { 15 | return documentSymbols 16 | } 17 | const globalFunctions = buffer.getGlobalFunctions() 18 | const scriptFunctions = buffer.getScriptFunctions() 19 | const globalVariables = buffer.getGlobalIdentifiers() 20 | const localVariables = buffer.getLocalIdentifiers() 21 | const functions = Object.values(globalFunctions).concat(Object.values(scriptFunctions)).reduce((pre, cur) => { 22 | return pre.concat(cur) 23 | }, []) 24 | let variables = Object.values(globalVariables).concat(Object.values(localVariables)).reduce((pre, cur) => { 25 | return pre.concat(cur) 26 | }, []) 27 | 28 | // hierarchicalDocumentSymbolSupport: false 29 | if (!config.capabilities || !shvl.get(config.capabilities, 'textDocument.documentSymbol.hierarchicalDocumentSymbolSupport')) { 30 | return ([] as (IFunction | IIdentifier)[]).concat(functions,variables).sort((a, b) => { 31 | if (a.startLine === b.startLine) { 32 | return a.startCol - b.startCol 33 | } 34 | return a.startLine - b.startLine 35 | }).map(item => { 36 | const vimRange = (item as IFunction).range 37 | const line = vimRange 38 | ? document.getText(Range.create( Position.create(vimRange.endLine - 1, 0), Position.create(vimRange.endLine, 0))) 39 | : '' 40 | const range = vimRange 41 | ? Range.create( 42 | Position.create(vimRange.startLine - 1, vimRange.startCol - 1), 43 | Position.create(vimRange.endLine - 1, vimRange.endCol - 1 + line.slice(vimRange.endCol - 1).split(' ')[0].length) 44 | ) 45 | : 46 | Range.create( 47 | Position.create(item.startLine - 1, item.startCol - 1), 48 | Position.create(item.startLine, item.startCol - 1 + item.name.length) 49 | ) 50 | return { 51 | name: item.name, 52 | kind: vimRange ? SymbolKind.Function : SymbolKind.Variable, 53 | location: { 54 | uri: textDocument.uri, 55 | range, 56 | } 57 | } 58 | }) 59 | } 60 | 61 | const sortFunctions: IFunction[] = [] 62 | functions.forEach(func => { 63 | if (sortFunctions.length === 0) { 64 | return sortFunctions.push(func) 65 | } 66 | let i = 0; 67 | for (const len = sortFunctions.length; i < len; i += 1) { 68 | const sf = sortFunctions[i] 69 | if (func.range.endLine < sf.range.endLine) { 70 | sortFunctions.splice(i, 0, func) 71 | break 72 | } 73 | } 74 | if (i === sortFunctions.length) { 75 | sortFunctions.push(func) 76 | } 77 | }) 78 | return sortFunctions 79 | .map(func => { 80 | const vimRange = func.range 81 | const line = document.getText(Range.create( 82 | Position.create(vimRange.endLine - 1, 0), 83 | Position.create(vimRange.endLine, 0) 84 | )) 85 | const range = Range.create( 86 | Position.create(vimRange.startLine - 1, vimRange.startCol - 1), 87 | Position.create(vimRange.endLine - 1, vimRange.endCol - 1 + line.slice(vimRange.endCol - 1).split(' ')[0].length) 88 | ) 89 | const ds: DocumentSymbol = { 90 | name: func.name, 91 | kind: SymbolKind.Function, 92 | range, 93 | selectionRange: range, 94 | children: [] 95 | } 96 | variables = variables.filter(v => { 97 | if (v.startLine >= vimRange.startLine && v.startLine <= vimRange.endLine) { 98 | const vRange = Range.create( 99 | Position.create(v.startLine - 1, v.startCol - 1), 100 | Position.create(v.startLine, v.startCol - 1 + v.name.length) 101 | ) 102 | ds.children.push({ 103 | name: v.name, 104 | kind: SymbolKind.Variable, 105 | range: vRange, 106 | selectionRange: vRange 107 | }) 108 | return false 109 | } 110 | return true 111 | }) 112 | return ds 113 | }) 114 | .reduce((res, cur) => { 115 | if (res.length === 0) { 116 | res.push(cur) 117 | } else { 118 | res = res.filter(item => { 119 | if (item.range.start.line >= cur.range.start.line && item.range.end.line <= cur.range.end.line) { 120 | cur.children.push(item) 121 | return false 122 | } 123 | return true 124 | }) 125 | res.push(cur) 126 | } 127 | return res 128 | }, [] as DocumentSymbol[]) 129 | .concat( 130 | variables.map(v => { 131 | const vRange = Range.create( 132 | Position.create(v.startLine - 1, v.startCol - 1), 133 | Position.create(v.startLine, v.startCol - 1 + v.name.length) 134 | ) 135 | return { 136 | name: v.name, 137 | kind: SymbolKind.Variable, 138 | range: vRange, 139 | selectionRange: vRange 140 | } 141 | }) 142 | ) 143 | } 144 | -------------------------------------------------------------------------------- /src/handles/foldingRange.ts: -------------------------------------------------------------------------------- 1 | import {FoldingRange, FoldingRangeParams} from "vscode-languageserver"; 2 | 3 | import {workspace} from "../server/workspaces"; 4 | 5 | export const foldingRangeProvider = async (params: FoldingRangeParams) => { 6 | const res: FoldingRange[] = []; 7 | const { textDocument } = params; 8 | const buffer = await workspace.getBufferByUri(textDocument.uri); 9 | if (!buffer) { 10 | return res; 11 | } 12 | const globalFunctions = buffer.getGlobalFunctions(); 13 | const scriptFunctions = buffer.getScriptFunctions(); 14 | return Object.values(globalFunctions).concat(Object.values(scriptFunctions)) 15 | .reduce((pre, cur) => { 16 | return pre.concat(cur); 17 | }, []) 18 | .map((func) => { 19 | return { 20 | startLine: func.startLine - 1, 21 | startCharacter: func.startCol - 1, 22 | endLine: func.endLine - 1, 23 | endCharacter: func.endCol - 1, 24 | kind: "region", 25 | }; 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/handles/hover.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Hover, 3 | TextDocumentPositionParams, 4 | } from "vscode-languageserver"; 5 | import { getWordFromPosition } from "../common/util"; 6 | import { builtinDocs } from "../server/builtin"; 7 | import { documents } from "../server/documents"; 8 | 9 | export const hoverProvider = ( 10 | params: TextDocumentPositionParams, 11 | ): Hover | undefined => { 12 | const { textDocument, position } = params; 13 | const doc = documents.get(textDocument.uri); 14 | if (!doc) { 15 | return; 16 | } 17 | 18 | const words = getWordFromPosition(doc, position); 19 | 20 | if (!words) { 21 | return; 22 | } 23 | 24 | return builtinDocs.getHoverDocument( 25 | words.word, 26 | words.wordLeft, 27 | words.wordRight, 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/handles/references.ts: -------------------------------------------------------------------------------- 1 | import { Location, ReferenceParams } from "vscode-languageserver"; 2 | import { getWordFromPosition } from "../common/util"; 3 | import { documents } from "../server/documents"; 4 | import { workspace } from "../server/workspaces"; 5 | 6 | export const referencesProvider = (params: ReferenceParams): Location[] | null => { 7 | const { textDocument, position } = params; 8 | const doc = documents.get(textDocument.uri); 9 | if (!doc) { 10 | return null; 11 | } 12 | const words = getWordFromPosition(doc, position); 13 | if (!words) { 14 | return null; 15 | } 16 | let currentName = words.word; 17 | if (/\./.test(words.right)) { 18 | const tail = words.right.replace(/^[^.]*(\.)/, "$1"); 19 | currentName = words.word.replace(tail, ""); 20 | } 21 | return workspace.getLocations(currentName, doc.uri, position, "references").locations; 22 | }; 23 | -------------------------------------------------------------------------------- /src/handles/rename.ts: -------------------------------------------------------------------------------- 1 | import { Position, Range, RenameParams, TextDocumentPositionParams, TextEdit, WorkspaceEdit } from "vscode-languageserver"; 2 | import { getWordFromPosition } from "../common/util"; 3 | import { documents } from "../server/documents"; 4 | import { workspace } from "../server/workspaces"; 5 | 6 | export const prepareProvider = (params: TextDocumentPositionParams): { 7 | range: Range 8 | placeholder: string, 9 | } | null => { 10 | const { textDocument, position } = params; 11 | const doc = documents.get(textDocument.uri); 12 | if (!doc) { 13 | return null; 14 | } 15 | const words = getWordFromPosition(doc, position); 16 | if (!words) { 17 | return null; 18 | } 19 | 20 | let currentName = words.word; 21 | if (/\./.test(words.right)) { 22 | const tail = words.right.replace(/^[^.]*(\.)/, "$1"); 23 | currentName = words.word.replace(tail, ""); 24 | } 25 | 26 | return { 27 | range: Range.create( 28 | Position.create(position.line, position.character - words.left.length), 29 | Position.create(position.line, position.character + words.right.length - 1), 30 | ), 31 | placeholder: currentName, 32 | }; 33 | }; 34 | 35 | export const renameProvider = (params: RenameParams): WorkspaceEdit | null => { 36 | const { textDocument, position, newName } = params; 37 | const doc = documents.get(textDocument.uri); 38 | if (!doc) { 39 | return null; 40 | } 41 | const words = getWordFromPosition(doc, position); 42 | if (!words) { 43 | return null; 44 | } 45 | 46 | let currentName = words.word; 47 | if (/\./.test(words.right)) { 48 | const tail = words.right.replace(/^[^.]*(\.)/, "$1"); 49 | currentName = words.word.replace(tail, ""); 50 | } 51 | 52 | const changes: Record = {}; 53 | let isChange = false; 54 | 55 | workspace.getLocations(currentName, doc.uri, position, "definition").locations 56 | .forEach((l) => { 57 | isChange = true; 58 | if (!changes[l.uri] || !Array.isArray(changes[l.uri])) { 59 | changes[l.uri] = []; 60 | } 61 | changes[l.uri].push({ 62 | newText: /^a:/.test(newName) ? newName.slice(2) : newName, 63 | range: l.range, 64 | }); 65 | }); 66 | 67 | const refs = workspace.getLocations(currentName, doc.uri, position, "references"); 68 | 69 | refs.locations.forEach((l) => { 70 | isChange = true; 71 | if (!changes[l.uri] || !Array.isArray(changes[l.uri])) { 72 | changes[l.uri] = []; 73 | } 74 | changes[l.uri].push({ 75 | newText: refs.isFunArg ? `a:${newName}` : newName, 76 | range: l.range, 77 | }); 78 | }); 79 | 80 | if (isChange) { 81 | return { 82 | changes, 83 | }; 84 | } 85 | return null; 86 | }; 87 | -------------------------------------------------------------------------------- /src/handles/selectionRange.ts: -------------------------------------------------------------------------------- 1 | import {SelectionRangeParams, SelectionRange, Range, Position} from "vscode-languageserver"; 2 | 3 | import {workspace} from "../server/workspaces"; 4 | import {documents} from "../server/documents"; 5 | 6 | export const selectionRangeProvider = async (params: SelectionRangeParams): Promise => { 7 | const selectRanges: SelectionRange[] = []; 8 | const { textDocument, positions } = params; 9 | if (!positions || positions.length === 0) { 10 | return selectRanges 11 | } 12 | const buffer = await workspace.getBufferByUri(textDocument.uri); 13 | const document = documents.get(textDocument.uri) 14 | if (!buffer || !document) { 15 | return selectRanges; 16 | } 17 | const vimRanges = buffer.getRanges() 18 | if (vimRanges.length === 0) { 19 | return selectRanges 20 | } 21 | 22 | let range = Range.create(positions[0], positions[0]) 23 | if (positions.length > 1) { 24 | range = Range.create(positions[0], positions[positions.length - 1]) 25 | } 26 | let ranges: Range[] = [] 27 | vimRanges.forEach(vimRange => { 28 | const line = document.getText(Range.create( 29 | Position.create(vimRange.endLine - 1, 0), 30 | Position.create(vimRange.endLine, 0) 31 | )) 32 | const newRange = Range.create( 33 | Position.create(vimRange.startLine - 1, vimRange.startCol - 1), 34 | Position.create(vimRange.endLine - 1, vimRange.endCol - 1 + line.slice(vimRange.endCol - 1).split(' ')[0].length) 35 | ) 36 | if (range.start.line >= newRange.start.line && range.end.line <= newRange.end.line) { 37 | if (ranges.length === 0) { 38 | ranges.push(newRange) 39 | } else { 40 | let i = 0; 41 | for(const len = ranges.length; i < len; i++) { 42 | if (ranges[i].start.line <= newRange.start.line && ranges[i].end.line >= newRange.end.line) { 43 | ranges.splice(i, 0, newRange) 44 | break 45 | } 46 | } 47 | if (i === ranges.length) { 48 | ranges.push(newRange) 49 | } 50 | } 51 | } 52 | }) 53 | if (ranges.length) { 54 | if (ranges.length > 1) { 55 | ranges = ranges.filter(newRange => { 56 | return range.start.line !== newRange.start.line || range.end.line !== newRange.end.line 57 | }) 58 | } 59 | selectRanges.push( 60 | ranges.reverse().reduce((pre, cur, idx) => { 61 | if (idx === 0) { 62 | return pre 63 | } 64 | return { 65 | range: cur, 66 | parent: pre 67 | } 68 | }, {range: ranges[0]} as SelectionRange) 69 | ) 70 | } 71 | return selectRanges 72 | }; 73 | -------------------------------------------------------------------------------- /src/handles/signatureHelp.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Position, 3 | Range, 4 | SignatureHelp, 5 | TextDocumentPositionParams, 6 | } from "vscode-languageserver"; 7 | import { commentPattern } from "../common/patterns"; 8 | import { builtinDocs } from "../server/builtin"; 9 | import { documents } from "../server/documents"; 10 | 11 | export const signatureHelpProvider = 12 | (params: TextDocumentPositionParams): SignatureHelp | undefined => { 13 | const { textDocument, position } = params; 14 | const doc = documents.get(textDocument.uri); 15 | if (!doc) { 16 | return; 17 | } 18 | 19 | const currentLine = doc.getText( 20 | Range.create( 21 | Position.create(position.line, 0), 22 | Position.create(position.line + 1, 0), 23 | ), 24 | ); 25 | 26 | // comment line 27 | if (commentPattern.test(currentLine)) { 28 | return; 29 | } 30 | 31 | const preSegment = currentLine.slice(0, position.character); 32 | 33 | const m = preSegment.match(/([\w#&:]+?)[ \t]*\([ \t]*([^()]*?)$/); 34 | if (!m) { 35 | return; 36 | } 37 | const functionName = m["1"]; 38 | const placeIdx = m[0].split(",").length - 1; 39 | return builtinDocs.getSignatureHelpByName(functionName, placeIdx); 40 | }; 41 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as shvl from "shvl"; 2 | import { InitializeParams, TextDocumentSyncKind } from "vscode-languageserver"; 3 | 4 | import { projectRootPatterns } from "./common/constant"; 5 | import { IConfig, IDiagnostic, IIndexes, ISuggest } from "./common/types"; 6 | import { completionProvider } from "./handles/completion"; 7 | import { completionResolveProvider } from "./handles/completionResolve"; 8 | import { definitionProvider } from "./handles/definition"; 9 | import { documentHighlightProvider } from "./handles/documentHighlight"; 10 | import {foldingRangeProvider} from "./handles/foldingRange"; 11 | import { hoverProvider } from "./handles/hover"; 12 | import { referencesProvider } from "./handles/references"; 13 | import { prepareProvider, renameProvider } from "./handles/rename"; 14 | import { signatureHelpProvider } from "./handles/signatureHelp"; 15 | import { builtinDocs } from "./server/builtin"; 16 | import config from "./server/config"; 17 | import { connection } from "./server/connection"; 18 | import { documents } from "./server/documents"; 19 | import { next, unsubscribe } from "./server/parser"; 20 | import {selectionRangeProvider} from "./handles/selectionRange"; 21 | import {documentSymbolProvider} from "./handles/documentSymbol"; 22 | import logger from "./common/logger"; 23 | 24 | // lsp initialize 25 | connection.onInitialize((param: InitializeParams) => { 26 | const renamePrepareSupport = param.capabilities.textDocument && param.capabilities.textDocument.rename && param.capabilities.textDocument.rename.prepareSupport === true; 27 | const { initializationOptions = {} } = param; 28 | const { 29 | isNeovim, 30 | iskeyword, 31 | runtimepath, 32 | vimruntime, 33 | diagnostic, 34 | suggest, 35 | indexes, 36 | }: { 37 | isNeovim: boolean 38 | iskeyword: string 39 | runtimepath: string 40 | vimruntime: string 41 | diagnostic: IDiagnostic 42 | suggest: ISuggest 43 | indexes: IIndexes, 44 | } = initializationOptions; 45 | 46 | const runtimepaths = runtimepath ? runtimepath.split(",") : []; 47 | 48 | // config by user's initializationOptions 49 | const conf: IConfig = { 50 | isNeovim: isNeovim || false, 51 | iskeyword: iskeyword || "", 52 | runtimepath: runtimepaths, 53 | vimruntime: (vimruntime || "").trim(), 54 | diagnostic: { 55 | enable: true, 56 | ...(diagnostic || {}), 57 | }, 58 | snippetSupport: shvl.get(param, "capabilities.textDocument.completion.completionItem.snippetSupport"), 59 | suggest: { 60 | fromRuntimepath: false, 61 | fromVimruntime: true, 62 | ...(suggest || {}), 63 | }, 64 | indexes: { 65 | runtimepath: true, 66 | gap: 100, 67 | count: 1, 68 | projectRootPatterns, 69 | ...(indexes || {}), 70 | }, 71 | capabilities: param.capabilities 72 | }; 73 | 74 | // init config 75 | config.init(conf); 76 | 77 | // init builtin docs 78 | builtinDocs.init(); 79 | 80 | return { 81 | capabilities: { 82 | textDocumentSync: TextDocumentSyncKind.Incremental, 83 | documentHighlightProvider: true, 84 | foldingRangeProvider: true, 85 | selectionRangeProvider: true, 86 | documentSymbolProvider: true, 87 | hoverProvider: true, 88 | completionProvider: { 89 | triggerCharacters: [".", ":", "#", "[", "&", "$", "<", '"', "'"], 90 | resolveProvider: true, 91 | }, 92 | signatureHelpProvider: { 93 | triggerCharacters: ["(", ","], 94 | }, 95 | definitionProvider: true, 96 | referencesProvider: true, 97 | renameProvider: renamePrepareSupport ? { 98 | prepareProvider: true, 99 | } : true, 100 | }, 101 | }; 102 | }); 103 | 104 | // document change or open 105 | documents.onDidChangeContent(( change ) => { 106 | next(change.document); 107 | }); 108 | 109 | documents.onDidClose((evt) => { 110 | unsubscribe(evt.document); 111 | }); 112 | 113 | // listen for document's open/close/change 114 | documents.listen(connection); 115 | 116 | // handle completion 117 | connection.onCompletion(completionProvider); 118 | 119 | // handle completion resolve 120 | connection.onCompletionResolve(completionResolveProvider); 121 | 122 | // handle signature help 123 | connection.onSignatureHelp(signatureHelpProvider); 124 | 125 | // handle hover 126 | connection.onHover(hoverProvider); 127 | 128 | // handle definition request 129 | connection.onDefinition(definitionProvider); 130 | 131 | // handle references 132 | connection.onReferences(referencesProvider); 133 | 134 | // handle rename 135 | connection.onPrepareRename(prepareProvider); 136 | connection.onRenameRequest(renameProvider); 137 | 138 | // document highlight 139 | connection.onDocumentHighlight(documentHighlightProvider); 140 | 141 | // folding range 142 | connection.onFoldingRanges(foldingRangeProvider); 143 | 144 | // select range 145 | connection.onSelectionRanges(selectionRangeProvider); 146 | 147 | // document symbols 148 | connection.onDocumentSymbol(documentSymbolProvider); 149 | 150 | connection.onNotification('$/change/iskeyword', (iskeyword: string) => { 151 | config.changeByKey('iskeyword', iskeyword) 152 | }) 153 | 154 | // lsp start 155 | connection.listen(); 156 | -------------------------------------------------------------------------------- /src/lib/vimparser.d.ts: -------------------------------------------------------------------------------- 1 | export declare interface IPos { 2 | lnum: number; 3 | col: number; 4 | offset: number; 5 | } 6 | 7 | export declare interface INode { 8 | type: number; 9 | pos: IPos; 10 | body: INode[]; 11 | ea: { 12 | linepos: IPos 13 | cmdpos: IPos 14 | argpos: IPos 15 | cmd: { 16 | name: string, 17 | }, 18 | }; 19 | cond?: INode; 20 | elseif?: INode[]; 21 | _else?: INode; 22 | op?: string; 23 | catch?: INode[]; 24 | _finally?: INode; 25 | left: INode; 26 | right: INode; 27 | rlist: INode[]; 28 | str: string; 29 | value?: any; 30 | endfunction?: INode; 31 | endif?: INode; 32 | endfor?: INode; 33 | endwhile?: INode; 34 | list?: INode[]; 35 | } 36 | 37 | export declare class StringReader { 38 | public buf: string[]; 39 | public pos: [number, number, number][]; 40 | constructor(lines: string[]) 41 | } 42 | 43 | // tslint:disable-next-line 44 | export declare class VimLParser { 45 | constructor(isNeovim: boolean) 46 | public parse(stringReader: StringReader): INode; 47 | } 48 | -------------------------------------------------------------------------------- /src/script/build-docs.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * vim builtin completion items 3 | * 4 | * 1. functions 5 | * 2. options 6 | * 3. variables 7 | * 4. commands 8 | * 5. has features 9 | * 6. expand Keyword 10 | */ 11 | import { readFile, writeFileSync } from "fs"; 12 | import { join } from "path"; 13 | 14 | import { 15 | CompletionItem, 16 | CompletionItemKind, 17 | InsertTextFormat, 18 | } from "vscode-languageserver"; 19 | import { sortTexts } from "../common/constant"; 20 | import { pcb } from "../common/util"; 21 | 22 | interface IConfig { 23 | vimruntime: string; 24 | } 25 | 26 | const EVAL_PATH = "/doc/eval.txt"; 27 | const OPTIONS_PATH = "/doc/options.txt"; 28 | const INDEX_PATH = "/doc/index.txt"; 29 | const API_PATH = "/doc/api.txt"; 30 | const AUTOCMD_PATH = "/doc/autocmd.txt"; 31 | const POPUP_PATH = "/doc/popup.txt"; 32 | const CHANNEL_PATH = "/doc/channel.txt"; 33 | const TEXTPROP_PATH = "/doc/textprop.txt"; 34 | const TERMINAL_PATH = "/doc/terminal.txt"; 35 | const TESTING_PATH = "/doc/testing.txt"; 36 | 37 | class Server { 38 | 39 | // completion items 40 | public vimPredefinedVariablesItems: CompletionItem[] = []; 41 | public vimOptionItems: CompletionItem[] = []; 42 | public vimBuiltinFunctionItems: CompletionItem[] = []; 43 | public vimCommandItems: CompletionItem[] = []; 44 | public vimFeatureItems: CompletionItem[] = []; 45 | public vimExpandKeywordItems: CompletionItem[] = []; 46 | public vimAutocmdItems: CompletionItem[] = []; 47 | 48 | // documents 49 | public vimBuiltFunctionDocuments: Record = {}; 50 | public vimOptionDocuments: Record = {}; 51 | public vimPredefinedVariableDocuments: Record = {}; 52 | public vimCommandDocuments: Record = {}; 53 | public vimFeatureDocuments: Record = {}; 54 | public expandKeywordDocuments: Record = {}; 55 | 56 | // signature help 57 | public vimBuiltFunctionSignatureHelp: Record = {}; 58 | 59 | // raw docs 60 | private text: Record = {}; 61 | constructor(private config: IConfig) {} 62 | 63 | public async build() { 64 | const { vimruntime } = this.config; 65 | if (vimruntime) { 66 | const paths = [ 67 | EVAL_PATH, 68 | OPTIONS_PATH, 69 | INDEX_PATH, 70 | API_PATH, 71 | AUTOCMD_PATH, 72 | POPUP_PATH, 73 | CHANNEL_PATH, 74 | TEXTPROP_PATH, 75 | TERMINAL_PATH, 76 | TESTING_PATH, 77 | ]; 78 | // tslint:disable-next-line: prefer-for-of 79 | for (let index = 0; index < paths.length; index++) { 80 | const p = join(vimruntime, paths[index]); 81 | const [err, data]: [Error, Buffer] = await pcb(readFile)(p, "utf-8"); 82 | if (err) { 83 | // tslint:disable-next-line: no-console 84 | console.error(`[vimls]: read ${p} error: ${ err.message}`); 85 | } 86 | this.text[paths[index]] = (data && data.toString().split("\n")) || []; 87 | } 88 | this.resolveVimPredefinedVariables(); 89 | this.resolveVimOptions(); 90 | this.resolveBuiltinFunctions(); 91 | this.resolveBuiltinFunctionsDocument(); 92 | this.resolveBuiltinVimPopupFunctionsDocument(); 93 | this.resolveBuiltinVimChannelFunctionsDocument(); 94 | this.resolveBuiltinVimJobFunctionsDocument(); 95 | this.resolveBuiltinVimTextpropFunctionsDocument(); 96 | this.resolveBuiltinVimTerminalFunctionsDocument(); 97 | this.resolveBuiltinVimTestingFunctionsDocument(); 98 | this.resolveBuiltinNvimFunctions(); 99 | this.resolveExpandKeywords(); 100 | this.resolveVimCommands(); 101 | this.resolveVimFeatures(); 102 | this.resolveVimAutocmds(); 103 | } 104 | } 105 | 106 | public serialize() { 107 | const str = JSON.stringify({ 108 | completionItems: { 109 | commands: this.vimCommandItems, 110 | functions: this.vimBuiltinFunctionItems, 111 | variables: this.vimPredefinedVariablesItems, 112 | options: this.vimOptionItems, 113 | features: this.vimFeatureItems, 114 | expandKeywords: this.vimExpandKeywordItems, 115 | autocmds: this.vimAutocmdItems, 116 | }, 117 | signatureHelp: this.vimBuiltFunctionSignatureHelp, 118 | documents: { 119 | commands: this.vimCommandDocuments, 120 | functions: this.vimBuiltFunctionDocuments, 121 | variables: this.vimPredefinedVariableDocuments, 122 | options: this.vimOptionDocuments, 123 | features: this.vimFeatureDocuments, 124 | expandKeywords: this.expandKeywordDocuments, 125 | }, 126 | }, null, 2); 127 | writeFileSync("./src/docs/builtin-docs.json", str, "utf-8"); 128 | } 129 | 130 | private formatFunctionSnippets(fname: string, snippets: string): string { 131 | if (snippets === "") { 132 | return `${fname}(\${0})`; 133 | } 134 | let idx = 0; 135 | if (/^\[.+\]/.test(snippets)) { 136 | return `${fname}(\${1})\${0}`; 137 | } 138 | const str = snippets.split("[")[0].trim().replace(/\{?(\w+)\}?/g, (m, g1) => { 139 | return `\${${idx += 1}:${g1}}`; 140 | }); 141 | return `${fname}(${str})\${0}`; 142 | } 143 | 144 | // get vim predefined variables from vim document eval.txt 145 | private resolveVimPredefinedVariables() { 146 | const evalText = this.text[EVAL_PATH] || []; 147 | let isMatchLine = false; 148 | let completionItem: CompletionItem; 149 | for (const line of evalText) { 150 | if (!isMatchLine) { 151 | if (/\*vim-variable\*/.test(line)) { 152 | isMatchLine = true; 153 | } 154 | continue; 155 | } else { 156 | const m = line.match(/^(v:[^ \t]+)[ \t]+([^ ].*)$/); 157 | if (m) { 158 | if (completionItem) { 159 | this.vimPredefinedVariablesItems.push(completionItem); 160 | this.vimPredefinedVariableDocuments[completionItem.label].pop(); 161 | completionItem = undefined; 162 | } 163 | const label = m[1]; 164 | completionItem = { 165 | label, 166 | kind: CompletionItemKind.Variable, 167 | sortText: sortTexts.four, 168 | insertText: label.slice(2), 169 | insertTextFormat: InsertTextFormat.PlainText, 170 | }; 171 | if (!this.vimPredefinedVariableDocuments[label]) { 172 | this.vimPredefinedVariableDocuments[label] = []; 173 | } 174 | this.vimPredefinedVariableDocuments[label].push(m[2]); 175 | 176 | } else if (/^\s*$/.test(line) && completionItem) { 177 | this.vimPredefinedVariablesItems.push(completionItem); 178 | completionItem = undefined; 179 | } else if (completionItem) { 180 | this.vimPredefinedVariableDocuments[completionItem.label].push( 181 | line, 182 | ); 183 | } else if (/===============/.test(line)) { 184 | break; 185 | } 186 | } 187 | } 188 | } 189 | 190 | // get vim options from vim document options.txt 191 | private resolveVimOptions() { 192 | const optionsText: string[] = this.text[OPTIONS_PATH] || []; 193 | let isMatchLine = false; 194 | let completionItem: CompletionItem; 195 | for (const line of optionsText) { 196 | if (!isMatchLine) { 197 | if (/\*'aleph'\*/.test(line)) { 198 | isMatchLine = true; 199 | } 200 | continue; 201 | } else { 202 | const m = line.match(/^'([^']+)'[ \t]+('[^']+')?[ \t]+([^ \t].*)$/); 203 | if (m) { 204 | const label = m[1]; 205 | completionItem = { 206 | label, 207 | kind: CompletionItemKind.Property, 208 | detail: m[3].trim().split(/[ \t]/)[0], 209 | documentation: "", 210 | sortText: "00004", 211 | insertText: m[1], 212 | insertTextFormat: InsertTextFormat.PlainText, 213 | }; 214 | if (!this.vimOptionDocuments[label]) { 215 | this.vimOptionDocuments[label] = []; 216 | } 217 | this.vimOptionDocuments[label].push(m[3]); 218 | } else if (/^\s*$/.test(line) && completionItem) { 219 | this.vimOptionItems.push(completionItem); 220 | completionItem = undefined; 221 | } else if (completionItem) { 222 | this.vimOptionDocuments[completionItem.label].push( 223 | line, 224 | ); 225 | } 226 | } 227 | } 228 | } 229 | 230 | // get vim builtin function from document eval.txt 231 | private resolveBuiltinFunctions() { 232 | const evalText = this.text[EVAL_PATH] || []; 233 | let isMatchLine = false; 234 | let completionItem: CompletionItem; 235 | for (const line of evalText) { 236 | if (!isMatchLine) { 237 | if (/\*functions\*/.test(line)) { 238 | isMatchLine = true; 239 | } 240 | continue; 241 | } else { 242 | const m = line.match(/^((\w+)\(([^)]*)\))[ \t]*([^ \t].*)?$/); 243 | if (m) { 244 | if (completionItem) { 245 | this.vimBuiltinFunctionItems.push(completionItem); 246 | } 247 | const label = m[2]; 248 | completionItem = { 249 | label, 250 | kind: CompletionItemKind.Function, 251 | detail: (m[4] || "").split(/[ \t]/)[0], 252 | sortText: "00004", 253 | insertText: this.formatFunctionSnippets(m[2], m[3]), 254 | insertTextFormat: InsertTextFormat.Snippet, 255 | }; 256 | this.vimBuiltFunctionSignatureHelp[label] = [ 257 | m[3], 258 | (m[4] || "").split(/[ \t]/)[0], 259 | ]; 260 | } else if (/^[ \t]*$/.test(line)) { 261 | if (completionItem) { 262 | this.vimBuiltinFunctionItems.push(completionItem); 263 | completionItem = undefined; 264 | break; 265 | } 266 | } else if (completionItem) { 267 | if (completionItem.detail === "") { 268 | completionItem.detail = line.trim().split(/[ \t]/)[0]; 269 | if (this.vimBuiltFunctionSignatureHelp[completionItem.label]) { 270 | this.vimBuiltFunctionSignatureHelp[completionItem.label][1] = line.trim().split(/[ \t]/)[0]; 271 | } 272 | } 273 | } 274 | } 275 | } 276 | } 277 | 278 | private resolveBuiltinFunctionsDocument() { 279 | const evalText = this.text[EVAL_PATH] || []; 280 | let isMatchLine = false; 281 | let label: string = ""; 282 | for (let idx = 0; idx < evalText.length; idx++) { 283 | const line = evalText[idx]; 284 | if (!isMatchLine) { 285 | if (/\*abs\(\)\*/.test(line)) { 286 | isMatchLine = true; 287 | idx -= 1; 288 | } 289 | continue; 290 | } else { 291 | const m = line.match(/^((\w+)\(([^)]*)\))[ \t]*([^ \t].*)?$/); 292 | if (m) { 293 | if (label) { 294 | this.vimBuiltFunctionDocuments[label].pop(); 295 | } 296 | label = m[2]; 297 | if (!this.vimBuiltFunctionDocuments[label]) { 298 | this.vimBuiltFunctionDocuments[label] = []; 299 | } 300 | } else if (/^[ \t]*\*string-match\*[ \t]*$/.test(line)) { 301 | if (label) { 302 | this.vimBuiltFunctionDocuments[label].pop(); 303 | } 304 | break; 305 | } else if (label) { 306 | this.vimBuiltFunctionDocuments[label].push(line); 307 | } 308 | } 309 | } 310 | } 311 | 312 | private resolveBuiltinVimPopupFunctionsDocument() { 313 | const popupText = this.text[POPUP_PATH] || []; 314 | let isMatchLine = false; 315 | let label: string = ""; 316 | for (let idx = 0; idx < popupText.length; idx++) { 317 | const line = popupText[idx]; 318 | if (!isMatchLine) { 319 | if (/^DETAILS\s+\*popup-function-details\*/.test(line)) { 320 | isMatchLine = true; 321 | idx += 1; 322 | } 323 | continue; 324 | } else { 325 | const m = line.match(/^((\w+)\(([^)]*)\))[ \t]*([^ \t].*)?$/); 326 | if (m) { 327 | if (label) { 328 | this.vimBuiltFunctionDocuments[label].pop(); 329 | } 330 | label = m[2]; 331 | if (!this.vimBuiltFunctionDocuments[label]) { 332 | this.vimBuiltFunctionDocuments[label] = []; 333 | } 334 | } else if (/^=+$/.test(line)) { 335 | if (label) { 336 | this.vimBuiltFunctionDocuments[label].pop(); 337 | } 338 | break; 339 | } else if (label) { 340 | this.vimBuiltFunctionDocuments[label].push(line); 341 | } 342 | } 343 | } 344 | } 345 | 346 | private resolveBuiltinVimChannelFunctionsDocument() { 347 | const channelText = this.text[CHANNEL_PATH] || []; 348 | let isMatchLine = false; 349 | let label: string = ""; 350 | for (let idx = 0; idx < channelText.length; idx++) { 351 | const line = channelText[idx]; 352 | if (!isMatchLine) { 353 | if (/^8\.\sChannel\sfunctions\sdetails\s+\*channel-functions-details\*/.test(line)) { 354 | isMatchLine = true; 355 | idx += 1; 356 | } 357 | continue; 358 | } else { 359 | const m = line.match(/^((\w+)\(([^)]*)\))[ \t]*([^ \t].*)?$/); 360 | if (m) { 361 | if (label) { 362 | this.vimBuiltFunctionDocuments[label].pop(); 363 | } 364 | label = m[2]; 365 | if (!this.vimBuiltFunctionDocuments[label]) { 366 | this.vimBuiltFunctionDocuments[label] = []; 367 | } 368 | } else if (/^=+$/.test(line)) { 369 | if (label) { 370 | this.vimBuiltFunctionDocuments[label].pop(); 371 | } 372 | break; 373 | } else if (label) { 374 | this.vimBuiltFunctionDocuments[label].push(line); 375 | } 376 | } 377 | } 378 | } 379 | 380 | private resolveBuiltinVimJobFunctionsDocument() { 381 | const channelText = this.text[CHANNEL_PATH] || []; 382 | let isMatchLine = false; 383 | let label: string = ""; 384 | for (let idx = 0; idx < channelText.length; idx++) { 385 | const line = channelText[idx]; 386 | if (!isMatchLine) { 387 | if (/^11\.\sJob\sfunctions\s+\*job-functions-details\*/.test(line)) { 388 | isMatchLine = true; 389 | idx += 1; 390 | } 391 | continue; 392 | } else { 393 | const m = line.match(/^((\w+)\(([^)]*)\))[ \t]*([^ \t].*)?$/); 394 | if (m) { 395 | if (label) { 396 | this.vimBuiltFunctionDocuments[label].pop(); 397 | } 398 | label = m[2]; 399 | if (!this.vimBuiltFunctionDocuments[label]) { 400 | this.vimBuiltFunctionDocuments[label] = []; 401 | } 402 | } else if (/^=+$/.test(line)) { 403 | if (label) { 404 | this.vimBuiltFunctionDocuments[label].pop(); 405 | } 406 | break; 407 | } else if (label) { 408 | this.vimBuiltFunctionDocuments[label].push(line); 409 | } 410 | } 411 | } 412 | } 413 | 414 | private resolveBuiltinVimTextpropFunctionsDocument() { 415 | const textpropText = this.text[TEXTPROP_PATH] || []; 416 | let isMatchLine = false; 417 | let label: string = ""; 418 | // tslint:disable-next-line: prefer-for-of 419 | for (let idx = 0; idx < textpropText.length; idx++) { 420 | const line = textpropText[idx]; 421 | if (!isMatchLine) { 422 | if (/^\s+\*prop_add\(\)\*\s\*E965/.test(line)) { 423 | isMatchLine = true; 424 | } 425 | continue; 426 | } else { 427 | const m = line.match(/^((\w+)\(([^)]*)\))[ \t]*([^ \t].*)?$/); 428 | if (m) { 429 | if (label) { 430 | this.vimBuiltFunctionDocuments[label].pop(); 431 | } 432 | label = m[2]; 433 | if (!this.vimBuiltFunctionDocuments[label]) { 434 | this.vimBuiltFunctionDocuments[label] = []; 435 | } 436 | } else if (/^=+$/.test(line)) { 437 | if (label) { 438 | this.vimBuiltFunctionDocuments[label].pop(); 439 | } 440 | break; 441 | } else if (label) { 442 | this.vimBuiltFunctionDocuments[label].push(line); 443 | } 444 | } 445 | } 446 | } 447 | 448 | private resolveBuiltinVimTerminalFunctionsDocument() { 449 | const terminalText = this.text[TERMINAL_PATH] || []; 450 | let isMatchLine = false; 451 | let label: string = ""; 452 | // tslint:disable-next-line: prefer-for-of 453 | for (let idx = 0; idx < terminalText.length; idx++) { 454 | const line = terminalText[idx]; 455 | if (!isMatchLine) { 456 | if (/^\s+\*term_dumpdiff\(\)/.test(line)) { 457 | isMatchLine = true; 458 | } 459 | continue; 460 | } else { 461 | const m = line.match(/^((\w+)\(([^)]*)\))[ \t]*([^ \t].*)?$/); 462 | if (m) { 463 | if (label) { 464 | this.vimBuiltFunctionDocuments[label].pop(); 465 | } 466 | label = m[2]; 467 | if (!this.vimBuiltFunctionDocuments[label]) { 468 | this.vimBuiltFunctionDocuments[label] = []; 469 | } 470 | } else if (/^=+$/.test(line)) { 471 | if (label) { 472 | this.vimBuiltFunctionDocuments[label].pop(); 473 | } 474 | break; 475 | } else if (label) { 476 | this.vimBuiltFunctionDocuments[label].push(line); 477 | } 478 | } 479 | } 480 | } 481 | 482 | private resolveBuiltinVimTestingFunctionsDocument() { 483 | const testingText = this.text[TESTING_PATH] || []; 484 | let isMatchLine = false; 485 | let label: string = ""; 486 | // tslint:disable-next-line: prefer-for-of 487 | for (let idx = 0; idx < testingText.length; idx++) { 488 | const line = testingText[idx]; 489 | if (!isMatchLine) { 490 | if (/^2\.\sTest\sfunctions\s+\*test-functions-details\*/.test(line)) { 491 | isMatchLine = true; 492 | idx += 1; 493 | } 494 | continue; 495 | } else { 496 | const m = line.match(/^((\w+)\(([^)]*)\))[ \t]*([^ \t].*)?$/); 497 | if (m) { 498 | if (label) { 499 | this.vimBuiltFunctionDocuments[label].pop(); 500 | } 501 | label = m[2]; 502 | if (!this.vimBuiltFunctionDocuments[label]) { 503 | this.vimBuiltFunctionDocuments[label] = []; 504 | } 505 | } else if (/^=+$/.test(line)) { 506 | if (label) { 507 | this.vimBuiltFunctionDocuments[label].pop(); 508 | } 509 | break; 510 | } else if (label) { 511 | this.vimBuiltFunctionDocuments[label].push(line); 512 | } 513 | } 514 | } 515 | } 516 | 517 | private resolveBuiltinNvimFunctions() { 518 | const evalText = this.text[API_PATH] || []; 519 | let completionItem: CompletionItem; 520 | const pattern = /^((nvim_\w+)\(([^)]*)\))[ \t]*/m; 521 | for (let idx = 0; idx < evalText.length; idx++) { 522 | const line = evalText[idx]; 523 | let m = line.match(pattern); 524 | if (!m && evalText[idx + 1]) { 525 | m = [line, evalText[idx + 1].trim()].join(" ").match(pattern); 526 | if (m) { 527 | idx++; 528 | } 529 | } 530 | if (m) { 531 | if (completionItem) { 532 | this.vimBuiltinFunctionItems.push( 533 | completionItem, 534 | ); 535 | if (this.vimBuiltFunctionDocuments[completionItem.label]) { 536 | this.vimBuiltFunctionDocuments[completionItem.label].pop(); 537 | } 538 | } 539 | const label = m[2]; 540 | completionItem = { 541 | label, 542 | kind: CompletionItemKind.Function, 543 | detail: "", 544 | documentation: "", 545 | sortText: "00004", 546 | insertText: this.formatFunctionSnippets(m[2], m[3]), 547 | insertTextFormat: InsertTextFormat.Snippet, 548 | }; 549 | if (!this.vimBuiltFunctionDocuments[label]) { 550 | this.vimBuiltFunctionDocuments[label] = []; 551 | } 552 | this.vimBuiltFunctionSignatureHelp[label] = [ 553 | m[3], 554 | "", 555 | ]; 556 | } else if (/^(================|[ \t]*vim:tw=78:ts=8:ft=help:norl:)/.test(line)) { 557 | if (completionItem) { 558 | this.vimBuiltinFunctionItems.push( 559 | completionItem, 560 | ); 561 | if (this.vimBuiltFunctionDocuments[completionItem.label]) { 562 | this.vimBuiltFunctionDocuments[completionItem.label].pop(); 563 | } 564 | completionItem = undefined; 565 | } 566 | } else if (completionItem && !/^[ \t]\*nvim(_\w+)+\(\)\*\s*$/.test(line)) { 567 | this.vimBuiltFunctionDocuments[completionItem.label].push(line); 568 | } 569 | } 570 | } 571 | 572 | private resolveVimCommands() { 573 | const indexText = this.text[INDEX_PATH] || []; 574 | let isMatchLine = false; 575 | let completionItem: CompletionItem; 576 | for (const line of indexText) { 577 | if (!isMatchLine) { 578 | if (/\*ex-cmd-index\*/.test(line)) { 579 | isMatchLine = true; 580 | } 581 | continue; 582 | } else { 583 | const m = line.match(/^\|?:([^ \t]+?)\|?[ \t]+:([^ \t]+)[ \t]+([^ \t].*)$/); 584 | if (m) { 585 | if (completionItem) { 586 | this.vimCommandItems.push(completionItem); 587 | } 588 | const label = m[1]; 589 | completionItem = { 590 | label: m[1], 591 | kind: CompletionItemKind.Operator, 592 | detail: m[2], 593 | documentation: m[3], 594 | sortText: "00004", 595 | insertText: m[1], 596 | insertTextFormat: InsertTextFormat.PlainText, 597 | }; 598 | if (!this.vimCommandDocuments[label]) { 599 | this.vimCommandDocuments[label] = []; 600 | } 601 | this.vimCommandDocuments[label].push( 602 | m[3], 603 | ); 604 | } else if (/^[ \t]*$/.test(line)) { 605 | if (completionItem) { 606 | this.vimCommandItems.push(completionItem); 607 | completionItem = undefined; 608 | break; 609 | } 610 | } else if (completionItem) { 611 | completionItem.documentation += ` ${line.trim()}`; 612 | this.vimCommandDocuments[completionItem.label].push( 613 | line, 614 | ); 615 | } 616 | } 617 | } 618 | } 619 | 620 | private resolveVimFeatures() { 621 | const text = this.text[EVAL_PATH] || []; 622 | let isMatchLine = false; 623 | let completionItem: CompletionItem; 624 | const features: CompletionItem[] = []; 625 | for (let idx = 0; idx < text.length; idx++) { 626 | const line = text[idx]; 627 | if (!isMatchLine) { 628 | if (/^[ \t]*acl[ \t]/.test(line)) { 629 | isMatchLine = true; 630 | idx -= 1; 631 | } 632 | continue; 633 | } else { 634 | const m = line.match(/^[ \t]*\*?([^ \t]+?)\*?[ \t]+([^ \t].*)$/); 635 | if (m) { 636 | if (completionItem) { 637 | features.push(completionItem); 638 | } 639 | const label = m[1]; 640 | completionItem = { 641 | label: m[1], 642 | kind: CompletionItemKind.EnumMember, 643 | documentation: "", 644 | sortText: "00004", 645 | insertText: m[1], 646 | insertTextFormat: InsertTextFormat.PlainText, 647 | }; 648 | if (!this.vimFeatureDocuments[label]) { 649 | this.vimFeatureDocuments[label] = []; 650 | } 651 | this.vimFeatureDocuments[label].push(m[2]); 652 | } else if (/^[ \t]*$/.test(line)) { 653 | if (completionItem) { 654 | features.push(completionItem); 655 | break; 656 | } 657 | } else if (completionItem) { 658 | this.vimFeatureDocuments[completionItem.label].push( 659 | line, 660 | ); 661 | } 662 | } 663 | } 664 | this.vimFeatureItems = features; 665 | } 666 | 667 | private resolveVimAutocmds() { 668 | const text = this.text[AUTOCMD_PATH] || []; 669 | let isMatchLine = false; 670 | for (let idx = 0; idx < text.length; idx++) { 671 | const line = text[idx]; 672 | if (!isMatchLine) { 673 | if (/^\|BufNewFile\|/.test(line)) { 674 | isMatchLine = true; 675 | idx -= 1; 676 | } 677 | continue; 678 | } else { 679 | const m = line.match(/^\|([^ \t]+)\|[ \t]+([^ \t].*)$/); 680 | if (m) { 681 | this.vimAutocmdItems.push({ 682 | label: m[1], 683 | kind: CompletionItemKind.EnumMember, 684 | documentation: m[2], 685 | sortText: "00004", 686 | insertText: m[1], 687 | insertTextFormat: InsertTextFormat.PlainText, 688 | }); 689 | if (m[1] === "Signal") { 690 | break; 691 | } 692 | } 693 | } 694 | } 695 | } 696 | 697 | private resolveExpandKeywords() { 698 | this.vimExpandKeywordItems = [ 699 | ",file name under the cursor", 700 | ",autocmd file name", 701 | ",autocmd buffer number (as a String!)", 702 | ",autocmd matched name", 703 | ",sourced script file or function name", 704 | ",sourced script file line number", 705 | ",word under the cursor", 706 | ",WORD under the cursor", 707 | ",the {clientid} of the last received message `server2client()`", 708 | ].map((line) => { 709 | const item = line.split(","); 710 | this.expandKeywordDocuments[item[0]] = [ 711 | item[1], 712 | ]; 713 | return { 714 | label: item[0], 715 | kind: CompletionItemKind.Keyword, 716 | documentation: item[1], 717 | sortText: "00004", 718 | insertText: item[0], 719 | insertTextFormat: InsertTextFormat.PlainText, 720 | }; 721 | }); 722 | } 723 | 724 | } 725 | 726 | async function main() { 727 | const servers: Server[] = []; 728 | for (let idx = 2; idx < process.argv.length; idx++) { 729 | servers.push( 730 | new Server({ 731 | vimruntime: process.argv[idx], 732 | }), 733 | ); 734 | await servers[servers.length - 1].build(); 735 | } 736 | const server: Server = servers.reduce((pre, next) => { 737 | // merge functions 738 | next.vimBuiltinFunctionItems.forEach((item) => { 739 | const { label } = item; 740 | if (!pre.vimBuiltFunctionDocuments[label]) { 741 | pre.vimBuiltinFunctionItems.push(item); 742 | pre.vimBuiltFunctionDocuments[label] = next.vimBuiltFunctionDocuments[label]; 743 | } 744 | }); 745 | // merge commands 746 | next.vimCommandItems.forEach((item) => { 747 | const { label } = item; 748 | if (!pre.vimCommandDocuments[label]) { 749 | pre.vimCommandItems.push(item); 750 | pre.vimCommandDocuments[label] = next.vimCommandDocuments[label]; 751 | } 752 | }); 753 | // merge options 754 | next.vimOptionItems.forEach((item) => { 755 | const { label } = item; 756 | if (!pre.vimOptionDocuments[label]) { 757 | pre.vimOptionItems.push(item); 758 | pre.vimOptionDocuments[label] = next.vimOptionDocuments[label]; 759 | } 760 | }); 761 | // merge variables 762 | next.vimPredefinedVariablesItems.forEach((item) => { 763 | const { label } = item; 764 | if (!pre.vimPredefinedVariableDocuments[label]) { 765 | pre.vimPredefinedVariablesItems.push(item); 766 | pre.vimPredefinedVariableDocuments[label] = next.vimPredefinedVariableDocuments[label]; 767 | } 768 | }); 769 | // merge features 770 | next.vimFeatureItems.forEach((item) => { 771 | const { label } = item; 772 | if (!pre.vimFeatureDocuments[label]) { 773 | pre.vimFeatureItems.push(item); 774 | pre.vimFeatureDocuments[label] = next.vimFeatureDocuments[label]; 775 | } 776 | }); 777 | // merge expand key words 778 | next.vimExpandKeywordItems.forEach((item) => { 779 | const { label } = item; 780 | if (!pre.expandKeywordDocuments[label]) { 781 | pre.vimExpandKeywordItems.push(item); 782 | pre.expandKeywordDocuments[label] = next.expandKeywordDocuments[label]; 783 | } 784 | }); 785 | // merge autocmd 786 | next.vimAutocmdItems.forEach((item) => { 787 | const { label } = item; 788 | if (!pre.vimAutocmdItems.some((n) => n.label === label)) { 789 | pre.vimAutocmdItems.push(item); 790 | } 791 | }); 792 | // merge signature help 793 | pre.vimBuiltFunctionSignatureHelp = { 794 | ...next.vimBuiltFunctionSignatureHelp, 795 | ...pre.vimBuiltFunctionSignatureHelp, 796 | }; 797 | return pre; 798 | }); 799 | 800 | server.serialize(); 801 | } 802 | 803 | main(); 804 | -------------------------------------------------------------------------------- /src/server/buffer.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem, CompletionItemKind, InsertTextFormat } from "vscode-languageserver"; 2 | import { sortTexts } from "../common/constant"; 3 | import logger from "../common/logger"; 4 | import { INode, IPos } from "../lib/vimparser"; 5 | 6 | const log = logger("buffer"); 7 | 8 | const NODE_TOPLEVEL = 1; 9 | const NODE_EXCMD = 3; 10 | const NODE_FUNCTION = 4; 11 | const NODE_DELFUNCTION = 6; 12 | const NODE_RETURN = 7; 13 | const NODE_EXCALL = 8; 14 | const NODE_LET = 9; 15 | const NODE_UNLET = 10; 16 | const NODE_LOCKVAR = 11; 17 | const NODE_UNLOCKVAR = 12; 18 | const NODE_IF = 13; 19 | const NODE_ELSEIF = 14; 20 | const NODE_ELSE = 15; 21 | const NODE_WHILE = 17; 22 | const NODE_FOR = 19; 23 | const NODE_TRY = 23; 24 | const NODE_CATCH = 24; 25 | const NODE_FINALLY = 25; 26 | const NODE_THROW = 27; 27 | const NODE_ECHO = 28; 28 | const NODE_ECHON = 29; 29 | const NODE_ECHOMSG = 31; 30 | const NODE_ECHOERR = 32; 31 | const NODE_EXECUTE = 33; 32 | const NODE_TERNARY = 34; 33 | const NODE_OR = 35; 34 | const NODE_AND = 36; 35 | const NODE_EQUAL = 37; 36 | const NODE_EQUALCI = 38; 37 | const NODE_EQUALCS = 39; 38 | const NODE_NEQUAL = 40; 39 | const NODE_NEQUALCI = 41; 40 | const NODE_NEQUALCS = 42; 41 | const NODE_GREATER = 43; 42 | const NODE_GREATERCI = 44; 43 | const NODE_GREATERCS = 45; 44 | const NODE_GEQUAL = 46; 45 | const NODE_GEQUALCI = 47; 46 | const NODE_GEQUALCS = 48; 47 | const NODE_SMALLER = 49; 48 | const NODE_SMALLERCI = 50; 49 | const NODE_SMALLERCS = 51; 50 | const NODE_SEQUAL = 52; 51 | const NODE_SEQUALCI = 53; 52 | const NODE_SEQUALCS = 54; 53 | const NODE_MATCH = 55; 54 | const NODE_MATCHCI = 56; 55 | const NODE_MATCHCS = 57; 56 | const NODE_NOMATCH = 58; 57 | const NODE_NOMATCHCI = 59; 58 | const NODE_NOMATCHCS = 60; 59 | const NODE_IS = 61; 60 | const NODE_ISCI = 62; 61 | const NODE_ISCS = 63; 62 | const NODE_ISNOT = 64; 63 | const NODE_ISNOTCI = 65; 64 | const NODE_ISNOTCS = 66; 65 | const NODE_ADD = 67; 66 | const NODE_SUBTRACT = 68; 67 | const NODE_CONCAT = 69; 68 | const NODE_MULTIPLY = 70; 69 | const NODE_DIVIDE = 71; 70 | const NODE_REMAINDER = 72; 71 | const NODE_NOT = 73; 72 | const NODE_MINUS = 74; 73 | const NODE_PLUS = 75; 74 | const NODE_SUBSCRIPT = 76; 75 | const NODE_SLICE = 77; 76 | const NODE_CALL = 78; 77 | const NODE_DOT = 79; 78 | const NODE_NUMBER = 80; 79 | const NODE_STRING = 81; 80 | const NODE_LIST = 82; 81 | const NODE_DICT = 83; 82 | const NODE_IDENTIFIER = 86; 83 | const NODE_CURLYNAME = 87; 84 | const NODE_ENV = 88; 85 | const NODE_REG = 89; // TODO 86 | const NODE_CURLYNAMEPART = 90; // TODO 87 | const NODE_CURLYNAMEEXPR = 91; // TODO 88 | const NODE_LAMBDA = 92; 89 | const NODE_CONST = 94; 90 | const NODE_EVAL = 95; 91 | const NODE_HEREDOC = 96; 92 | const NODE_METHOD = 97; 93 | 94 | /* 95 | * buffer's completion items 96 | * 97 | * 1. functions: xxx g:xxx s:xxx xx#xxx 98 | * 2. identifier: xxx g:xxx s:xxx b:xxx l:xxx a:xxx 99 | */ 100 | 101 | /* 102 | * global function declation 103 | * 104 | * - g:function_name 105 | * - Captial_function_name 106 | */ 107 | export interface IFunction { 108 | name: string; 109 | args: INode[]; 110 | startLine: number; 111 | startCol: number; 112 | endLine: number; 113 | endCol: number; 114 | range: { 115 | startLine: number; 116 | startCol: number; 117 | endLine: number; 118 | endCol: number; 119 | } 120 | } 121 | 122 | export interface IFunRef { 123 | name: string; 124 | args: INode[]; 125 | startLine: number; 126 | startCol: number; 127 | } 128 | 129 | export interface IIdentifier { 130 | name: string; 131 | startLine: number; 132 | startCol: number; 133 | } 134 | 135 | /* 136 | * xxxx 137 | * endxxx 138 | * 139 | */ 140 | export interface IRange { 141 | startLine: number 142 | startCol: number 143 | endLine: number 144 | endCol: number 145 | } 146 | 147 | const globalFuncPattern = /^(g:\w+(\.\w+)*|[a-zA-Z_]\w*(\.\w+)*|(\w+#)+\w*)$/; 148 | const scriptFuncPattern = /^(s:\w+(\.\w+)*|\w+(\.\w+)*)$/i; 149 | const globalVariablePattern = /^(g:\w+(\.\w+)*|b:\w+(\.\w+)*|\w{1,}(\.\w+)*|\w+(#\w+)+)$/; 150 | const localVariablePattern = /^(s:\w+(\.\w+)*|l:\w+(\.\w+)*|a:\w+(\.\w+)*)$/; 151 | const envPattern = /^\$\w+$/; 152 | 153 | export class Buffer { 154 | 155 | private globalFunctions: Record = {}; 156 | private scriptFunctions: Record = {}; 157 | private globalFunctionRefs: Record = {}; 158 | private scriptFunctionRefs: Record = {}; 159 | 160 | private globalVariables: Record = {}; 161 | private localVariables: Record = {}; 162 | private globalVariableRefs: Record = {}; 163 | private localVariableRefs: Record = {}; 164 | 165 | private envs: Record = {}; 166 | private envRefs: Record = {}; 167 | 168 | private ranges: IRange[] = [] 169 | 170 | constructor( 171 | private uri: string, 172 | private projectRoot: string, 173 | private node: INode, 174 | ) { 175 | this.updateBufferByNode(this.node); 176 | } 177 | 178 | public getGlobalFunctions() { 179 | return this.globalFunctions; 180 | } 181 | 182 | public getGlobalFunctionRefs() { 183 | return this.globalFunctionRefs; 184 | } 185 | 186 | public getScriptFunctions() { 187 | return this.scriptFunctions; 188 | } 189 | 190 | public getScriptFunctionRefs() { 191 | return this.scriptFunctionRefs; 192 | } 193 | 194 | public getGlobalIdentifiers() { 195 | return this.globalVariables; 196 | } 197 | 198 | public getGlobalIdentifierRefs() { 199 | return this.globalVariableRefs; 200 | } 201 | 202 | public getLocalIdentifiers() { 203 | return this.localVariables; 204 | } 205 | 206 | public getLocalIdentifierRefs() { 207 | return this.localVariableRefs; 208 | } 209 | 210 | public getRanges() { 211 | return this.ranges 212 | } 213 | 214 | public getProjectRoot() { 215 | return this.projectRoot; 216 | } 217 | 218 | public isBelongToWorkdir(workUri: string) { 219 | return this.projectRoot === workUri; 220 | } 221 | 222 | public updateBufferByNode(node: INode) { 223 | this.node = node; 224 | this.resetProperties(); 225 | try { 226 | this.resolveCompletionItems([node]); 227 | } catch (error) { 228 | log.warn(`updateBufferByNode: ${error.stack}`); 229 | } 230 | } 231 | 232 | /* 233 | * global function 234 | * 235 | * - g:xxx 236 | * - xx#xxx 237 | */ 238 | public getGlobalFunctionItems(): CompletionItem[] { 239 | const refs: Record = {}; 240 | Object.keys(this.globalFunctionRefs).forEach((name) => { 241 | if (!this.globalFunctions[name]) { 242 | refs[name] = this.globalFunctionRefs[name]; 243 | } 244 | }); 245 | return this.getFunctionItems(this.globalFunctions, sortTexts.three) 246 | .concat( 247 | this.getFunctionItems(refs, sortTexts.three), 248 | ); 249 | } 250 | 251 | /* 252 | * script function 253 | * 254 | * - s:xxx 255 | */ 256 | public getScriptFunctionItems(): CompletionItem[] { 257 | const refs: Record = {}; 258 | Object.keys(this.scriptFunctionRefs).forEach((name) => { 259 | if (!this.scriptFunctions[name]) { 260 | refs[name] = this.scriptFunctionRefs[name]; 261 | } 262 | }); 263 | return this.getFunctionItems(this.scriptFunctions, sortTexts.two) 264 | .concat( 265 | this.getFunctionItems(refs, sortTexts.two), 266 | ); 267 | } 268 | 269 | /* 270 | * global identifier 271 | * 272 | * - g:xxx 273 | * - b:xxx 274 | * - [a-zA-Z]+ 275 | * - xx#xxx 276 | */ 277 | public getGlobalIdentifierItems(): CompletionItem[] { 278 | const refs: Record = {}; 279 | Object.keys(this.globalVariableRefs).forEach((name) => { 280 | if (!this.globalVariables[name]) { 281 | refs[name] = this.globalVariableRefs[name]; 282 | } 283 | }); 284 | const globalVariables: CompletionItem[] = []; 285 | const localVariables: CompletionItem[] = []; 286 | this.getIdentifierItems(this.globalVariables, sortTexts.three) 287 | .concat( 288 | this.getIdentifierItems(refs, sortTexts.three), 289 | ) 290 | .forEach((item) => { 291 | if (/^([a-zA-Z_]\w*(\.\w+)*)$/.test(item.label)) { 292 | localVariables.push(item); 293 | } else { 294 | globalVariables.push(item); 295 | } 296 | }); 297 | if (localVariables.length) { 298 | const gloalFunctions = this.getGlobalFunctions(); 299 | const scriptFunctions = this.getScriptFunctions(); 300 | const funList = Object.values(gloalFunctions).concat( 301 | Object.values(scriptFunctions), 302 | ).reduce((res, fs) => res.concat(fs), []); 303 | 304 | localVariables.forEach((l) => { 305 | if ((l.data as IIdentifier[]).some((identifier) => { 306 | return funList.every((fun) => 307 | !(fun.startLine < identifier.startLine && identifier.startLine < fun.endLine)); 308 | })) { 309 | globalVariables.push(l); 310 | } 311 | }); 312 | } 313 | return globalVariables; 314 | } 315 | 316 | /* 317 | * local identifier 318 | * 319 | * - s:xxx 320 | */ 321 | public getLocalIdentifierItems(): CompletionItem[] { 322 | const refs: Record = {}; 323 | Object.keys(this.localVariableRefs).forEach((name) => { 324 | if (!this.localVariables[name]) { 325 | refs[name] = this.localVariableRefs[name]; 326 | } 327 | }); 328 | return this.getIdentifierItems(this.localVariables, sortTexts.two) 329 | .concat( 330 | this.getIdentifierItems(refs, sortTexts.two), 331 | ) 332 | .filter((item) => !/^(a|l):/.test(item.label)); 333 | } 334 | 335 | /* 336 | * function local identifier 337 | * 338 | * - l:xxx 339 | * - a:xxx 340 | * - identifiers in function range 341 | */ 342 | public getFunctionLocalIdentifierItems(line: number): CompletionItem[] { 343 | const vimLineNum = line + 1; 344 | let startLine = -1; 345 | let endLine = -1; 346 | // get function args completion items 347 | const funArgs: CompletionItem[] = ([] as IFunction[]) 348 | .concat(Object.values(this.globalFunctions).reduce((res, next) => res.concat(next), [])) 349 | .concat(Object.values(this.scriptFunctions).reduce((res, next) => res.concat(next), [])) 350 | .filter((fun) => { 351 | if (startLine === -1 && endLine === -1 && fun.startLine < vimLineNum && vimLineNum < fun.endLine) { 352 | startLine = fun.startLine; 353 | endLine = fun.endLine; 354 | } else if (fun.startLine > startLine && endLine > fun.endLine) { 355 | startLine = fun.startLine; 356 | endLine = fun.endLine; 357 | } 358 | 359 | return fun.startLine < vimLineNum && vimLineNum < fun.endLine; 360 | }) 361 | .reduce((res, next) => { 362 | (next.args || []).forEach((name) => { 363 | if (res.indexOf(name.value) === -1) { 364 | res.push(name.value); 365 | } 366 | }); 367 | return res; 368 | }, []) 369 | .map((name) => ({ 370 | label: `a:${name}`, 371 | kind: CompletionItemKind.Variable, 372 | sortText: sortTexts.one, 373 | insertText: `a:${name}`, 374 | insertTextFormat: InsertTextFormat.PlainText, 375 | })); 376 | if (startLine !== -1 && endLine !== -1) { 377 | const funcLocalIdentifiers = this.getIdentifierItems(this.localVariables, sortTexts.one) 378 | .concat( 379 | this.getIdentifierItems(this.globalVariables, sortTexts.one), 380 | ) 381 | .filter((item) => { 382 | if (!(/^l:/.test(item.label) || /^([a-zA-Z_]\w*(\.\w+)*)$/.test(item.label))) { 383 | return false; 384 | } 385 | const { data } = item; 386 | if (!data) { 387 | return false; 388 | } 389 | return data.some((i: IIdentifier) => startLine < i.startLine && i.startLine < endLine); 390 | }); 391 | return funArgs.concat(funcLocalIdentifiers); 392 | } 393 | return []; 394 | } 395 | 396 | /* 397 | * environment identifier 398 | * 399 | * - $xxx 400 | */ 401 | public getEnvItems(): CompletionItem[] { 402 | return Object.keys(this.envs).map((name) => { 403 | return { 404 | label: name, 405 | insertText: name, 406 | sortText: sortTexts.three, 407 | insertTextFormat: InsertTextFormat.PlainText, 408 | }; 409 | }); 410 | } 411 | 412 | private resetProperties() { 413 | this.globalFunctions = {}; 414 | this.scriptFunctions = {}; 415 | this.globalFunctionRefs = {}; 416 | this.scriptFunctionRefs = {}; 417 | this.globalVariables = {}; 418 | this.localVariables = {}; 419 | this.globalVariableRefs = {}; 420 | this.localVariableRefs = {}; 421 | this.envs = {}; 422 | this.envRefs = {}; 423 | this.ranges = [] 424 | } 425 | 426 | private resolveCompletionItems(nodes: INode | INode[]) { 427 | let nodeList: INode[] = [].concat(nodes); 428 | while (nodeList.length > 0) { 429 | const node = nodeList.pop(); 430 | switch (node.type) { 431 | case NODE_TOPLEVEL: 432 | nodeList = nodeList.concat(node.body); 433 | break; 434 | // autocmd/command/map 435 | case NODE_EXCMD: 436 | this.takeFuncRefByExcmd(node); 437 | break; 438 | case NODE_EXCALL: 439 | case NODE_RETURN: 440 | case NODE_DELFUNCTION: 441 | case NODE_THROW: 442 | case NODE_EVAL: 443 | nodeList = nodeList.concat(node.left); 444 | break; 445 | case NODE_DOT: 446 | nodeList = nodeList.concat(node.left); 447 | this.takeIdentifier(node); 448 | break; 449 | case NODE_ECHO: 450 | case NODE_ECHON: 451 | case NODE_ECHOMSG: 452 | case NODE_ECHOERR: 453 | case NODE_UNLET: 454 | case NODE_LOCKVAR: 455 | case NODE_UNLOCKVAR: 456 | case NODE_EXECUTE: 457 | nodeList = nodeList.concat(node.list || []); 458 | break; 459 | case NODE_TERNARY: 460 | nodeList = nodeList.concat(node.cond || []); 461 | nodeList = nodeList.concat(node.left || []); 462 | nodeList = nodeList.concat(node.right || []); 463 | break; 464 | case NODE_IF: 465 | case NODE_ELSEIF: 466 | case NODE_ELSE: 467 | case NODE_WHILE: 468 | this.takeRange(node, ['endif', 'endwhile']) 469 | nodeList = nodeList.concat(node.body || []); 470 | nodeList = nodeList.concat(node.cond || []); 471 | nodeList = nodeList.concat(node.elseif || []); 472 | nodeList = nodeList.concat(node._else || []); 473 | break; 474 | case NODE_OR: 475 | case NODE_AND: 476 | case NODE_EQUAL: 477 | case NODE_EQUALCI: 478 | case NODE_EQUALCS: 479 | case NODE_NEQUAL: 480 | case NODE_NEQUALCI: 481 | case NODE_NEQUALCS: 482 | case NODE_GREATER: 483 | case NODE_GREATERCI: 484 | case NODE_GREATERCS: 485 | case NODE_GEQUAL: 486 | case NODE_GEQUALCI: 487 | case NODE_GEQUALCS: 488 | case NODE_SMALLER: 489 | case NODE_SMALLERCI: 490 | case NODE_SMALLERCS: 491 | case NODE_SEQUAL: 492 | case NODE_SEQUALCI: 493 | case NODE_SEQUALCS: 494 | case NODE_MATCH: 495 | case NODE_MATCHCI: 496 | case NODE_MATCHCS: 497 | case NODE_NOMATCH: 498 | case NODE_NOMATCHCI: 499 | case NODE_NOMATCHCS: 500 | case NODE_IS: 501 | case NODE_ISCI: 502 | case NODE_ISCS: 503 | case NODE_ISNOT: 504 | case NODE_ISNOTCI: 505 | case NODE_ISNOTCS: 506 | case NODE_CONCAT: 507 | case NODE_MULTIPLY: 508 | case NODE_DIVIDE: 509 | case NODE_REMAINDER: 510 | case NODE_NOT: 511 | case NODE_MINUS: 512 | case NODE_PLUS: 513 | case NODE_ADD: 514 | case NODE_SUBTRACT: 515 | case NODE_SUBSCRIPT: 516 | case NODE_METHOD: 517 | nodeList = nodeList.concat(node.left || []); 518 | nodeList = nodeList.concat(node.right || []); 519 | break; 520 | case NODE_FOR: 521 | nodeList = nodeList.concat(node.body || []); 522 | nodeList = nodeList.concat(node.right || []); 523 | this.takeFor([].concat(node.left || []).concat(node.list || [])); 524 | this.takeRange(node, 'endfor') 525 | break; 526 | case NODE_TRY: 527 | case NODE_CATCH: 528 | case NODE_FINALLY: 529 | this.takeRange(node, 'endtry') 530 | nodeList = nodeList.concat(node.body || []); 531 | nodeList = nodeList.concat(node.catch || []); 532 | nodeList = nodeList.concat(node._finally || []); 533 | break; 534 | case NODE_FUNCTION: 535 | nodeList = nodeList.concat(node.body || []); 536 | if (node.left && node.left.type === NODE_DOT) { 537 | nodeList = nodeList.concat(node.left.left); 538 | } 539 | this.takeFunction(node); 540 | this.takeRange(node, 'endfunction') 541 | break; 542 | case NODE_LIST: 543 | nodeList = nodeList.concat(node.value || []); 544 | break; 545 | case NODE_DICT: 546 | nodeList = nodeList.concat( 547 | (node.value || []).map((item: [INode, INode]) => item[1]), 548 | ); 549 | break; 550 | case NODE_SLICE: 551 | case NODE_LAMBDA: 552 | nodeList = nodeList.concat(node.left || []); 553 | nodeList = nodeList.concat(node.rlist || []); 554 | break; 555 | case NODE_CALL: 556 | nodeList = nodeList.concat(node.rlist || []); 557 | if (node.left && node.left.type === NODE_DOT) { 558 | nodeList = nodeList.concat(node.left.left); 559 | } 560 | this.takeFuncRefByRef(node); 561 | this.takeFuncRef(node); 562 | break; 563 | case NODE_LET: 564 | case NODE_CONST: 565 | nodeList = nodeList.concat(node.right || []); 566 | if (node.left && node.left.type === NODE_DOT) { 567 | nodeList = nodeList.concat(node.left.left); 568 | } 569 | // not a function by function()/funcref() 570 | if (!this.takeFunctionByRef(node)) { 571 | this.takeLet(node); 572 | } 573 | break; 574 | case NODE_ENV: 575 | case NODE_IDENTIFIER: 576 | this.takeIdentifier(node); 577 | break; 578 | default: 579 | break; 580 | } 581 | } 582 | // log.info(`parse_buffer: ${JSON.stringify(this)}`) 583 | } 584 | 585 | private takeFunction(node: INode) { 586 | const { left, rlist, endfunction } = node; 587 | const name = this.getDotName(left); 588 | if (!name) { 589 | return; 590 | } 591 | const pos = this.getDotPos(left); 592 | if (!pos) { 593 | return; 594 | } 595 | const func: IFunction = { 596 | name, 597 | args: rlist || [], 598 | startLine: pos.lnum, 599 | startCol: pos.col, 600 | endLine: endfunction!.pos.lnum, 601 | endCol: endfunction!.pos.col, 602 | range: { 603 | startLine: node.pos.lnum, 604 | startCol: node.pos.col, 605 | endLine: endfunction!.pos.lnum, 606 | endCol: endfunction!.pos.col, 607 | } 608 | }; 609 | if (globalFuncPattern.test(name)) { 610 | if (!this.globalFunctions[name] || !Array.isArray(this.globalFunctions[name])) { 611 | this.globalFunctions[name] = []; 612 | } 613 | this.globalFunctions[name].push(func); 614 | } else if (scriptFuncPattern.test(name)) { 615 | if (!this.scriptFunctions[name] || !Array.isArray(this.scriptFunctions[name])) { 616 | this.scriptFunctions[name] = []; 617 | } 618 | this.scriptFunctions[name].push(func); 619 | } 620 | } 621 | 622 | /* 623 | * vim function 624 | * 625 | * - let funcName = function() 626 | * - let funcName = funcref() 627 | */ 628 | private takeFunctionByRef(node: INode): boolean { 629 | const { left, right } = node; 630 | if (!right || right.type !== NODE_CALL) { 631 | return; 632 | } 633 | // is not function()/funcref() 634 | if ( 635 | !right.left || 636 | !right.left.value || 637 | ["function", "funcref"].indexOf(right.left.value) === -1 638 | ) { 639 | return; 640 | } 641 | const name = this.getDotName(left); 642 | if (!name) { 643 | return; 644 | } 645 | const pos = this.getDotPos(left); 646 | if (!pos) { 647 | return false; 648 | } 649 | const func: IFunction = { 650 | name, 651 | args: [], 652 | startLine: pos.lnum, 653 | startCol: pos.col, 654 | endLine: pos.lnum, 655 | endCol: pos.col, 656 | range: { 657 | startLine: pos.lnum, 658 | startCol: pos.col, 659 | endLine: pos.lnum, 660 | endCol: pos.col, 661 | } 662 | }; 663 | if (globalFuncPattern.test(name)) { 664 | if (!this.globalFunctions[name] || !Array.isArray(this.globalFunctions[name])) { 665 | this.globalFunctions[name] = []; 666 | } 667 | this.globalFunctions[name].push(func); 668 | return true; 669 | } else if (scriptFuncPattern.test(name)) { 670 | if (!this.scriptFunctions[name] || !Array.isArray(this.scriptFunctions[name])) { 671 | this.scriptFunctions[name] = []; 672 | } 673 | this.scriptFunctions[name].push(func); 674 | return true; 675 | } 676 | return false; 677 | } 678 | 679 | private takeFuncRef(node: INode) { 680 | const { left, rlist } = node; 681 | let name = ""; 682 | if (left.type === NODE_IDENTIFIER) { 683 | name = left.value; 684 | // funName 685 | } else if (left.type === NODE_CURLYNAME) { 686 | name = ((left.value || []) as INode[]).map((item) => item.value).join(""); 687 | } else if (left.type === NODE_DOT) { 688 | name = this.getDotName(left); 689 | } 690 | if (!name) { 691 | return; 692 | } 693 | const pos = this.getDotPos(left); 694 | if (!pos) { 695 | return; 696 | } 697 | const funcRef: IFunRef = { 698 | name, 699 | args: rlist || [], 700 | startLine: pos.lnum, 701 | startCol: pos.col, 702 | }; 703 | 704 | if (globalFuncPattern.test(name)) { 705 | if (!this.globalFunctionRefs[name] || !Array.isArray(this.globalFunctionRefs[name])) { 706 | this.globalFunctionRefs[name] = []; 707 | } 708 | this.globalFunctionRefs[name].push(funcRef); 709 | } else if (scriptFuncPattern.test(name)) { 710 | if (!this.scriptFunctionRefs[name] || !Array.isArray(this.scriptFunctionRefs[name])) { 711 | this.scriptFunctionRefs[name] = []; 712 | } 713 | this.scriptFunctionRefs[name].push(funcRef); 714 | } 715 | 716 | } 717 | 718 | /* 719 | * vim function ref 720 | * first value is function name 721 | * 722 | * - function('funcName') 723 | * - funcref('funcName') 724 | */ 725 | private takeFuncRefByRef(node: INode) { 726 | const { left, rlist } = node; 727 | const funcNode = rlist && rlist[0]; 728 | if ( 729 | !left || 730 | ["function", "funcref"].indexOf(left.value) === -1 || 731 | !funcNode || 732 | !funcNode.pos || 733 | typeof funcNode.value !== "string" 734 | ) { 735 | return; 736 | } 737 | 738 | // delete '/" of function name 739 | const name = (funcNode.value as string).replace(/^['"]|['"]$/g, ""); 740 | const funcRef: IFunRef = { 741 | name, 742 | args: [], 743 | startLine: funcNode.pos.lnum, 744 | startCol: funcNode.pos.col + 1, // +1 by '/" 745 | }; 746 | 747 | if (globalFuncPattern.test(name)) { 748 | if (!this.globalFunctionRefs[name] || !Array.isArray(this.globalFunctionRefs[name])) { 749 | this.globalFunctionRefs[name] = []; 750 | } 751 | this.globalFunctionRefs[name].push(funcRef); 752 | } else if (scriptFuncPattern.test(name)) { 753 | if (!this.scriptFunctionRefs[name] || !Array.isArray(this.scriptFunctionRefs[name])) { 754 | this.scriptFunctionRefs[name] = []; 755 | } 756 | this.scriptFunctionRefs[name].push(funcRef); 757 | } 758 | } 759 | 760 | /* 761 | * FIXME: take function ref by 762 | * 763 | * - autocmd 764 | * - command 765 | * - map 766 | */ 767 | private takeFuncRefByExcmd(node: INode) { 768 | const { pos, str } = node; 769 | if (!str) { 770 | return; 771 | } 772 | 773 | // tslint:disable-next-line: max-line-length 774 | if (!/^[ \t]*((au|aut|auto|autoc|autocm|autocmd|com|comm|comma|comman|command)!?[ \t]+|([a-zA-Z]*map!?[ \t]+.*?:))/.test(str)) { 775 | return; 776 | } 777 | 778 | const regFunc = /([\w_#]+|[a-zA-Z_]:[\w_#]+|[\w_#]+)[ \t]*\(/gi; 779 | let m = regFunc.exec(str); 780 | 781 | while (m) { 782 | const name = m[1]; 783 | if (name) { 784 | const funcRef: IFunRef = { 785 | name, 786 | args: [], 787 | startLine: pos.lnum, 788 | startCol: pos.col + m.index, 789 | }; 790 | 791 | if (globalFuncPattern.test(name)) { 792 | if (!this.globalFunctionRefs[name] || !Array.isArray(this.globalFunctionRefs[name])) { 793 | this.globalFunctionRefs[name] = []; 794 | } 795 | this.globalFunctionRefs[name].push(funcRef); 796 | } else if (scriptFuncPattern.test(name)) { 797 | if (!this.scriptFunctionRefs[name] || !Array.isArray(this.scriptFunctionRefs[name])) { 798 | this.scriptFunctionRefs[name] = []; 799 | } 800 | this.scriptFunctionRefs[name].push(funcRef); 801 | } 802 | } 803 | m = regFunc.exec(str); 804 | } 805 | } 806 | 807 | private takeLet(node: INode) { 808 | const pos = this.getDotPos(node.left); 809 | const name = this.getDotName(node.left); 810 | if (!pos || !name) { 811 | return; 812 | } 813 | const identifier: IIdentifier = { 814 | name, 815 | startLine: pos.lnum, 816 | startCol: pos.col, 817 | }; 818 | if (localVariablePattern.test(name)) { 819 | if (!this.localVariables[name] || !Array.isArray(this.localVariables[name])) { 820 | this.localVariables[name] = []; 821 | } 822 | this.localVariables[name].push(identifier); 823 | } else if (globalVariablePattern.test(name)) { 824 | if (!this.globalVariables[name] || !Array.isArray(this.globalVariables[name])) { 825 | this.globalVariables[name] = []; 826 | } 827 | this.globalVariables[name].push(identifier); 828 | } else if (envPattern.test(name)) { 829 | if (!this.envs[name] || !Array.isArray(this.envs[name])) { 830 | this.envs[name] = []; 831 | } 832 | this.envs[name].push(identifier); 833 | } 834 | } 835 | 836 | private takeRange(node: INode, keys: string | string[]) { 837 | [].concat(keys).forEach(key => { 838 | if (node.pos && node[key] && node[key].pos) { 839 | this.ranges.push({ 840 | startLine: node.pos.lnum, 841 | startCol: node.pos.col, 842 | endLine: node[key].pos.lnum, 843 | endCol: node[key].pos.col 844 | }) 845 | } 846 | }) 847 | } 848 | 849 | private takeFor(nodes: INode[]) { 850 | nodes.forEach((node) => { 851 | if (node.type !== NODE_IDENTIFIER || !node.pos) { 852 | return; 853 | } 854 | const name = node.value; 855 | const identifier: IIdentifier = { 856 | name, 857 | startLine: node.pos.lnum, 858 | startCol: node.pos.col, 859 | }; 860 | if (localVariablePattern.test(name)) { 861 | if (!this.localVariables[name] || !Array.isArray(this.localVariables[name])) { 862 | this.localVariables[name] = []; 863 | } 864 | this.localVariables[name].push(identifier); 865 | } else if (globalVariablePattern.test(name)) { 866 | if (!this.globalVariables[name] || !Array.isArray(this.globalVariables[name])) { 867 | this.globalVariables[name] = []; 868 | } 869 | this.globalVariables[name].push(identifier); 870 | } else if (envPattern.test(name)) { 871 | if (!this.envs[name] || !Array.isArray(this.envs[name])) { 872 | this.envs[name] = []; 873 | } 874 | this.envs[name].push(identifier); 875 | } 876 | }); 877 | } 878 | 879 | private takeIdentifier(node: INode) { 880 | const name = this.getDotName(node); 881 | if (!name) { 882 | return; 883 | } 884 | const pos = this.getDotPos(node); 885 | if (!pos) { 886 | return; 887 | } 888 | const identifier: IIdentifier = { 889 | name, 890 | startLine: pos.lnum, 891 | startCol: pos.col, 892 | }; 893 | if (globalVariablePattern.test(name)) { 894 | if (!this.globalVariableRefs[name] || !Array.isArray(this.globalVariableRefs[name])) { 895 | this.globalVariableRefs[name] = []; 896 | } 897 | this.globalVariableRefs[name].push(identifier); 898 | } else if (localVariablePattern.test(name)) { 899 | if (!this.localVariableRefs[name] || !Array.isArray(this.localVariableRefs[name])) { 900 | this.localVariableRefs[name] = []; 901 | } 902 | this.localVariableRefs[name].push(identifier); 903 | } else if (envPattern.test(name)) { 904 | if (!this.envRefs[name] || !Array.isArray(this.envRefs[name])) { 905 | this.envRefs[name] = []; 906 | } 907 | this.envRefs[name].push(identifier); 908 | } 909 | } 910 | 911 | private getDotPos(node: INode): IPos | null { 912 | if (!node) { 913 | return null; 914 | } 915 | if ( 916 | node.type === NODE_IDENTIFIER || 917 | node.type === NODE_ENV || 918 | node.type === NODE_CURLYNAME 919 | ) { 920 | return node.pos; 921 | } 922 | const { left } = node; 923 | return this.getDotPos(left); 924 | } 925 | 926 | private getDotName(node: INode) { 927 | if ( 928 | node.type === NODE_IDENTIFIER || 929 | node.type === NODE_STRING || 930 | node.type === NODE_NUMBER || 931 | node.type === NODE_ENV 932 | ) { 933 | return node.value; 934 | } else if (node.type === NODE_CURLYNAME) { 935 | return ((node.value || []) as INode[]).map((item) => item.value).join(""); 936 | } else if (node.type === NODE_SUBSCRIPT) { 937 | return this.getDotName(node.left); 938 | } 939 | const { left, right } = node; 940 | const list = []; 941 | if (left) { 942 | list.push(this.getDotName(left)); 943 | } 944 | if (right) { 945 | list.push(this.getDotName(right)); 946 | } 947 | return list.join("."); 948 | } 949 | 950 | private getFunctionItems( 951 | items: Record, 952 | sortText: string, 953 | ): CompletionItem[] { 954 | return Object.keys(items).map((name) => { 955 | const list = items[name]; 956 | let args = "${1}"; 957 | if (list[0] && list[0].args && list[0].args.length > 0) { 958 | args = (list[0].args || []).reduce((res, next, idx) => { 959 | // FIXME: resove next.value is not string 960 | const value = typeof next.value !== "string" ? "param" : next.value; 961 | if (idx === 0) { 962 | return `\${${idx + 1}:${value}}`; 963 | } 964 | return `${res}, \${${idx + 1}:${value}}`; 965 | }, ""); 966 | } 967 | let label = name; 968 | if (/^/.test(name)) { 969 | label = name.replace(/^/, "s:"); 970 | } 971 | return { 972 | label, 973 | detail: "any", 974 | sortText, 975 | documentation: "User defined function", 976 | kind: CompletionItemKind.Function, 977 | insertText: `${label}(${args})\${0}`, 978 | insertTextFormat: InsertTextFormat.Snippet, 979 | }; 980 | }); 981 | } 982 | 983 | private getIdentifierItems(items: Record, sortText: string): CompletionItem[] { 984 | return Object.keys(items) 985 | .filter((name) => !this.globalFunctions[name] && !this.scriptFunctions[name]) 986 | .map((name) => { 987 | const list: IIdentifier[] = items[name]; 988 | return { 989 | label: name, 990 | kind: CompletionItemKind.Variable, 991 | sortText, 992 | documentation: "User defined variable", 993 | insertText: name, 994 | insertTextFormat: InsertTextFormat.PlainText, 995 | data: list || [], 996 | }; 997 | }); 998 | } 999 | } 1000 | -------------------------------------------------------------------------------- /src/server/builtin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * vim builtin completion items 3 | * 4 | * 1. functions 5 | * 2. options 6 | * 3. variables 7 | * 4. commands 8 | * 5. has features 9 | * 6. expand Keyword 10 | * 7. map args 11 | */ 12 | import fg from "fast-glob"; 13 | import { readFile } from "fs"; 14 | import path, { join } from "path"; 15 | import { 16 | CompletionItem, 17 | CompletionItemKind, 18 | Hover, 19 | InsertTextFormat, 20 | MarkupContent, 21 | MarkupKind, 22 | SignatureHelp, 23 | } from "vscode-languageserver"; 24 | 25 | import logger from "../common/logger"; 26 | import { 27 | builtinFunctionPattern, 28 | builtinVariablePattern, 29 | commandPattern, 30 | expandPattern, 31 | featurePattern, 32 | optionPattern, 33 | } from "../common/patterns"; 34 | import { IBuiltinDoc } from "../common/types"; 35 | import { isSomeMatchPattern, pcb } from "../common/util"; 36 | import buildDocs from "../docs/builtin-docs.json"; 37 | import config from "./config"; 38 | 39 | const log = logger("builtin"); 40 | 41 | class Builtin { 42 | 43 | // completion items 44 | private vimPredefinedVariablesItems: CompletionItem[] = []; 45 | private vimOptionItems: CompletionItem[] = []; 46 | private vimBuiltinFunctionItems: CompletionItem[] = []; 47 | private vimBuiltinFunctionMap: Record = {}; 48 | private vimCommandItems: CompletionItem[] = []; 49 | private vimMapArgsItems: CompletionItem[] = []; 50 | private vimFeatureItems: CompletionItem[] = []; 51 | private vimAutocmdItems: CompletionItem[] = []; 52 | private expandKeywordItems: CompletionItem[] = []; 53 | private colorschemeItems: CompletionItem[] = []; 54 | private highlightArgKeys: CompletionItem[] = []; 55 | private highlightArgValues: Record = {}; 56 | 57 | // signature help 58 | private vimBuiltFunctionSignatureHelp: Record = {}; 59 | 60 | // documents 61 | private vimBuiltFunctionDocuments: Record = {}; 62 | private vimOptionDocuments: Record = {}; 63 | private vimPredefinedVariableDocuments: Record = {}; 64 | private vimCommandDocuments: Record = {}; 65 | private vimFeatureDocuments: Record = {}; 66 | private expandKeywordDocuments: Record = {}; 67 | 68 | public init() { 69 | this.start(); 70 | } 71 | 72 | public getPredefinedVimVariables() { 73 | return this.vimPredefinedVariablesItems; 74 | } 75 | 76 | public getVimOptions() { 77 | return this.vimOptionItems; 78 | } 79 | 80 | public getBuiltinVimFunctions() { 81 | return this.vimBuiltinFunctionItems; 82 | } 83 | 84 | public isBuiltinFunction(label: string) { 85 | return this.vimBuiltinFunctionMap[label]; 86 | } 87 | 88 | public getExpandKeywords() { 89 | return this.expandKeywordItems; 90 | } 91 | 92 | public getVimCommands() { 93 | return this.vimCommandItems; 94 | } 95 | 96 | public getVimMapArgs() { 97 | return this.vimMapArgsItems; 98 | } 99 | 100 | public getVimFeatures() { 101 | return this.vimFeatureItems; 102 | } 103 | 104 | public getVimAutocmds() { 105 | return this.vimAutocmdItems; 106 | } 107 | 108 | public getColorschemes() { 109 | return this.colorschemeItems; 110 | } 111 | 112 | public getHighlightArgKeys() { 113 | return this.highlightArgKeys; 114 | } 115 | 116 | public getHighlightArgValues() { 117 | return this.highlightArgValues; 118 | } 119 | 120 | public getSignatureHelpByName(name: string, idx: number): SignatureHelp | undefined { 121 | const params = this.vimBuiltFunctionSignatureHelp[name]; 122 | if (params) { 123 | return { 124 | signatures: [{ 125 | label: `${name}(${params[0]})${params[1] ? `: ${params[1]}` : ""}`, 126 | documentation: this.formatVimDocument(this.vimBuiltFunctionDocuments[name]), 127 | parameters: params[0].replace(/[\[\]]/g, "").split(",").map((param) => { 128 | return { 129 | label: param.trim(), 130 | }; 131 | }), 132 | }], 133 | activeSignature: 0, 134 | activeParameter: idx, 135 | }; 136 | } 137 | return; 138 | } 139 | 140 | public getDocumentByCompletionItem( 141 | params: { label: string, kind: CompletionItemKind } | CompletionItem, 142 | ): CompletionItem { 143 | const { kind } = params; 144 | switch (kind) { 145 | case CompletionItemKind.Variable: 146 | if (!this.vimPredefinedVariableDocuments[params.label]) { 147 | return params; 148 | } 149 | return { 150 | ...params, 151 | documentation: this.formatVimDocument( 152 | this.vimPredefinedVariableDocuments[params.label], 153 | ), 154 | }; 155 | case CompletionItemKind.Property: 156 | if (!this.vimOptionDocuments[params.label]) { 157 | return params; 158 | } 159 | return { 160 | ...params, 161 | documentation: this.formatVimDocument( 162 | this.vimOptionDocuments[params.label], 163 | ), 164 | }; 165 | case CompletionItemKind.Function: 166 | if (!this.vimBuiltFunctionDocuments[params.label]) { 167 | return params; 168 | } 169 | return { 170 | ...params, 171 | documentation: this.formatVimDocument( 172 | this.vimBuiltFunctionDocuments[params.label], 173 | ), 174 | }; 175 | case CompletionItemKind.EnumMember: 176 | if (!this.vimFeatureDocuments[params.label]) { 177 | return params; 178 | } 179 | return { 180 | ...params, 181 | documentation: this.formatVimDocument( 182 | this.vimFeatureDocuments[params.label], 183 | ), 184 | }; 185 | case CompletionItemKind.Operator: 186 | if (!this.vimCommandDocuments[params.label]) { 187 | return params; 188 | } 189 | return { 190 | ...params, 191 | documentation: this.formatVimDocument( 192 | this.vimCommandDocuments[params.label], 193 | ), 194 | }; 195 | default: 196 | return params; 197 | } 198 | } 199 | 200 | public getHoverDocument(name: string, pre: string, next: string): Hover { 201 | // builtin variables 202 | if (isSomeMatchPattern(builtinVariablePattern, pre) && this.vimPredefinedVariableDocuments[name]) { 203 | return { 204 | contents: this.formatVimDocument(this.vimPredefinedVariableDocuments[name]), 205 | }; 206 | // options 207 | } else if ( 208 | isSomeMatchPattern(optionPattern, pre) 209 | && (this.vimOptionDocuments[name] || this.vimOptionDocuments[name.slice(1)]) 210 | ) { 211 | return { 212 | contents: this.formatVimDocument(this.vimOptionDocuments[name] || this.vimOptionDocuments[name.slice(1)]), 213 | }; 214 | // builtin functions 215 | } else if (builtinFunctionPattern.test(next) && this.vimBuiltFunctionDocuments[name]) { 216 | return { 217 | contents: this.formatVimDocument(this.vimBuiltFunctionDocuments[name]), 218 | }; 219 | // has features 220 | } else if (isSomeMatchPattern(featurePattern, pre) && this.vimFeatureDocuments[name]) { 221 | return { 222 | contents: this.formatVimDocument(this.vimFeatureDocuments[name]), 223 | }; 224 | // expand Keywords 225 | } else if (isSomeMatchPattern(expandPattern, pre) && this.expandKeywordDocuments[`<${name}>`]) { 226 | return { 227 | contents: this.formatVimDocument(this.expandKeywordDocuments[`<${name}>`]), 228 | }; 229 | // command 230 | } else if (isSomeMatchPattern(commandPattern, pre) && this.vimCommandDocuments[name]) { 231 | return { 232 | contents: this.formatVimDocument(this.vimCommandDocuments[name]), 233 | }; 234 | } 235 | } 236 | 237 | private async start() { 238 | const { runtimepath } = config; 239 | 240 | // get colorschemes 241 | if (runtimepath) { 242 | this.resolveColorschemes(runtimepath); 243 | } 244 | 245 | // get map args 246 | this.resolveMapArgs(); 247 | 248 | // get highlight arg keys 249 | this.resolveHighlightArgKeys(); 250 | 251 | // get highlight arg values 252 | this.resolveHighlightArgValues(); 253 | 254 | try { 255 | const data: IBuiltinDoc = buildDocs as IBuiltinDoc; 256 | this.vimBuiltinFunctionItems = data.completionItems.functions; 257 | this.vimBuiltinFunctionItems.forEach((item) => { 258 | if (!this.vimBuiltinFunctionMap[item.label]) { 259 | this.vimBuiltinFunctionMap[item.label] = true; 260 | } 261 | }); 262 | this.vimBuiltFunctionDocuments = data.documents.functions; 263 | this.vimCommandItems = data.completionItems.commands; 264 | this.vimCommandDocuments = data.documents.commands; 265 | this.vimPredefinedVariablesItems = data.completionItems.variables; 266 | this.vimPredefinedVariableDocuments = data.documents.variables; 267 | this.vimOptionItems = data.completionItems.options; 268 | this.vimOptionDocuments = data.documents.options; 269 | this.vimFeatureItems = data.completionItems.features; 270 | this.vimAutocmdItems = data.completionItems.autocmds; 271 | this.vimFeatureDocuments = data.documents.features; 272 | this.expandKeywordItems = data.completionItems.expandKeywords; 273 | this.expandKeywordDocuments = data.documents.expandKeywords; 274 | 275 | this.vimBuiltFunctionSignatureHelp = data.signatureHelp; 276 | } catch (error) { 277 | log.error(`[vimls]: parse docs/builtin-doc.json fail => ${error.message || error}`); 278 | } 279 | } 280 | 281 | // format vim document to markdown 282 | private formatVimDocument(document: string[]): MarkupContent { 283 | let indent: number = 0; 284 | return { 285 | kind: MarkupKind.Markdown, 286 | value: [ 287 | "```help", 288 | ...document.map((line) => { 289 | if (indent === 0) { 290 | const m = line.match(/^([ \t]+)/); 291 | if (m) { 292 | indent = m[1].length; 293 | } 294 | } 295 | return line.replace(new RegExp(`^[ \\t]{${indent}}`, "g"), "").replace(/\t/g, " "); 296 | }), 297 | "```", 298 | ].join("\n"), 299 | }; 300 | } 301 | 302 | private resolveMapArgs() { 303 | this.vimMapArgsItems = [ "", "", "", "