├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── README.md ├── package.json ├── scripts ├── gen_package_type.js ├── gendoc.ts └── readme.md ├── src ├── client.ts ├── config.ts ├── core.ts ├── index.ts ├── provider │ ├── command.ts │ ├── completion.ts │ ├── format.ts │ └── hover.ts ├── types │ └── pkg-config.d.ts └── util.ts ├── tsconfig.json └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | }, 5 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 6 | parser: '@typescript-eslint/parser', 7 | parserOptions: { 8 | ecmaVersion: 2018, 9 | sourceType: 'module', 10 | }, 11 | plugins: ['@typescript-eslint'], 12 | rules: { 13 | '@typescript-eslint/explicit-function-return-type': 'off', 14 | '@typescript-eslint/no-explicit-any': 'off', 15 | '@typescript-eslint/no-non-null-assertion': 'off', 16 | '@typescript-eslint/no-empty-function': 'off', 17 | '@typescript-eslint/explicit-module-boundary-types': 'off', 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: push 4 | 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Setup Node 11 | uses: actions/setup-node@v1 12 | with: 13 | node-version: '12.x' 14 | registry-url: 'https://registry.npmjs.org' 15 | - name: Build Package 16 | run: | 17 | npm install -g json ts-node 18 | npm install 19 | - name: Publish 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | run: | 23 | pkgname=$(json name < package.json) 24 | localversion=$(json version < package.json) 25 | remoteversion=$(npm view ${pkgname} version) 26 | 27 | echo ${localversion} 28 | echo ${remoteversion} 29 | 30 | if [ ${localversion} \> ${remoteversion} ] 31 | then 32 | npm publish 33 | git config user.name github-actions 34 | git config user.email github-actions@github.com 35 | git tag -a ${localversion} -m v${localversion} 36 | git push --tags 37 | fi 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # For current directory only 2 | # ---------------------------------------------------------------------------- 3 | lib 4 | 5 | # General 6 | # ---------------------------------------------------------------------------- 7 | *.o 8 | *.out 9 | 10 | # log 11 | *.log 12 | 13 | # cache 14 | *.cache 15 | cache/ 16 | 17 | # Windows 18 | # ---------------------------------------------------------------------------- 19 | Thumbs.db 20 | Desktop.ini 21 | 22 | # Tags 23 | # ----------------------------------------------------------------------------- 24 | TAGS 25 | !TAGS/ 26 | tags 27 | tags-cn 28 | !tags/ 29 | .tags 30 | .tags1 31 | tags.lock 32 | tags.temp 33 | gtags.files 34 | GTAGS 35 | GRTAGS 36 | GPATH 37 | cscope.files 38 | cscope.out 39 | cscope.in.out 40 | cscope.po.out 41 | 42 | # Vim 43 | # ------------------------------------------------------------------------------ 44 | [._]*.s[a-w][a-z] 45 | [._]s[a-w][a-z] 46 | *.un~ 47 | Session.vim 48 | .netrwhist 49 | *~ 50 | 51 | # Test % Tmp 52 | # ------------------------------------------------------------------------------- 53 | test.* 54 | tmp.* 55 | temp.* 56 | 57 | # Java 58 | # ------------------------------------------------------------------------------- 59 | *.class 60 | 61 | # JavaScript 62 | # ------------------------------------------------------------------------------- 63 | node_modules 64 | yarn-debug.log* 65 | yarn-error.log* 66 | package-lock.json 67 | 68 | # Python 69 | # ------------------------------------------------------------------------------- 70 | *.pyc 71 | .idea/ 72 | /.idea 73 | build/ 74 | __pycache__ 75 | 76 | # Rust 77 | # ------------------------------------------------------------------------------- 78 | target/ 79 | **/*.rs.bk 80 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | tsconfig.json 4 | tslint.json 5 | yarn.lock 6 | webpack.config.js 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coc-cmake 2 | 3 | ![publish](https://github.com/voldikss/coc-cmake/workflows/publish/badge.svg) 4 | [![npm version](https://badge.fury.io/js/coc-cmake.svg)](https://badge.fury.io/js/coc-cmake) 5 | 6 | coc.nvim extension for cmake language. 7 | 8 | ![](https://user-images.githubusercontent.com/20282795/75767012-06869580-5d7d-11ea-9e89-8b8f173eed96.png) 9 | ![](https://user-images.githubusercontent.com/20282795/75767017-07b7c280-5d7d-11ea-900b-11eac5213b82.png) 10 | 11 | ## Features 12 | 13 | - Code completion 14 | - Code formatting 15 | - Hover documentation 16 | - Online document help 17 | 18 | ## Install 19 | 20 | ``` 21 | :CocInstall coc-cmake 22 | ``` 23 | 24 | ## Commands 25 | 26 | - `:CocCommand cmake.onlineHelp` 27 | 28 | ## Configuration 29 | 30 | 31 | 32 | Properties 33 |
34 | cmake.cmakePath: Path to CMake generator executable. 35 | Type:
string
Default:
"cmake"
36 |
37 |
38 | cmake.formatter: Path to [cmake-format](https://github.com/cheshirekow/cmake_format). 39 | Type:
string
Default:
"cmake-format"
40 |
41 |
42 | cmake.formatter_args: Additional arguments to be passed down to the formatter. 43 | Type:
string[]
Default:
[]
44 |
45 |
46 | cmake.lsp.enable: Enable language server(https://github.com/regen100/cmake-language-server), Notice that the functionality(completion, formatting, etc.) of lsp and extension builtin can not coexist. 47 | Type:
boolean
Default:
false
48 |
49 |
50 | cmake.lsp.serverPath: Path to [cmake-language-server](https://github.com/regen100/cmake-language-server). 51 | Type:
string
Default:
"cmake-language-server"
52 |
53 |
54 | cmake.lsp.buildDirectory: See https://github.com/regen100/cmake-language-server#configuration. 55 | Type:
string
Default:
"build"
56 |
57 | 58 | 59 | 60 | ## References 61 | 62 | - [vs.language.cmake](https://github.com/twxs/vs.language.cmake) 63 | - [cmake-format](https://github.com/cheshirekow/cmake_format) 64 | - [cmake-language-server](https://github.com/regen100/cmake-language-server) 65 | 66 | ## License 67 | 68 | MIT 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coc-cmake", 3 | "version": "0.2.1", 4 | "description": "coc.nvim extension for cmake language", 5 | "main": "lib/index.js", 6 | "publisher": "voldikss", 7 | "keywords": [ 8 | "coc.nvim", 9 | "cmake" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/voldikss/coc-cmake" 14 | }, 15 | "homepage": "https://github.com/voldikss/coc-cmake#readme", 16 | "engines": { 17 | "coc": "^0.0.80" 18 | }, 19 | "scripts": { 20 | "clean": "rimraf lib", 21 | "watch": "webpack --watch", 22 | "build:types": "node scripts/gen_package_type.js", 23 | "build:webpack": "webpack --mode production", 24 | "build:doc": "ts-node ./scripts/gendoc.ts", 25 | "build": "run-s build:types build:webpack build:doc", 26 | "prepare": "webpack" 27 | }, 28 | "activationEvents": [ 29 | "onLanguage:cmake" 30 | ], 31 | "contributes": { 32 | "configuration": { 33 | "title": "CMake", 34 | "type": "object", 35 | "properties": { 36 | "cmake.cmakePath": { 37 | "type": "string", 38 | "default": "cmake", 39 | "description": "Path to CMake generator executable" 40 | }, 41 | "cmake.formatter": { 42 | "type": "string", 43 | "default": "cmake-format", 44 | "description": "Path to [cmake-format](https://github.com/cheshirekow/cmake_format)" 45 | }, 46 | "cmake.formatter_args": { 47 | "type": "array", 48 | "default": [], 49 | "description": "Additional arguments to be passed down to the formatter", 50 | "items": { 51 | "type":"string" 52 | } 53 | }, 54 | "cmake.lsp.enable": { 55 | "type": "boolean", 56 | "default": false, 57 | "description": "Enable language server(https://github.com/regen100/cmake-language-server), Notice that the functionality(completion, formatting, etc.) of lsp and extension builtin can not coexist" 58 | }, 59 | "cmake.lsp.serverPath": { 60 | "type": "string", 61 | "default": "cmake-language-server", 62 | "description": "Path to [cmake-language-server](https://github.com/regen100/cmake-language-server)" 63 | }, 64 | "cmake.lsp.buildDirectory": { 65 | "type": "string", 66 | "default": "build", 67 | "description": "See https://github.com/regen100/cmake-language-server#configuration" 68 | } 69 | } 70 | }, 71 | "commands": [ 72 | { 73 | "command": "cmake.onlineHelp", 74 | "title": "CMake: Online Help" 75 | } 76 | ] 77 | }, 78 | "author": "dyzplus@gmail.com", 79 | "license": "MIT", 80 | "devDependencies": { 81 | "@types/node": "^14.17.15", 82 | "@types/tmp": "^0.2.0", 83 | "@typescript-eslint/eslint-plugin": "^4.11.1", 84 | "@typescript-eslint/parser": "^4.11.1", 85 | "@voldikss/tsconfig": "^1.0.0", 86 | "coc.nvim": "^0.0.80", 87 | "command-exists": "^1.2.9", 88 | "eslint": "^7.16.0", 89 | "json-schema-to-typescript": "^10.1.2", 90 | "npm-run-all": "^4.1.5", 91 | "opener": "^1.5.2", 92 | "rimraf": "^3.0.2", 93 | "tmp": "^0.2.1", 94 | "ts-loader": "^8.0.12", 95 | "tslib": "^1.14.1", 96 | "tslint": "^6.1.3", 97 | "typescript": "^4.1.2", 98 | "webpack": "^5.11.1", 99 | "webpack-cli": "^4.3.0" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /scripts/gen_package_type.js: -------------------------------------------------------------------------------- 1 | const Pkg = require('../package.json'); 2 | const fs = require('fs'); 3 | const { compile } = require('json-schema-to-typescript'); 4 | 5 | const fsp = fs.promises; 6 | 7 | async function main() { 8 | const s = await compile(Pkg.contributes.configuration, 'Extension', { 9 | style: { 10 | semi: true, 11 | singleQuote: true, 12 | }, 13 | }); 14 | await fsp.writeFile('src/types/pkg-config.d.ts', s); 15 | } 16 | 17 | main().then(console.error); 18 | 19 | -------------------------------------------------------------------------------- /scripts/gendoc.ts: -------------------------------------------------------------------------------- 1 | import Pkg from '../package.json'; 2 | import fs from 'fs'; 3 | import { JSONSchema7, JSONSchema7Type } from 'json-schema'; 4 | import ts from 'typescript'; 5 | import pathLib from 'path'; 6 | 7 | const fsp = fs.promises; 8 | 9 | type Definition = JSONSchema7; 10 | 11 | type Cmd = { 12 | title: string; 13 | command: string; 14 | }; 15 | 16 | type Section = { 17 | title?: string; 18 | rows: Row[]; 19 | }; 20 | 21 | type Row = { 22 | name: string; 23 | description: string; 24 | type?: string; 25 | default?: JSONSchema7Type; 26 | }; 27 | 28 | abstract class DocGenerator { 29 | protected ignorePrettierStart = ''; 30 | protected ignorePrettierEnd = ''; 31 | protected hint = ``; 32 | 33 | constructor(public generateCommand: string) {} 34 | 35 | abstract generate(): Promise; 36 | 37 | protected printJson(obj: JSONSchema7Type, format = false) { 38 | return JSON.stringify(obj, undefined, format ? ' ' : undefined); 39 | } 40 | 41 | protected printAsDetails(rows: Row[]) { 42 | const lines: string[] = []; 43 | rows.forEach((row) => { 44 | let hideLine = ''; 45 | if (row.type) { 46 | hideLine += `Type:
${row.type}
`; 47 | } 48 | if (row.default !== undefined) { 49 | hideLine += 'Default: '; 50 | hideLine += 51 | '
' + this.printJson(row.default, true) + '
'; 52 | } 53 | if (hideLine) { 54 | lines.push(`
`); 55 | } 56 | lines.push( 57 | `${row.name}: ${row.description}.`, 58 | ); 59 | if (hideLine) { 60 | lines.push(hideLine); 61 | lines.push('
'); 62 | } 63 | }); 64 | return lines; 65 | } 66 | 67 | /** 68 | * @deprecated 69 | */ 70 | protected printAsList(rows: Row[]) { 71 | const lines: string[] = []; 72 | rows.forEach((row) => { 73 | let line = `- \`${row.name}\``; 74 | const descriptions: string[] = []; 75 | if (row.description) { 76 | descriptions.push(row.description); 77 | } 78 | if (row.type) { 79 | descriptions.push(`type: \`${this.printJson(row.type)}\``); 80 | } 81 | if (row.default !== undefined) { 82 | descriptions.push(`default: \`${this.printJson(row.default)}\``); 83 | } 84 | if (descriptions.length) { 85 | line += ': ' + descriptions.join(', '); 86 | } 87 | lines.push(line); 88 | }); 89 | return lines; 90 | } 91 | 92 | async attach(headLevel: number, attachTitle: string, markdownPath: string) { 93 | const markdown = await fsp.readFile(markdownPath, 'utf8'); 94 | const markdownLines = markdown.split('\n'); 95 | let startIndex = markdownLines.findIndex((line) => 96 | new RegExp('#'.repeat(headLevel) + '\\s*' + attachTitle + '\\s*').test( 97 | line, 98 | ), 99 | ); 100 | if (startIndex < 0) { 101 | return; 102 | } 103 | startIndex += 1; 104 | const endIndex = markdownLines 105 | .slice(startIndex) 106 | .findIndex((line) => new RegExp(`#{1,${headLevel}}[^#]`).test(line)); 107 | const removeCount = endIndex < 0 ? 0 : endIndex; 108 | 109 | const sections = await this.generate(); 110 | const lines: string[] = ['', this.hint, this.ignorePrettierStart]; 111 | for (const section of sections) { 112 | if (section.title) { 113 | lines.push(`${section.title}`); 114 | } 115 | lines.push(...this.printAsDetails(section.rows)); 116 | } 117 | lines.push(''); 118 | lines.push(this.ignorePrettierEnd); 119 | lines.push(''); 120 | markdownLines.splice(startIndex, removeCount, ...lines); 121 | console.log(markdownLines.join('\n')) 122 | await fsp.writeFile(markdownPath, markdownLines.join('\n')); 123 | console.log(`Attached to ${attachTitle} header`); 124 | } 125 | } 126 | 127 | class ConfigurationDocGenerator extends DocGenerator { 128 | constructor( 129 | generateCommand: string, 130 | public packageDeclarationFilepath: string, 131 | ) { 132 | super(generateCommand); 133 | } 134 | 135 | isNodeExported(node: ts.Node) { 136 | return ( 137 | (ts.getCombinedModifierFlags(node as ts.Declaration) & 138 | ts.ModifierFlags.Export) !== 139 | 0 || 140 | (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile) 141 | ); 142 | } 143 | 144 | async generate() { 145 | const defRows: Row[] = []; 146 | const propRows: Row[] = []; 147 | 148 | const conf = Pkg.contributes.configuration; 149 | const title = conf.title; 150 | const filename = pathLib.basename(this.packageDeclarationFilepath); 151 | 152 | const Kind = ts.SyntaxKind; 153 | const prog = ts.createProgram([this.packageDeclarationFilepath], { 154 | strict: true, 155 | }); 156 | const sourceFile = prog.getSourceFile(this.packageDeclarationFilepath)!; 157 | const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); 158 | const checker = prog.getTypeChecker(); 159 | 160 | function print(node: ts.Node): string { 161 | return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); 162 | } 163 | 164 | function debug(node: ts.Node) { 165 | console.log(Kind[node.kind]); 166 | console.log(print(node)); 167 | } 168 | 169 | sourceFile.forEachChild((node) => { 170 | if (!this.isNodeExported(node)) { 171 | return; 172 | } 173 | 174 | if (ts.isTypeAliasDeclaration(node)) { 175 | defRows.push({ 176 | name: node.name.text, 177 | description: node.name.text, 178 | type: print(node.type), 179 | }); 180 | } else if (ts.isInterfaceDeclaration(node)) { 181 | if (node.name.text === title) { 182 | node.forEachChild((prop) => { 183 | if (!ts.isPropertySignature(prop)) { 184 | return; 185 | } 186 | const symbol = checker.getSymbolAtLocation(prop.name); 187 | if (!symbol) { 188 | return; 189 | } 190 | 191 | const name = symbol.getName(); 192 | // @ts-ignore 193 | const jsonProp = conf.properties[name as any] as Definition & { 194 | default_doc?: string; 195 | }; 196 | propRows.push({ 197 | name, 198 | description: ts.displayPartsToString( 199 | symbol.getDocumentationComment(checker), 200 | ), 201 | type: prop.type ? print(prop.type) : undefined, 202 | default: jsonProp.default_doc 203 | ? jsonProp.default_doc 204 | : jsonProp.default, 205 | }); 206 | }); 207 | } 208 | } else { 209 | console.error(`[gen_doc] ${filename} not support ${print(node)}`); 210 | } 211 | }); 212 | 213 | return [ 214 | { title: 'Properties', rows: propRows }, 215 | ]; 216 | } 217 | } 218 | 219 | // class CommandDocGenerator extends DocGenerator { 220 | // async generate() { 221 | // const cmds = Pkg.contributes.commands as Cmd[]; 222 | // const rows: Row[] = []; 223 | // cmds.forEach((cmd) => { 224 | // rows.push({ 225 | // name: cmd.command, 226 | // description: cmd.title, 227 | // }); 228 | // }); 229 | // return [{ rows }]; 230 | // } 231 | // } 232 | 233 | async function main() { 234 | const cmd = 'yarn run bulid:doc'; 235 | const markdownPath = `${__dirname}/../README.md` 236 | const packageDeclarationFilepath = `${__dirname}/../src/types/pkg-config.d.ts` 237 | // await new CommandDocGenerator(cmd).attach(2, 'Commands', markdownPath); 238 | await new ConfigurationDocGenerator(cmd, packageDeclarationFilepath).attach( 239 | 2, 240 | 'Configuration', 241 | markdownPath 242 | ); 243 | } 244 | 245 | main().catch(console.error); 246 | -------------------------------------------------------------------------------- /scripts/readme.md: -------------------------------------------------------------------------------- 1 | Reference: https://github.com/weirongxu/coc-explorer/tree/master/scripts 2 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import { LanguageClient, LanguageClientOptions, ServerOptions, window } from 'coc.nvim' 2 | import getConfig from './config' 3 | import { checkCommand } from './util' 4 | 5 | const serverPath = getConfig('lsp.serverPath') 6 | 7 | const serverOptions: ServerOptions = { 8 | command: serverPath, 9 | } 10 | 11 | const clientOptions: LanguageClientOptions = { 12 | documentSelector: ['cmake'], 13 | initializationOptions: { 14 | buildDirectory: getConfig('lsp.buildDirectory'), 15 | }, 16 | } 17 | 18 | export default class CMakeLanguageClient extends LanguageClient { 19 | constructor() { 20 | super('cmake', 'cmake language server', serverOptions, clientOptions) 21 | checkServerBin() 22 | } 23 | } 24 | 25 | async function checkServerBin(): Promise { 26 | const serverExists = await checkCommand(serverPath) 27 | if (!serverExists) { 28 | const install = await window.showPrompt( 29 | '`cmake.lsp.enable` is set to `true` but ' + 30 | 'cmake-language-server is not installed, install it?' 31 | ) 32 | if (install) { 33 | await window.openTerminal('pip install cmake-language-server') 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { workspace } from 'coc.nvim' 2 | 3 | export default function getConfig(key: string, defaultValue?: any): T { 4 | const cmake_conf = workspace.getConfiguration('cmake') 5 | return cmake_conf.get(key, defaultValue) 6 | } 7 | -------------------------------------------------------------------------------- /src/core.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompletionItem, 3 | CompletionItemKind, 4 | InsertTextFormat, 5 | Thenable, 6 | } from 'coc.nvim' 7 | import getConfig from './config' 8 | import { parseCmdArgs, strContains, strEquals } from './util' 9 | import child_process = require('child_process') 10 | 11 | function cmakeType2complKind(kind: string): CompletionItemKind { 12 | switch (kind) { 13 | case 'function': 14 | return CompletionItemKind.Function 15 | case 'variable': 16 | return CompletionItemKind.Variable 17 | case 'module': 18 | return CompletionItemKind.Module 19 | } 20 | return CompletionItemKind.Property 21 | } 22 | 23 | export function complKind2cmakeType(kind: CompletionItemKind): string { 24 | switch (kind) { 25 | case CompletionItemKind.Function: 26 | return 'function' 27 | case CompletionItemKind.Variable: 28 | return 'variable' 29 | case CompletionItemKind.Module: 30 | return 'module' 31 | } 32 | return 'property' 33 | } 34 | 35 | // Simple helper function that invoke the CMAKE executable and return a promise 36 | // with stdout 37 | export async function cmake(args: string[]): Promise { 38 | return new Promise((resolve, reject) => { 39 | const cmake_path = getConfig('cmakePath') 40 | const cmake_args = parseCmdArgs(cmake_path) 41 | const cmd = child_process.spawn( 42 | cmake_args[0], 43 | cmake_args 44 | .slice(1, cmake_args.length) 45 | .concat(args.map((arg) => arg.replace(/\r/gm, ''))) 46 | ) 47 | let stdout = '' 48 | cmd.stdout.on('data', (data) => { 49 | const txt: string = data.toString() 50 | stdout += txt.replace(/\r/gm, '') 51 | }) 52 | cmd.on('error', () => { 53 | reject() 54 | }) 55 | cmd.on('exit', () => resolve(stdout)) 56 | }) 57 | } 58 | 59 | // return the cmake command list 60 | function cmake_help_command_list(): Promise { 61 | return cmake(['--help-command-list']) 62 | } 63 | 64 | function cmake_help_command(name: string): Thenable { 65 | return cmake_help_command_list() 66 | .then( 67 | (result: string) => { 68 | const contains = result.indexOf(name) > -1 69 | return new Promise((resolve, reject) => { 70 | if (contains) { 71 | resolve(name) 72 | } else { 73 | reject('not found') 74 | } 75 | }) 76 | }, 77 | () => {} 78 | ) 79 | .then((n: string) => { 80 | return cmake(['--help-command', n]) 81 | }, null) 82 | } 83 | 84 | function cmake_help_variable_list(): Promise { 85 | return cmake(['--help-variable-list']) 86 | } 87 | 88 | function cmake_help_variable(name: string): Promise { 89 | return cmake_help_variable_list() 90 | .then( 91 | (result: string) => { 92 | const contains = result.indexOf(name) > -1 93 | return new Promise((resolve, reject) => { 94 | if (contains) { 95 | resolve(name) 96 | } else { 97 | reject('not found') 98 | } 99 | }) 100 | }, 101 | () => {} 102 | ) 103 | .then((name: string) => cmake(['--help-variable', name]), null) 104 | } 105 | 106 | function cmake_help_property_list(): Promise { 107 | return cmake(['--help-property-list']) 108 | } 109 | 110 | function cmake_help_property(name: string): Promise { 111 | return cmake_help_property_list() 112 | .then( 113 | (result: string) => { 114 | const contains = result.indexOf(name) > -1 115 | return new Promise((resolve, reject) => { 116 | if (contains) { 117 | resolve(name) 118 | } else { 119 | reject('not found') 120 | } 121 | }) 122 | }, 123 | () => {} 124 | ) 125 | .then((name: string) => cmake(['--help-property', name]), null) 126 | } 127 | 128 | function cmake_help_module_list(): Promise { 129 | return cmake(['--help-module-list']) 130 | } 131 | 132 | function cmake_help_module(name: string): Promise { 133 | return cmake_help_module_list() 134 | .then( 135 | (result: string) => { 136 | const contains = result.indexOf(name) > -1 137 | return new Promise((resolve, reject) => { 138 | if (contains) { 139 | resolve(name) 140 | } else { 141 | reject('not found') 142 | } 143 | }) 144 | }, 145 | () => {} 146 | ) 147 | .then((name: string) => cmake(['--help-module', name]), null) 148 | } 149 | 150 | export function cmake_help_all(): any { 151 | const promises = { 152 | function: (name: string) => { 153 | return cmake_help_command(name) 154 | }, 155 | module: (name: string) => { 156 | return cmake_help_module(name) 157 | }, 158 | variable: (name: string) => { 159 | return cmake_help_variable(name) 160 | }, 161 | property: (name: string) => { 162 | return cmake_help_property(name) 163 | }, 164 | } 165 | return promises 166 | } 167 | 168 | function suggestionsHelper( 169 | cmake_cmd: Promise, 170 | currentWord: string, 171 | type: string, 172 | insertText, 173 | matchPredicate 174 | ): Thenable { 175 | return new Promise((resolve, reject) => { 176 | cmake_cmd 177 | .then((stdout: string) => { 178 | const commands = stdout 179 | .split('\n') 180 | .filter((v) => matchPredicate(v, currentWord)) 181 | if (commands.length > 0) { 182 | const suggestions = commands.map((command_name) => { 183 | const item: CompletionItem = { label: command_name } 184 | item.kind = cmakeType2complKind(type) 185 | if (insertText == null || insertText == '') { 186 | item.insertText = command_name 187 | } else { 188 | item.insertTextFormat = InsertTextFormat.Snippet 189 | item.insertText = insertText(command_name) 190 | } 191 | return item 192 | }) 193 | resolve(suggestions) 194 | } else { 195 | resolve([]) 196 | } 197 | }) 198 | .catch((err) => reject(err)) 199 | }) 200 | } 201 | 202 | function cmModuleInsertText(module: string): string { 203 | if (module.indexOf('Find') == 0) { 204 | return 'find_package(' + module.replace('Find', '') + '${1: REQUIRED})' 205 | } else { 206 | return 'include(' + module + ')' 207 | } 208 | } 209 | 210 | function cmFunctionInsertText(func: string): string { 211 | const scoped_func = ['if', 'function', 'while', 'macro', 'foreach'] 212 | const is_scoped = scoped_func.reduceRight( 213 | (prev, name) => prev || func == name, 214 | false 215 | ) 216 | if (is_scoped) { 217 | return func + '(${1})\n\t\nend' + func + '(${1})\n' 218 | } else { 219 | return func + '(${1})' 220 | } 221 | } 222 | 223 | function cmVariableInsertText(variable: string): string { 224 | return variable.replace(/<(.*)>/g, '${1:<$1>}') 225 | } 226 | 227 | function cmPropetryInsertText(variable: string): string { 228 | return variable.replace(/<(.*)>/g, '${1:<$1>}') 229 | } 230 | 231 | export function cmCommandsSuggestions( 232 | currentWord: string 233 | ): Thenable { 234 | const cmd = cmake_help_command_list() 235 | return suggestionsHelper( 236 | cmd, 237 | currentWord, 238 | 'function', 239 | cmFunctionInsertText, 240 | strContains 241 | ) 242 | } 243 | 244 | export function cmVariablesSuggestions( 245 | currentWord: string 246 | ): Thenable { 247 | const cmd = cmake_help_variable_list() 248 | return suggestionsHelper( 249 | cmd, 250 | currentWord, 251 | 'variable', 252 | cmVariableInsertText, 253 | strContains 254 | ) 255 | } 256 | 257 | export function cmPropertiesSuggestions( 258 | currentWord: string 259 | ): Thenable { 260 | const cmd = cmake_help_property_list() 261 | return suggestionsHelper( 262 | cmd, 263 | currentWord, 264 | 'property', 265 | cmPropetryInsertText, 266 | strContains 267 | ) 268 | } 269 | 270 | export function cmModulesSuggestions( 271 | currentWord: string 272 | ): Thenable { 273 | const cmd = cmake_help_module_list() 274 | return suggestionsHelper( 275 | cmd, 276 | currentWord, 277 | 'module', 278 | cmModuleInsertText, 279 | strContains 280 | ) 281 | } 282 | 283 | export function cmCommandsSuggestionsExact( 284 | currentWord: string 285 | ): Thenable { 286 | const cmd = cmake_help_command_list() 287 | return suggestionsHelper( 288 | cmd, 289 | currentWord, 290 | 'function', 291 | cmFunctionInsertText, 292 | strEquals 293 | ) 294 | } 295 | 296 | export function cmVariablesSuggestionsExact( 297 | currentWord: string 298 | ): Thenable { 299 | const cmd = cmake_help_variable_list() 300 | return suggestionsHelper( 301 | cmd, 302 | currentWord, 303 | 'variable', 304 | cmVariableInsertText, 305 | strEquals 306 | ) 307 | } 308 | 309 | export function cmPropertiesSuggestionsExact( 310 | currentWord: string 311 | ): Thenable { 312 | const cmd = cmake_help_property_list() 313 | return suggestionsHelper( 314 | cmd, 315 | currentWord, 316 | 'property', 317 | cmPropetryInsertText, 318 | strEquals 319 | ) 320 | } 321 | 322 | export function cmModulesSuggestionsExact( 323 | currentWord: string 324 | ): Thenable { 325 | const cmd = cmake_help_module_list() 326 | return suggestionsHelper(cmd, currentWord, 'module', cmModuleInsertText, strEquals) 327 | } 328 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { commands, languages, services, window } from 'coc.nvim' 2 | import { checkCommand } from './util' 3 | import getConfig from './config' 4 | import onLineHelp from './provider/command' 5 | import CMakeExtraInfoProvider from './provider/hover' 6 | import CMakeCompletionProvider from './provider/completion' 7 | import CMakeFormattingEditProvider from './provider/format' 8 | import CMakeLanguageClient from './client' 9 | 10 | export async function activate(): Promise { 11 | if (!(await checkCommand(getConfig('cmakePath')))) { 12 | window.showMessage( 13 | 'Install cmake or specify its path using `cmake.cmakePath`.', 14 | 'error' 15 | ) 16 | return 17 | } 18 | 19 | commands.registerCommand( 20 | 'cmake.onlineHelp', 21 | async () => await onLineHelp() 22 | ) 23 | 24 | if (getConfig('lsp.enable')) { 25 | services.registLanguageClient( 26 | new CMakeLanguageClient() 27 | ) 28 | return 29 | } 30 | 31 | languages.registerHoverProvider( 32 | ['cmake'], 33 | new CMakeExtraInfoProvider() 34 | ) 35 | 36 | languages.registerDocumentFormatProvider( 37 | ['cmake'], 38 | new CMakeFormattingEditProvider() 39 | ) 40 | 41 | languages.registerCompletionItemProvider( 42 | 'coc-cmake', 43 | 'CMAKE', 44 | 'cmake', 45 | new CMakeCompletionProvider() 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/provider/command.ts: -------------------------------------------------------------------------------- 1 | import { window, workspace } from 'coc.nvim' 2 | import { 3 | complKind2cmakeType, 4 | cmCommandsSuggestionsExact, 5 | cmModulesSuggestionsExact, 6 | cmPropertiesSuggestionsExact, 7 | cmVariablesSuggestionsExact, 8 | cmake, 9 | } from '../core' 10 | import opener from 'opener' 11 | 12 | // Show Tooltip on over 13 | export default async function onLineHelp(): Promise { 14 | const document = await workspace.document 15 | const position = await window.getCursorPosition() 16 | const range = document.getWordRangeAtPosition(position) 17 | let currentWord = document.textDocument.getText(range) 18 | 19 | if (range && range.start.character < position.character) { 20 | const word = document.textDocument.getText(range) 21 | currentWord = word 22 | } 23 | 24 | let result = await window.requestInput( 25 | 'Search on Cmake online documentation', 26 | currentWord 27 | ) 28 | if (result != null) { 29 | if (result.length === 0) { 30 | result = currentWord 31 | } 32 | if (result != '') { 33 | await cmake_online_help(result) 34 | } 35 | } 36 | } 37 | 38 | export async function cmake_online_help(search: string): Promise { 39 | const url = await cmake_help_url() 40 | const v2x = url.endsWith('html') // cmake < 3.0 41 | return Promise.all([ 42 | cmCommandsSuggestionsExact(search), 43 | cmVariablesSuggestionsExact(search), 44 | cmModulesSuggestionsExact(search), 45 | cmPropertiesSuggestionsExact(search), 46 | ]).then((results) => { 47 | const suggestions = Array.prototype.concat.apply([], results) 48 | 49 | if (suggestions.length == 0) { 50 | search = search.replace(/[<>]/g, '') 51 | if (v2x || search.length == 0) { 52 | opener(url) 53 | } else { 54 | opener(`${url}search.html?q=${search}&check_keywords=yes&area=default`) 55 | } 56 | } else { 57 | const suggestion = suggestions[0] 58 | let type = complKind2cmakeType(suggestion.kind) 59 | if (type == 'property') { 60 | if (v2x) { 61 | opener(url) 62 | } else { 63 | // TODO : needs to filter properties per scope to detect the right URL 64 | opener(`${url}search.html?q=${search}&check_keywords=yes&area=default`) 65 | } 66 | } else { 67 | if (type == 'function') { 68 | type = 'command' 69 | } 70 | search = search.replace(/[<>]/g, '') 71 | if (v2x) { 72 | opener(`${url}#${type}:${search}`) 73 | } else { 74 | opener(`${url}${type}/${search}.html`) 75 | } 76 | } 77 | } 78 | }) 79 | } 80 | 81 | // Return the url for the online help based on the cmake executable binary used 82 | export async function cmake_help_url(): Promise { 83 | const base_url = 'https://cmake.org/cmake/help' 84 | let version = await cmake_version() 85 | if (version.length > 0) { 86 | if (version >= '3.0') { 87 | const re = /(\d+.\d+).\d+/ 88 | version = version.replace(re, '$1/') 89 | } else { 90 | const older_versions = [ 91 | '2.8.12', 92 | '2.8.11', 93 | '2.8.10', 94 | '2.8.9', 95 | '2.8.8', 96 | '2.8.7', 97 | '2.8.6', 98 | '2.8.5', 99 | '2.8.4', 100 | '2.8.3', 101 | '2.8.2', 102 | '2.8.1', 103 | '2.8.0', 104 | '2.6', 105 | ] 106 | if (older_versions.indexOf(version) == -1) { 107 | version = 'latest/' 108 | } else { 109 | version = version + '/cmake.html' 110 | } 111 | } 112 | } else { 113 | version = 'latest/' 114 | } 115 | return base_url + '/v' + version 116 | } 117 | 118 | async function cmake_version(): Promise { 119 | const cmd_output = await cmake(['--version']) 120 | const re = /cmake\s+version\s+(\d+.\d+.\d+)/ 121 | if (re.test(cmd_output)) { 122 | const result = re.exec(cmd_output) 123 | return result[1] 124 | } 125 | return '' 126 | } 127 | -------------------------------------------------------------------------------- /src/provider/completion.ts: -------------------------------------------------------------------------------- 1 | import { 2 | workspace, 3 | CompletionItemProvider, 4 | Thenable, 5 | CompletionItem, 6 | TextDocument, 7 | Position, 8 | ProviderResult, 9 | } from 'coc.nvim' 10 | import { 11 | complKind2cmakeType, 12 | cmake_help_all, 13 | cmCommandsSuggestions, 14 | cmModulesSuggestions, 15 | cmPropertiesSuggestions, 16 | cmVariablesSuggestions, 17 | } from '../core' 18 | 19 | export default class CMakeCompletionProvider implements CompletionItemProvider { 20 | provideCompletionItems( 21 | document: TextDocument, 22 | position: Position 23 | ): ProviderResult { 24 | const doc = workspace.getDocument(document.uri) 25 | if (!doc) return [] 26 | const wordRange = doc.getWordRangeAtPosition( 27 | Position.create(position.line, position.character - 1) 28 | ) 29 | if (!wordRange) return [] 30 | const text = document.getText(wordRange) 31 | 32 | return new Promise((resolve, reject) => { 33 | Promise.all([ 34 | cmCommandsSuggestions(text), 35 | cmVariablesSuggestions(text), 36 | cmPropertiesSuggestions(text), 37 | cmModulesSuggestions(text), 38 | ]) 39 | .then((results) => { 40 | const suggestions = Array.prototype.concat.apply([], results) 41 | resolve(suggestions) 42 | }) 43 | .catch((err) => { 44 | reject(err) 45 | }) 46 | }) 47 | } 48 | 49 | public resolveCompletionItem(item: CompletionItem): Thenable { 50 | const promises = cmake_help_all() 51 | const type = complKind2cmakeType(item.kind) 52 | return promises[type](item.label).then((result: string) => { 53 | item.documentation = result.split('\n')[3] 54 | return item 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/provider/format.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Uri, 3 | Range, 4 | ProviderResult, 5 | TextDocument, 6 | TextEdit, 7 | workspace, 8 | DocumentFormattingEditProvider, 9 | DocumentRangeFormattingEditProvider, 10 | window, 11 | } from 'coc.nvim' 12 | import getConfig from '../config' 13 | import { checkCommand, fsCreateTmpfile, fsWriteFile, runCommand } from '../util' 14 | 15 | export default class CMakeFormattingEditProvider 16 | implements DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider { 17 | provideDocumentFormattingEdits( 18 | document: TextDocument 19 | ): ProviderResult { 20 | return this._providerEdits(document) 21 | } 22 | 23 | provideDocumentRangeFormattingEdits( 24 | document: TextDocument, 25 | range: Range 26 | ): ProviderResult { 27 | return this._providerEdits(document, range) 28 | } 29 | 30 | async _providerEdits(document: TextDocument, range?: Range): Promise { 31 | workspace.nvim.command('update') 32 | const replacementText = await format(document, range) 33 | if (replacementText?.length == 0) return [] 34 | 35 | if (!range) range = wholeRange(document) 36 | 37 | return [TextEdit.replace(range, replacementText)] 38 | } 39 | } 40 | 41 | async function format(document: TextDocument, range?: Range): Promise { 42 | const formatter = getConfig('formatter') 43 | const args = Array.from(getConfig>('formatter_args')) 44 | if (!range) { 45 | args.push(Uri.parse(document.uri).fsPath) 46 | } else { 47 | // write the selected code into a tmp file and invoke formatter 48 | const tmpfile = await fsCreateTmpfile() 49 | const text = document.getText(range) 50 | await fsWriteFile(tmpfile, text) 51 | args.push(tmpfile) 52 | } 53 | 54 | try { 55 | return await runCommand(formatter, args) 56 | } catch { 57 | const formatterExists = await checkCommand(formatter) 58 | if (!formatterExists) { 59 | const install = await window.showPrompt( 60 | 'cmake-format is not installed, install it?' 61 | ) 62 | if (install) { 63 | await window.openTerminal('pip3 install cmake-format') 64 | } 65 | return '' 66 | } 67 | } 68 | } 69 | 70 | function wholeRange(document: TextDocument): Range { 71 | const doc = workspace.getDocument(document.uri) 72 | return Range.create( 73 | { 74 | line: 0, 75 | character: 0, 76 | }, 77 | { 78 | line: document.lineCount - 1, 79 | character: doc.getline(doc.lineCount - 1).length, 80 | } 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /src/provider/hover.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompletionItem, 3 | Hover, 4 | HoverProvider, 5 | Position, 6 | TextDocument, 7 | workspace 8 | } from "coc.nvim" 9 | import { 10 | complKind2cmakeType, 11 | cmake_help_all, 12 | cmCommandsSuggestionsExact, 13 | cmModulesSuggestionsExact, 14 | cmPropertiesSuggestionsExact, 15 | cmVariablesSuggestionsExact 16 | } from "../core" 17 | 18 | export default class CMakeExtraInfoProvider implements HoverProvider { 19 | 20 | public async provideHover(document: TextDocument, position: Position): Promise { 21 | const doc = workspace.getDocument(document.uri) 22 | if (!doc) return null 23 | const wordRange = doc.getWordRangeAtPosition(position) 24 | if (!wordRange) return null 25 | const text = document.getText(wordRange) || '' 26 | if (!text) { return null } 27 | const promises = cmake_help_all() 28 | 29 | return Promise.all([ 30 | cmCommandsSuggestionsExact(text), 31 | cmVariablesSuggestionsExact(text), 32 | cmModulesSuggestionsExact(text), 33 | cmPropertiesSuggestionsExact(text), 34 | ]).then(results => { 35 | const suggestions = Array.prototype.concat.apply([], results) 36 | if (suggestions.length == 0) { 37 | return null 38 | } 39 | const suggestion: CompletionItem = suggestions[0] 40 | 41 | return promises[complKind2cmakeType(suggestion.kind)](suggestion.label).then((result: string) => { 42 | let lines = result.split('\n') 43 | lines = lines.slice(2, lines.length) 44 | const hover: Hover = { 45 | contents: { 46 | kind: 'markdown', 47 | value: lines.join('\n') 48 | } 49 | } 50 | return hover 51 | }) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/types/pkg-config.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /** 3 | * This file was automatically generated by json-schema-to-typescript. 4 | * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, 5 | * and run json-schema-to-typescript to regenerate this file. 6 | */ 7 | 8 | export interface CMake { 9 | /** 10 | * Path to CMake generator executable 11 | */ 12 | 'cmake.cmakePath'?: string; 13 | /** 14 | * Path to [cmake-format](https://github.com/cheshirekow/cmake_format) 15 | */ 16 | 'cmake.formatter'?: string; 17 | /** 18 | * Additional arguments to be passed down to the formatter 19 | */ 20 | 'cmake.formatter_args'?: string[]; 21 | /** 22 | * Enable language server(https://github.com/regen100/cmake-language-server), Notice that the functionality(completion, formatting, etc.) of lsp and extension builtin can not coexist 23 | */ 24 | 'cmake.lsp.enable'?: boolean; 25 | /** 26 | * Path to [cmake-language-server](https://github.com/regen100/cmake-language-server) 27 | */ 28 | 'cmake.lsp.serverPath'?: string; 29 | /** 30 | * See https://github.com/regen100/cmake-language-server#configuration 31 | */ 32 | 'cmake.lsp.buildDirectory'?: string; 33 | [k: string]: unknown; 34 | } 35 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import child_process = require('child_process') 3 | import tmp from 'tmp' 4 | import commandExists from 'command-exists' 5 | 6 | export function strContains(word: string, pattern: string): boolean { 7 | return word.indexOf(pattern) > -1 8 | } 9 | 10 | export function strEquals(word: string, pattern: string): boolean { 11 | return word.toLowerCase() == pattern.toLowerCase() 12 | } 13 | 14 | // https://stackoverflow.com/questions/13796594/how-to-split-string-into-arguments-and-options-in-javascript 15 | export function parseCmdArgs(text: string): string[] { 16 | const re = /^"[^"]*"$/ // Check if argument is surrounded with double-quotes 17 | const re2 = /^([^"]|[^"].*?[^"])$/ // Check if argument is NOT surrounded with double-quotes 18 | 19 | const arr = [] 20 | let argPart = null 21 | 22 | // tslint:disable-next-line: no-unused-expression 23 | text && 24 | text.split(' ').forEach((arg) => { 25 | if ((re.test(arg) || re2.test(arg)) && !argPart) { 26 | arr.push(arg) 27 | } else { 28 | argPart = argPart ? argPart + ' ' + arg : arg 29 | // If part is complete (ends with a double quote), we can add it to the array 30 | if (/"$/.test(argPart)) { 31 | arr.push(argPart) 32 | argPart = null 33 | } 34 | } 35 | }) 36 | return arr 37 | } 38 | 39 | export async function fsCreateTmpfile(): Promise { 40 | return new Promise((resolve, reject) => { 41 | tmp.file((err, path) => { 42 | if (err) reject(new Error('Failed to create a tmp file')) 43 | resolve(path) 44 | }) 45 | }) 46 | } 47 | 48 | export async function fsWriteFile(fullpath: string, content: string): Promise { 49 | return new Promise((resolve, reject) => { 50 | fs.writeFile(fullpath, content, 'utf8', (err) => { 51 | if (err) reject() 52 | resolve() 53 | }) 54 | }) 55 | } 56 | 57 | export async function runCommand(command: string, args?: string[]): Promise { 58 | return new Promise((resolve, reject) => { 59 | child_process.execFile(command, args, (error, stdout) => { 60 | if (error) reject(error) 61 | resolve(stdout) 62 | }) 63 | }) 64 | } 65 | 66 | export async function checkCommand(command: string): Promise { 67 | return new Promise((resolve) => { 68 | commandExists(command, (_err, exists) => { 69 | resolve(exists) 70 | }) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@voldikss/tsconfig/tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "lib", 6 | "target": "es2015", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "lib": ["es2018"], 10 | "plugins": [] 11 | }, 12 | "include": ["src"], 13 | "exclude": [] 14 | } 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/index.ts', 5 | target: 'node', 6 | mode: 'none', 7 | resolve: { 8 | mainFields: ['module', 'main'], 9 | extensions: ['.js', '.ts'] 10 | }, 11 | externals: { 12 | 'coc.nvim': 'commonjs coc.nvim' 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.ts$/, 18 | exclude: /node_modules/, 19 | use: [ 20 | { 21 | loader: 'ts-loader', 22 | options: { 23 | compilerOptions: { 24 | sourceMap: true 25 | } 26 | } 27 | } 28 | ] 29 | } 30 | ] 31 | }, 32 | output: { 33 | path: path.join(__dirname, 'lib'), 34 | filename: 'index.js', 35 | libraryTarget: 'commonjs' 36 | }, 37 | plugins: [], 38 | node: { 39 | __dirname: false, 40 | __filename: false 41 | } 42 | }; 43 | --------------------------------------------------------------------------------