33 Commits

Author SHA1 Message Date
Regen
310c449250 Merge branch 'release-v0.1.1' 2020-03-01 16:50:13 +09:00
Regen
bd4f357e59 Release v0.1.1 2020-03-01 16:42:53 +09:00
Regen
02ae7c4a7c Merge pull request #5 from regen100/format-stdin
Add stdin mode to cmake-format
2020-03-01 16:38:50 +09:00
Regen
6e03b145ba Add stdin mode to cmake-format 2020-03-01 16:26:59 +09:00
Regen
b3123db464 Merge branch 'add-codecovyml' 2020-03-01 16:26:07 +09:00
Regen
7d85c2c8dd Add codecov.yml 2020-03-01 16:16:21 +09:00
Regen
b08dc91e53 Merge branch 'add-test-format' 2020-02-29 19:53:28 +09:00
Regen
894b38d55d Add tests 2020-02-29 19:46:06 +09:00
Regen
cc9877adbe Refactor formatter 2020-02-29 19:45:36 +09:00
Regen
66af586b2a Merge branch 'add-server-test' 2020-01-04 02:17:26 +09:00
Regen
c870e3d512 Add server tests 2020-01-04 02:10:30 +09:00
Regen
9e6fc1a277 Merge branch 'add-include-completion' 2020-01-02 23:56:25 +09:00
Regen
79864d24ee Add module parser 2020-01-02 23:51:56 +09:00
Regen
6f93218462 Merge pull request #2 from otreblan/master
Aur badge
2019-12-24 22:31:59 +09:00
otreblan
600659fe81 Aur badge added 2019-12-22 14:02:35 -05:00
Regen
af59e9b3f6 Merge branch 'cov' 2019-12-23 02:52:44 +09:00
Regen
39aa03cd55 Clean cmake on CI 2019-12-23 02:48:01 +09:00
Regen
8d5c6b588c Add codecov badge 2019-12-23 02:43:14 +09:00
Regen
b2d8e66ef1 Integrate codecov 2019-12-23 02:42:07 +09:00
Regen
4624bdf4e8 WIP 2019-12-23 02:25:21 +09:00
Regen
6628dfe5d0 Merge branch 'fix-parse-commands' 2019-12-23 02:24:43 +09:00
Regen
e73b0bab0f Fix windows build 2019-12-23 02:20:41 +09:00
Regen
06f7a4669d Fix _parse_commands() 2019-12-23 01:04:16 +09:00
Regen
1a8267bb74 Add version option to cmake-format 2019-11-24 18:14:46 +09:00
Regen
9670ecfb59 Update README 2019-11-24 13:10:55 +09:00
Regen
3b8c225d06 Merge branch 'windows-ci' 2019-11-24 03:39:31 +09:00
Regen
ff727b7793 Add windows build 2019-11-24 03:01:06 +09:00
Regen
1ac3bf421d Remove cache 2019-11-23 17:40:05 +09:00
Regen
7919cf5025 Merge branch 'update-doc' 2019-11-23 15:08:59 +09:00
Regen
b4dd6c840d Add installation 2019-11-22 00:34:38 +09:00
Regen
0021c07b56 Add badges 2019-11-22 00:34:07 +09:00
Regen
5e2736a710 Merge branch 'fix-cache' 2019-11-22 00:10:47 +09:00
Regen
f89f0d6b56 Fix cache in CI 2019-11-17 00:48:32 +09:00
14 changed files with 539 additions and 94 deletions

View File

@@ -4,32 +4,38 @@ on: [push]
jobs: jobs:
build: build:
env:
CMAKE_VERSION: 3.14.7
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ubuntu-18.04]
python: [3.6, 3.7, 3.8] python: [3.6, 3.7, 3.8]
os: [ubuntu-18.04, windows-2016]
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Cache .tox
uses: actions/cache@v1
with:
path: .tox
key: ${{ runner.OS }}-tox-${{ hashFiles('poetry.lock') }}
restore-keys: |
${{ runner.OS }}-tox-
- name: Set up Python ${{ matrix.python }} - name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v1 uses: actions/setup-python@v1
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
- name: Setup VC
uses: ilammy/msvc-dev-cmd@v1
if: contains(matrix.os, 'windows')
- name: Install CMake
if: contains(matrix.os, 'ubuntu')
run: |
CMAKE_VERSION=3.14.7
curl -sSL https://github.com/Kitware/CMake/releases/download/v$CMAKE_VERSION/cmake-$CMAKE_VERSION-Linux-x86_64.tar.gz | tar xz
sudo cp -rT cmake-$CMAKE_VERSION-Linux-x86_64 /usr/local
rm -rf cmake-$CMAKE_VERSION-Linux-x86_64
- name: Install dependencies - name: Install dependencies
run: | run: |
curl -sSL https://github.com/Kitware/CMake/releases/download/v$CMAKE_VERSION/cmake-$CMAKE_VERSION-Linux-x86_64.tar.gz | tar xz
python -m pip install --upgrade setuptools pip wheel python -m pip install --upgrade setuptools pip wheel
python -m pip install poetry tox-gh-actions python -m pip install poetry tox-gh-actions
- name: Test with tox - name: Test with tox
run: | run: |
export PATH=$GITHUB_WORKSPACE/cmake-$CMAKE_VERSION-Linux-x86_64/bin:$PATH
tox tox
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: unittests
name: Python ${{ matrix.python }} on ${{ matrix.os }}
fail_ci_if_error: true

View File

@@ -1,5 +1,9 @@
# cmake-language-server # cmake-language-server
[![PyPI](https://img.shields.io/pypi/v/cmake-language-server)](https://pypi.org/project/cmake-language-server)
[![AUR version](https://img.shields.io/aur/version/cmake-language-server)](https://aur.archlinux.org/packages/cmake-language-server/)
[![GitHub Actions (Tests)](https://github.com/regen100/cmake-language-server/workflows/Tests/badge.svg)](https://github.com/regen100/cmake-language-server/actions) [![GitHub Actions (Tests)](https://github.com/regen100/cmake-language-server/workflows/Tests/badge.svg)](https://github.com/regen100/cmake-language-server/actions)
[![codecov](https://codecov.io/gh/regen100/cmake-language-server/branch/master/graph/badge.svg)](https://codecov.io/gh/regen100/cmake-language-server)
[![GitHub](https://img.shields.io/github/license/regen100/cmake-language-server)](https://github.com/regen100/cmake-language-server/blob/master/LICENSE)
CMake LSP Implementation. CMake LSP Implementation.
@@ -12,11 +16,15 @@ Alpha Stage, work in progress.
## Commands ## Commands
- cmake-language-server: LSP server - `cmake-language-server`: LSP server
- cmake-format: CLI frontend for formatting - `cmake-format`: CLI frontend for formatting
## Installation ## Installation
```bash
$ pip install cmake-language-server
```
### Clients ### Clients
- Neovim ([neoclide/coc.nvim][coc.nvim]) - Neovim ([neoclide/coc.nvim][coc.nvim])

8
codecov.yml Normal file
View File

@@ -0,0 +1,8 @@
coverage:
status:
project:
default:
threshold: 10%
patch:
default:
threshold: 20%

24
poetry.lock generated
View File

@@ -23,6 +23,14 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.4.1" version = "0.4.1"
[[package]]
category = "dev"
description = "Code coverage measurement for Python"
name = "coverage"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
version = "5.0"
[[package]] [[package]]
category = "dev" category = "dev"
description = "Discover and load entry points from installed packages." description = "Discover and load entry points from installed packages."
@@ -197,6 +205,18 @@ wcwidth = "*"
python = "<3.8" python = "<3.8"
version = ">=0.12" version = ">=0.12"
[[package]]
category = "dev"
description = "Pytest plugin for measuring coverage."
name = "pytest-cov"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.8.1"
[package.dependencies]
coverage = ">=4.4"
pytest = ">=3.6"
[[package]] [[package]]
category = "dev" category = "dev"
description = "pytest plugin for test data directories and files" description = "pytest plugin for test data directories and files"
@@ -309,13 +329,14 @@ version = "0.6.0"
more-itertools = "*" more-itertools = "*"
[metadata] [metadata]
content-hash = "2fa2f64a1c51f6312594f611baa29f434f3a8ede16543988ac0d76a6de587c7f" content-hash = "284b539e6199a16441b6196fcbc38a374c886e328ae0c5e8bf07d0aaa47b0670"
python-versions = "^3.6" python-versions = "^3.6"
[metadata.hashes] [metadata.hashes]
atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"] atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"]
attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"] attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"]
colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"]
coverage = ["0cd13a6e98c37b510a2d34c8281d5e1a226aaf9b65b7d770ef03c63169965351", "1a4b6b6a2a3a6612e6361130c2cc3dc4378d8c221752b96167ccbad94b47f3cd", "2ee55e6dba516ddf6f484aa83ccabbb0adf45a18892204c23486938d12258cde", "3be5338a2eb4ef03c57f20917e1d12a1fd10e3853fed060b6d6b677cb3745898", "44b783b02db03c4777d8cf71bae19eadc171a6f2a96777d916b2c30a1eb3d070", "475bf7c4252af0a56e1abba9606f1e54127cdf122063095c75ab04f6f99cf45e", "47c81ee687eafc2f1db7f03fbe99aab81330565ebc62fb3b61edfc2216a550c8", "4a7f8e72b18f2aca288ff02255ce32cc830bc04d993efbc87abf6beddc9e56c0", "50197163a22fd17f79086e087a787883b3ec9280a509807daf158dfc2a7ded02", "56b13000acf891f700f5067512b804d1ec8c301d627486c678b903859d07f798", "79388ae29c896299b3567965dbcd93255f175c17c6c7bca38614d12718c47466", "79fd5d3d62238c4f583b75d48d53cdae759fe04d4fb18fe8b371d88ad2b6f8be", "7fe3e2fde2bf1d7ce25ebcd2d3de3650b8d60d9a73ce6dcef36e20191291613d", "81042a24f67b96e4287774014fa27220d8a4d91af1043389e4d73892efc89ac6", "81326f1095c53111f8afc95da281e1414185f4a538609a77ca50bdfa39a6c207", "8873dc0d8f42142ea9f20c27bbdc485190fff93823c6795be661703369e5877d", "88d2cbcb0a112f47eef71eb95460b6995da18e6f8ca50c264585abc2c473154b", "91f2491aeab9599956c45a77c5666d323efdec790bfe23fcceafcd91105d585a", "979daa8655ae5a51e8e7a24e7d34e250ae8309fd9719490df92cbb2fe2b0422b", "9c871b006c878a890c6e44a5b2f3c6291335324b298c904dc0402ee92ee1f0be", "a6d092545e5af53e960465f652e00efbf5357adad177b2630d63978d85e46a72", "b5ed7837b923d1d71c4f587ae1539ccd96bfd6be9788f507dbe94dab5febbb5d", "ba259f68250f16d2444cbbfaddaa0bb20e1560a4fdaad50bece25c199e6af864", "be1d89614c6b6c36d7578496dc8625123bda2ff44f224cf8b1c45b810ee7383f", "c1b030a79749aa8d1f1486885040114ee56933b15ccfc90049ba266e4aa2139f", "c95bb147fab76f2ecde332d972d8f4138b8f2daee6c466af4ff3b4f29bd4c19e", "d52c1c2d7e856cecc05aa0526453cb14574f821b7f413cc279b9514750d795c1", "d609a6d564ad3d327e9509846c2c47f170456344521462b469e5cb39e48ba31c", "e1bad043c12fb58e8c7d92b3d7f2f49977dcb80a08a6d1e7a5114a11bf819fca", "e5a675f6829c53c87d79117a8eb656cc4a5f8918185a32fc93ba09778e90f6db", "fec32646b98baf4a22fdceb08703965bd16dea09051fbeb31a04b5b6e72b846c"]
entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"] entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"]
filelock = ["18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", "929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"] filelock = ["18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", "929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"]
flake8 = ["19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", "8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"] flake8 = ["19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", "8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"]
@@ -333,6 +354,7 @@ pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
pygls = ["3ee878a828b7bc0873a2ea44208d6846a91aa7dbbbdc052e7fe8cc689f6644fa", "780fd0c5ae95ad02ecaf70b071e43ff8ced8384b7d6bed19311a7b431d26fb88"] pygls = ["3ee878a828b7bc0873a2ea44208d6846a91aa7dbbbdc052e7fe8cc689f6644fa", "780fd0c5ae95ad02ecaf70b071e43ff8ced8384b7d6bed19311a7b431d26fb88"]
pyparsing = ["6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", "d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"] pyparsing = ["6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", "d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"]
pytest = ["7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8", "ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0"] pytest = ["7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8", "ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0"]
pytest-cov = ["cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", "cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"]
pytest-datadir = ["1847ed0efe0bc54cac40ab3fba6d651c2f03d18dd01f2a582979604d32e7621e", "d3af1e738df87515ee509d6135780f25a15959766d9c2b2dbe02bf4fb979cb18"] pytest-datadir = ["1847ed0efe0bc54cac40ab3fba6d651c2f03d18dd01f2a582979604d32e7621e", "d3af1e738df87515ee509d6135780f25a15959766d9c2b2dbe02bf4fb979cb18"]
six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"]
toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"]

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "cmake-language-server" name = "cmake-language-server"
version = "0.1.0" version = "0.1.1"
description = "CMake LSP Implementation" description = "CMake LSP Implementation"
license = "MIT" license = "MIT"
authors = ["regen"] authors = ["regen"]
@@ -30,6 +30,7 @@ yapf = "^0.28.0"
pytest-datadir = "^1.3" pytest-datadir = "^1.3"
tox = "^3.14" tox = "^3.14"
isort = "^4.3" isort = "^4.3"
pytest-cov = "^2.8"
[tool.poetry.scripts] [tool.poetry.scripts]
cmake-format = "cmake_language_server.formatter:main" cmake-format = "cmake_language_server.formatter:main"

View File

@@ -1 +1 @@
__version__ = '0.1.0' __version__ = '0.1.1'

View File

@@ -10,6 +10,15 @@ from typing import Dict, List, Optional, Pattern
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _tidy_doc(doc: str) -> str:
doc = doc.strip()
doc = re.sub(r':.+?:`(.+?)`', r'\1', doc)
doc = re.sub(r'``([^`]+)``', r'`\1`', doc)
doc = doc.replace('\n', ' ')
doc = doc.replace('. ', '. ')
return doc
class API(object): class API(object):
_cmake: str _cmake: str
_build: Path _build: Path
@@ -17,6 +26,7 @@ class API(object):
_builtin_commands: Dict[str, str] _builtin_commands: Dict[str, str]
_builtin_variables: Dict[str, str] _builtin_variables: Dict[str, str]
_builtin_variable_template: Dict[Pattern, str] _builtin_variable_template: Dict[Pattern, str]
_builtin_modules: Dict[str, str]
_targets: List[str] _targets: List[str]
_cached_variables: Dict[str, str] _cached_variables: Dict[str, str]
_generated_list_parsed: bool _generated_list_parsed: bool
@@ -29,6 +39,7 @@ class API(object):
self._builtin_commands = {} self._builtin_commands = {}
self._builtin_variables = {} self._builtin_variables = {}
self._builtin_variable_template = {} self._builtin_variable_template = {}
self._builtin_modules = {}
self._targets = [] self._targets = []
self._cached_variables = {} self._cached_variables = {}
self._generated_list_parsed = False self._generated_list_parsed = False
@@ -48,7 +59,7 @@ class API(object):
] ]
}''') }''')
proc = subprocess.run([self._cmake, self._build], proc = subprocess.run([self._cmake, str(self._build)],
universal_newlines=True, universal_newlines=True,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
@@ -131,7 +142,8 @@ foreach (variable ${variables})
message("${variable}=${${variable}}") message("${variable}=${${variable}}")
endforeach() endforeach()
''') ''')
p = subprocess.run([self._cmake, '-P', tmplist], p = subprocess.run(
[self._cmake, '-P', str(tmplist)],
cwd=cmake_files['paths']['source'], cwd=cmake_files['paths']['source'],
universal_newlines=True, universal_newlines=True,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
@@ -173,6 +185,7 @@ endforeach()
def parse_doc(self) -> None: def parse_doc(self) -> None:
self._parse_commands() self._parse_commands()
self._parse_variables() self._parse_variables()
self._parse_modules()
def _parse_commands(self) -> None: def _parse_commands(self) -> None:
p = subprocess.run([self._cmake, '--help-commands'], p = subprocess.run([self._cmake, '--help-commands'],
@@ -185,9 +198,9 @@ endforeach()
matches = re.finditer( matches = re.finditer(
r''' r'''
(?P<command>.+)\n (?P<command>.+)\n
-+\n\n -+\n+?
[\s\S]*? [\s\S]*?
(?P<signature>\ (?P=command)\s*\([^)]*\)) (?P<signature>(?P=command)\s*\([^)]*\))
''', p.stdout, re.VERBOSE) ''', p.stdout, re.VERBOSE)
self._builtin_commands.clear() self._builtin_commands.clear()
for match in matches: for match in matches:
@@ -214,11 +227,7 @@ endforeach()
self._builtin_variables.clear() self._builtin_variables.clear()
for match in matches: for match in matches:
variable = match.group('variable') variable = match.group('variable')
doc = match.group('doc') doc = _tidy_doc(match.group('doc'))
doc = re.sub(r':.+:`(.+)`', r'\1', doc)
doc = re.sub(r'``(.+)``', r'`\1`', doc)
doc = doc.replace('\n', ' ')
doc = doc.replace('. ', '. ')
if variable == 'CMAKE_MATCH_<n>': if variable == 'CMAKE_MATCH_<n>':
for i in range(10): for i in range(10):
self._builtin_variables[f'CMAKE_MATCH_{i}'] = doc self._builtin_variables[f'CMAKE_MATCH_{i}'] = doc
@@ -229,6 +238,30 @@ endforeach()
else: else:
self._builtin_variables[variable] = doc self._builtin_variables[variable] = doc
def _parse_modules(self) -> None:
p = subprocess.run([self._cmake, '--help-modules'],
stdout=subprocess.PIPE,
universal_newlines=True)
if p.returncode != 0:
return
matches = re.finditer(
r'''
(?P<module>.+)\n
-+\n+?
(?:(?P<header>\w[\w\s]+)\n\^+\n+?)?
(?P<doc>.(?:.|\n)+?\n\n)
''', p.stdout + '\n\n', re.VERBOSE)
self._builtin_modules.clear()
for match in matches:
module = match.group('module')
header = match.group('header')
doc = _tidy_doc(match.group('doc'))
if header is not None and header != 'Overview':
doc = ''
self._builtin_modules[module] = doc
def get_command_doc(self, command: str) -> Optional[str]: def get_command_doc(self, command: str) -> Optional[str]:
return self._builtin_commands.get(command) return self._builtin_commands.get(command)
@@ -249,6 +282,24 @@ endforeach()
if x.startswith(variable)) if x.startswith(variable))
return list(cached | builtin) return list(cached | builtin)
def get_module_doc(self, module: str, package: bool) -> Optional[str]:
if package:
return self._builtin_modules.get('Find' + module)
return self._builtin_modules.get(module)
def search_module(self, module: str, package: bool) -> List[str]:
if package:
module = 'Find' + module
return [
x[4:] for x in self._builtin_modules if x.startswith(module)
]
return [
x for x in self._builtin_modules
if x.startswith(module) and not x.startswith('Find')
]
def search_target(self, target: str) -> List[str]: def search_target(self, target: str) -> List[str]:
return [x for x in self._targets if x.startswith(target)] return [x for x in self._targets if x.startswith(target)]

View File

@@ -83,24 +83,46 @@ class Formatter(object):
def main(args: List[str] = None): def main(args: List[str] = None):
from pathlib import Path
from argparse import ArgumentParser from argparse import ArgumentParser
from .parser import ListParser
from difflib import unified_diff from difflib import unified_diff
from pathlib import Path
import sys
from . import __version__
from .parser import ListParser
parser = ArgumentParser() parser = ArgumentParser(
description='Format CMake list files.',
epilog='''
If no arguments are specified, it formats the code from
standard input and writes the result to the standard output.''',
)
parser.add_argument('lists', type=Path, nargs='*', help='CMake list files') parser.add_argument('lists', type=Path, nargs='*', help='CMake list files')
parser.add_argument('-i', group = parser.add_mutually_exclusive_group()
group.add_argument('-i',
'--inplace', '--inplace',
action='store_true', action='store_true',
help='inplace edit') help='inplace edit')
parser.add_argument('-d', '--diff', action='store_true', help='show diff') group.add_argument('-d', '--diff', action='store_true', help='show diff')
parser.add_argument('--version',
action='version',
version=f'%(prog)s {__version__}')
args = parser.parse_args(args) args = parser.parse_args(args)
if not args.lists and args.inplace:
print('error: cannot use -i when no arguments are specified.',
file=sys.stderr)
return
if not args.lists:
args.lists.append(None)
list_parser = ListParser() list_parser = ListParser()
formatter = Formatter() formatter = Formatter()
for listpath in args.lists: for listpath in args.lists:
if listpath is None:
listpath = '(stdin)'
content = sys.stdin.read()
else:
with listpath.open() as fp: with listpath.open() as fp:
content = fp.read() content = fp.read()
tokens, remain = list_parser.parse(content) tokens, remain = list_parser.parse(content)
@@ -110,14 +132,12 @@ def main(args: List[str] = None):
if not remain: if not remain:
with listpath.open('w') as fp: with listpath.open('w') as fp:
fp.write(formatted) fp.write(formatted)
else: elif args.diff:
if args.diff:
diff = unified_diff(content.splitlines(True), diff = unified_diff(content.splitlines(True),
formatted.splitlines(True), str(listpath), formatted.splitlines(True), str(listpath),
str(listpath), '(before formatting)', str(listpath), '(before formatting)',
'(after formatting)') '(after formatting)')
diffstr = ''.join(diff) diffstr = ''.join(diff)
if diffstr:
print(diffstr, end='') print(diffstr, end='')
else: else:
print(formatted, end='') print(formatted, end='')

View File

@@ -23,8 +23,8 @@ class CMakeLanguageServer(LanguageServer):
_parser: ListParser _parser: ListParser
_api: API _api: API
def __init__(self): def __init__(self, *args):
super().__init__() super().__init__(*args)
self._parser = ListParser() self._parser = ListParser()
self._api = None self._api = None
@@ -40,23 +40,21 @@ class CMakeLanguageServer(LanguageServer):
self._api = API(cmake, Path(builddir)) self._api = API(cmake, Path(builddir))
self._api.parse_doc() self._api.parse_doc()
@self.feature(COMPLETION, trigger_characters=['{']) @self.feature(COMPLETION, trigger_characters=['{', '('])
def completions(params: CompletionParams): def completions(params: CompletionParams):
if (params.context.triggerKind == if (params.context.triggerKind ==
CompletionTriggerKind.TriggerCharacter): CompletionTriggerKind.TriggerCharacter):
token = '' token = ''
trigger = params.context.triggerCharacter trigger = params.context.triggerCharacter
else: else:
ret = self.cursor_word(params.textDocument.uri, word = self._cursor_word(params.textDocument.uri,
params.position, False) params.position, False)
if not ret: token = '' if word is None else word[0]
return None
token = ret[0]
trigger = None trigger = None
items: List[CompletionItem] = [] items: List[CompletionItem] = []
if trigger != '{': if trigger is None:
commands = self._api.search_command(token) commands = self._api.search_command(token)
items.extend( items.extend(
CompletionItem(x, CompletionItem(x,
@@ -64,6 +62,7 @@ class CMakeLanguageServer(LanguageServer):
documentation=self._api.get_command_doc(x)) documentation=self._api.get_command_doc(x))
for x in commands) for x in commands)
if trigger is None or trigger == '{':
variables = self._api.search_variable(token) variables = self._api.search_variable(token)
items.extend( items.extend(
CompletionItem(x, CompletionItem(x,
@@ -71,12 +70,34 @@ class CMakeLanguageServer(LanguageServer):
documentation=self._api.get_variable_doc(x)) documentation=self._api.get_variable_doc(x))
for x in variables) for x in variables)
if trigger != '{': if trigger is None:
targets = self._api.search_target(token) targets = self._api.search_target(token)
items.extend( items.extend(
CompletionItem(x, CompletionItemKind.Class) CompletionItem(x, CompletionItemKind.Class)
for x in targets) for x in targets)
if trigger == '(':
func = self._cursor_function(params.textDocument.uri,
params.position)
if func is not None:
func = func.lower()
if func == 'include':
modules = self._api.search_module(token, False)
items.extend(
CompletionItem(x,
CompletionItemKind.Module,
documentation=self._api.
get_module_doc(x, False))
for x in modules)
elif func == 'find_package':
modules = self._api.search_module(token, True)
items.extend(
CompletionItem(x,
CompletionItemKind.Module,
documentation=self._api.
get_module_doc(x, True))
for x in modules)
return CompletionList(False, items) return CompletionList(False, items)
@self.feature(FORMATTING) @self.feature(FORMATTING)
@@ -97,15 +118,23 @@ class CMakeLanguageServer(LanguageServer):
@self.feature(HOVER) @self.feature(HOVER)
def hover(params: TextDocumentPositionParams): def hover(params: TextDocumentPositionParams):
ret = self.cursor_word(params.textDocument.uri, params.position) word = self._cursor_word(params.textDocument.uri, params.position,
if not ret: True)
if not word:
return None return None
doc = self._api.get_command_doc(ret[0].lower())
if not doc: candidates = [
doc = self._api.get_variable_doc(ret[0]) lambda x: self._api.get_command_doc(x.lower()),
if not doc: lambda x: self._api.get_variable_doc(x),
lambda x: self._api.get_module_doc(x, False),
lambda x: self._api.get_module_doc(x, True),
]
for c in candidates:
doc = c(word[0])
if doc is None:
continue
return Hover(MarkupContent(MarkupKind.Markdown, doc), word[1])
return None return None
return Hover(MarkupContent(MarkupKind.Markdown, doc), ret[1])
@self.thread() @self.thread()
@self.feature(TEXT_DOCUMENT_DID_SAVE, includeText=False) @self.feature(TEXT_DOCUMENT_DID_SAVE, includeText=False)
@@ -114,21 +143,32 @@ class CMakeLanguageServer(LanguageServer):
if self._api.query(): if self._api.query():
self._api.read_reply() self._api.read_reply()
def cursor_word(self, def _cursor_function(self, uri: str, position: Position) -> Optional[str]:
uri: str, doc = self.workspace.get_document(uri)
position: Position, lines = doc.source.split('\n')[:position.line + 1]
include_all: bool = True) -> Optional[Tuple[str, Range]]: lines[-1] = lines[-1][:position.character - 1].strip()
words = re.split(r'[\s\n()]+', '\n'.join(lines))
return words[-1] if words else None
def _cursor_line(self, uri: str, position: Position) -> str:
doc = self.workspace.get_document(uri) doc = self.workspace.get_document(uri)
content = doc.source content = doc.source
line = content.split('\n')[position.line] line = content.split('\n')[position.line]
return line
def _cursor_word(self,
uri: str,
position: Position,
include_all: bool = True) -> Optional[Tuple[str, Range]]:
line = self._cursor_line(uri, position)
cursor = position.character cursor = position.character
for m in re.finditer(r'\w+', line): for m in re.finditer(r'\w+', line):
if m.start() <= cursor <= m.end():
end = m.end() if include_all else cursor end = m.end() if include_all else cursor
return (line[m.start():end], if m.start() <= cursor <= m.end():
word = (line[m.start():end],
Range(Position(position.line, m.start()), Range(Position(position.line, m.start()),
Position(position.line, end))) Position(position.line, end)))
return word
return None return None

View File

@@ -1,16 +1,59 @@
import asyncio
import logging
import os
import pprint
from subprocess import PIPE, run
from threading import Thread
import pytest import pytest
from pygls import features
from pygls.server import LanguageServer
from cmake_language_server.server import CMakeLanguageServer
@pytest.fixture() @pytest.fixture()
def cmake_build(shared_datadir): def cmake_build(shared_datadir):
from subprocess import run, PIPE
source = shared_datadir / 'cmake' source = shared_datadir / 'cmake'
build = source / 'build' build = source / 'build'
build.mkdir() build.mkdir()
run(['cmake', source], p = run(['cmake', str(source)],
check=True,
cwd=build, cwd=build,
stdout=PIPE, stdout=PIPE,
stderr=PIPE, stderr=PIPE,
universal_newlines=True) universal_newlines=True)
if p.returncode != 0:
logging.error('env:\n' + pprint.pformat(os.environ))
logging.error('stdout:\n' + p.stdout)
logging.error('stderr:\n' + p.stderr)
raise RuntimeError("CMake failed")
yield build yield build
@pytest.fixture()
def client_server():
c2s_r, c2s_w = os.pipe()
s2c_r, s2c_w = os.pipe()
def start(ls: LanguageServer, fdr, fdw):
# TODO: better patch is needed
# disable `close()` to avoid error messages
close = ls.loop.close
ls.loop.close = lambda: None
ls.start_io(os.fdopen(fdr, 'rb'), os.fdopen(fdw, 'wb'))
ls.loop.close = close
server = CMakeLanguageServer(asyncio.new_event_loop())
server_thread = Thread(target=start, args=(server, c2s_r, s2c_w))
server_thread.start()
client = LanguageServer(asyncio.new_event_loop())
client_thread = Thread(target=start, args=(client, s2c_r, c2s_w))
client_thread.start()
yield client, server
client.send_notification(features.EXIT)
server.send_notification(features.EXIT)
server_thread.join()
client_thread.join()

View File

@@ -35,7 +35,14 @@ def test_read_cmake_files(cmake_build):
assert api.query() assert api.query()
api.read_reply() api.read_reply()
import platform
system = platform.system()
if system == 'Linux':
assert 'GNU' in api.get_variable_doc('CMAKE_CXX_COMPILER_ID') assert 'GNU' in api.get_variable_doc('CMAKE_CXX_COMPILER_ID')
elif system == 'Windows':
assert 'MSVC' in api.get_variable_doc('CMAKE_CXX_COMPILER_ID')
else:
raise RuntimeError('Unexpected system')
def test_parse_commands(cmake_build): def test_parse_commands(cmake_build):
@@ -73,3 +80,31 @@ def test_parse_variables(cmake_build):
assert api.get_variable_doc('BUILD_SHARED_LIBS') is not None assert api.get_variable_doc('BUILD_SHARED_LIBS') is not None
assert api.get_variable_doc('not_existing_variable') is None assert api.get_variable_doc('not_existing_variable') is None
def test_parse_modules(cmake_build):
api = API('cmake', cmake_build)
api.parse_doc()
p = subprocess.run(['cmake', '--help-module-list'],
universal_newlines=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
modules = p.stdout.strip().split('\n')
for module in modules:
if module.startswith('Find'):
assert api.get_module_doc(module[4:],
True) is not None, f'{module} not found'
else:
assert api.get_module_doc(module,
False) is not None, f'{module} not found'
assert api.get_module_doc('GoogleTest', False) is not None
assert api.get_module_doc('GoogleTest', True) is None
assert api.search_module('GoogleTest', False) == ['GoogleTest']
assert api.search_module('GoogleTest', True) == []
assert api.get_module_doc('Boost', False) is None
assert api.get_module_doc('Boost', True) is not None
assert api.search_module('Boost', False) == []
assert api.search_module('Boost', True) == ['Boost']

View File

@@ -1,4 +1,8 @@
from cmake_language_server.formatter import Formatter import sys
from contextlib import contextmanager
from io import StringIO
from cmake_language_server.formatter import Formatter, main
from cmake_language_server.parser import ListParser from cmake_language_server.parser import ListParser
@@ -72,3 +76,81 @@ if()
) # h ) # h
endif() endif()
''') ''')
@contextmanager
def mock_stdin(buf: str):
stdin = sys.stdin
sys.stdin = StringIO(buf)
yield
sys.stdin = stdin
def test_main_stdin(capsys):
with mock_stdin(' a()'):
main([])
captured = capsys.readouterr()
assert captured.out == 'a()\n'
assert captured.err == ''
def test_main_stdin_diff(capsys):
with mock_stdin(' a()'):
main(['-d'])
captured = capsys.readouterr()
assert '- a()' in captured.out
assert '+a()' in captured.out
assert captured.err == ''
def test_main_file_1(capsys, tmp_path):
testfile1 = tmp_path / 'list1.cmake'
with testfile1.open('w') as fp:
fp.write(' a()')
main([str(testfile1)])
captured = capsys.readouterr()
assert captured.out == 'a()\n'
assert captured.err == ''
def test_main_file_2(capsys, tmp_path):
testfile1 = tmp_path / 'list1.cmake'
with testfile1.open('w') as fp:
fp.write(' a()')
testfile2 = tmp_path / 'list2.cmake'
with testfile2.open('w') as fp:
fp.write(' b()')
main([str(testfile1), str(testfile2)])
captured = capsys.readouterr()
assert captured.out == 'a()\nb()\n'
assert captured.err == ''
def test_main_inplace(capsys, tmp_path):
testfile1 = tmp_path / 'list1.cmake'
with testfile1.open('w') as fp:
fp.write(' a()')
main(['-i', str(testfile1)])
captured = capsys.readouterr()
assert captured.out == ''
assert captured.err == ''
with testfile1.open() as fp:
content = fp.read()
assert content == 'a()\n'
def test_main_diff(capsys, tmp_path):
testfile1 = tmp_path / 'list1.cmake'
with testfile1.open('w') as fp:
fp.write(' a()')
main(['-d', str(testfile1)])
captured = capsys.readouterr()
assert str(testfile1) in captured.out
assert '- a()' in captured.out
assert '+a()' in captured.out
assert captured.err == ''

130
tests/test_server.py Normal file
View File

@@ -0,0 +1,130 @@
from concurrent import futures
from pathlib import Path
from typing import Optional
from pygls.features import (COMPLETION, FORMATTING, HOVER, INITIALIZE,
TEXT_DOCUMENT_DID_OPEN)
from pygls.server import LanguageServer
from pygls.types import (CompletionContext, CompletionParams,
CompletionTriggerKind, DidOpenTextDocumentParams,
DocumentFormattingParams, FormattingOptions,
InitializeParams, Position, TextDocumentIdentifier,
TextDocumentItem, TextDocumentPositionParams)
CALL_TIMEOUT = 2
def _init(client: LanguageServer, root: Path):
retry = 3
while retry > 0:
try:
client.lsp.send_request(
INITIALIZE,
InitializeParams(
process_id=1234, root_uri=root.as_uri(),
capabilities=None)).result(timeout=CALL_TIMEOUT)
except futures.TimeoutError:
retry -= 1
else:
break
def _open(client: LanguageServer, path: Path, text: Optional[str] = None):
if text is None:
with open(path) as fp:
text = fp.read()
client.lsp.notify(
TEXT_DOCUMENT_DID_OPEN,
DidOpenTextDocumentParams(
TextDocumentItem(path.as_uri(), 'cmake', 1, text)))
def test_initialize(client_server, datadir):
client, server = client_server
assert server._api is None
_init(client, datadir)
assert server._api is not None
def test_completions_invoked(client_server, datadir):
client, server = client_server
_init(client, datadir)
path = datadir / 'CMakeLists.txt'
_open(client, path, 'projec')
response = client.lsp.send_request(
COMPLETION,
CompletionParams(TextDocumentIdentifier(path.as_uri()), Position(
0, 6), CompletionContext(
CompletionTriggerKind.Invoked))).result(timeout=CALL_TIMEOUT)
item = next(filter(lambda x: x.label == 'project', response.items), None)
assert item is not None
assert '<PROJECT-NAME>' in item.documentation
def test_completions_triggercharacter_variable(client_server, datadir):
client, server = client_server
_init(client, datadir)
path = datadir / 'CMakeLists.txt'
_open(client, path, '${')
response = client.lsp.send_request(
COMPLETION,
CompletionParams(
TextDocumentIdentifier(path.as_uri()), Position(0, 2),
CompletionContext(CompletionTriggerKind.TriggerCharacter,
'{'))).result(timeout=CALL_TIMEOUT)
assert 'PROJECT_VERSION' in [x.label for x in response.items]
def test_completions_triggercharacter_module(client_server, datadir):
client, server = client_server
_init(client, datadir)
path = datadir / 'CMakeLists.txt'
_open(client, path, 'include(')
response = client.lsp.send_request(
COMPLETION,
CompletionParams(
TextDocumentIdentifier(path.as_uri()), Position(0, 8),
CompletionContext(CompletionTriggerKind.TriggerCharacter,
'('))).result(timeout=CALL_TIMEOUT)
assert 'GoogleTest' in [x.label for x in response.items]
def test_completions_triggercharacter_package(client_server, datadir):
client, server = client_server
_init(client, datadir)
path = datadir / 'CMakeLists.txt'
_open(client, path, 'find_package(')
response = client.lsp.send_request(
COMPLETION,
CompletionParams(
TextDocumentIdentifier(path.as_uri()), Position(0, 13),
CompletionContext(CompletionTriggerKind.TriggerCharacter,
'('))).result(timeout=CALL_TIMEOUT)
assert 'Boost' in [x.label for x in response.items]
def test_formatting(client_server, datadir):
client, server = client_server
_init(client, datadir)
path = datadir / 'CMakeLists.txt'
_open(client, path, 'a ( b c ) ')
response = client.lsp.send_request(
FORMATTING,
DocumentFormattingParams(TextDocumentIdentifier(path.as_uri()),
FormattingOptions(
2, True))).result(timeout=CALL_TIMEOUT)
assert response[0].newText == 'a(b c)\n'
def test_hover(client_server, datadir):
client, server = client_server
_init(client, datadir)
path = datadir / 'CMakeLists.txt'
_open(client, path, 'project()')
response = client.lsp.send_request(
HOVER,
TextDocumentPositionParams(TextDocumentIdentifier(path.as_uri()),
Position())).result(timeout=CALL_TIMEOUT)
assert '<PROJECT-NAME>' in response.contents.value

View File

@@ -12,16 +12,15 @@ python =
[testenv] [testenv]
whitelist_externals = poetry whitelist_externals = poetry
skip_install = true skip_install = true
passenv = INCLUDE LIB LIBPATH Platform VCTools* VSCMD_* WindowsSDK*
commands_pre = commands_pre =
poetry install poetry install
commands = commands =
poetry run pytest -sv tests poetry run pytest --cov-report=term --cov-report=xml --cov=src -sv tests
[testenv:lint] [testenv:lint]
whitelist_externals = poetry
skip_install = true
commands = commands =
poetry run isort -c -rc src tests poetry run isort -c -rc src tests
poetry run yapf -d -r src tests poetry run yapf -d -r src tests
poetry run flake8 poetry run flake8 src tests
poetry run mypy src tests poetry run mypy src tests