Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
310c449250 | ||
|
|
bd4f357e59 | ||
|
|
02ae7c4a7c | ||
|
|
6e03b145ba | ||
|
|
b3123db464 | ||
|
|
7d85c2c8dd | ||
|
|
b08dc91e53 | ||
|
|
894b38d55d | ||
|
|
cc9877adbe | ||
|
|
66af586b2a | ||
|
|
c870e3d512 | ||
|
|
9e6fc1a277 | ||
|
|
79864d24ee | ||
|
|
6f93218462 | ||
|
|
600659fe81 | ||
|
|
af59e9b3f6 | ||
|
|
39aa03cd55 | ||
|
|
8d5c6b588c | ||
|
|
b2d8e66ef1 | ||
|
|
4624bdf4e8 | ||
|
|
6628dfe5d0 | ||
|
|
e73b0bab0f | ||
|
|
06f7a4669d | ||
|
|
1a8267bb74 | ||
|
|
9670ecfb59 | ||
|
|
3b8c225d06 | ||
|
|
ff727b7793 | ||
|
|
1ac3bf421d | ||
|
|
7919cf5025 | ||
|
|
b4dd6c840d | ||
|
|
0021c07b56 | ||
|
|
5e2736a710 | ||
|
|
f89f0d6b56 |
30
.github/workflows/tests.yml
vendored
30
.github/workflows/tests.yml
vendored
@@ -4,32 +4,38 @@ on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
env:
|
||||
CMAKE_VERSION: 3.14.7
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-18.04]
|
||||
python: [3.6, 3.7, 3.8]
|
||||
os: [ubuntu-18.04, windows-2016]
|
||||
steps:
|
||||
- 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 }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
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
|
||||
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 poetry tox-gh-actions
|
||||
- name: Test with tox
|
||||
run: |
|
||||
export PATH=$GITHUB_WORKSPACE/cmake-$CMAKE_VERSION-Linux-x86_64/bin:$PATH
|
||||
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
|
||||
|
||||
12
README.md
12
README.md
@@ -1,5 +1,9 @@
|
||||
# cmake-language-server
|
||||
[](https://pypi.org/project/cmake-language-server)
|
||||
[](https://aur.archlinux.org/packages/cmake-language-server/)
|
||||
[](https://github.com/regen100/cmake-language-server/actions)
|
||||
[](https://codecov.io/gh/regen100/cmake-language-server)
|
||||
[](https://github.com/regen100/cmake-language-server/blob/master/LICENSE)
|
||||
|
||||
CMake LSP Implementation.
|
||||
|
||||
@@ -12,11 +16,15 @@ Alpha Stage, work in progress.
|
||||
|
||||
## Commands
|
||||
|
||||
- cmake-language-server: LSP server
|
||||
- cmake-format: CLI frontend for formatting
|
||||
- `cmake-language-server`: LSP server
|
||||
- `cmake-format`: CLI frontend for formatting
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
$ pip install cmake-language-server
|
||||
```
|
||||
|
||||
### Clients
|
||||
|
||||
- Neovim ([neoclide/coc.nvim][coc.nvim])
|
||||
|
||||
8
codecov.yml
Normal file
8
codecov.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: 10%
|
||||
patch:
|
||||
default:
|
||||
threshold: 20%
|
||||
24
poetry.lock
generated
24
poetry.lock
generated
@@ -23,6 +23,14 @@ optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
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]]
|
||||
category = "dev"
|
||||
description = "Discover and load entry points from installed packages."
|
||||
@@ -197,6 +205,18 @@ wcwidth = "*"
|
||||
python = "<3.8"
|
||||
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]]
|
||||
category = "dev"
|
||||
description = "pytest plugin for test data directories and files"
|
||||
@@ -309,13 +329,14 @@ version = "0.6.0"
|
||||
more-itertools = "*"
|
||||
|
||||
[metadata]
|
||||
content-hash = "2fa2f64a1c51f6312594f611baa29f434f3a8ede16543988ac0d76a6de587c7f"
|
||||
content-hash = "284b539e6199a16441b6196fcbc38a374c886e328ae0c5e8bf07d0aaa47b0670"
|
||||
python-versions = "^3.6"
|
||||
|
||||
[metadata.hashes]
|
||||
atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"]
|
||||
attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"]
|
||||
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"]
|
||||
filelock = ["18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", "929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"]
|
||||
flake8 = ["19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", "8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"]
|
||||
@@ -333,6 +354,7 @@ pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
|
||||
pygls = ["3ee878a828b7bc0873a2ea44208d6846a91aa7dbbbdc052e7fe8cc689f6644fa", "780fd0c5ae95ad02ecaf70b071e43ff8ced8384b7d6bed19311a7b431d26fb88"]
|
||||
pyparsing = ["6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", "d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"]
|
||||
pytest = ["7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8", "ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0"]
|
||||
pytest-cov = ["cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", "cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"]
|
||||
pytest-datadir = ["1847ed0efe0bc54cac40ab3fba6d651c2f03d18dd01f2a582979604d32e7621e", "d3af1e738df87515ee509d6135780f25a15959766d9c2b2dbe02bf4fb979cb18"]
|
||||
six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"]
|
||||
toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "cmake-language-server"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "CMake LSP Implementation"
|
||||
license = "MIT"
|
||||
authors = ["regen"]
|
||||
@@ -30,6 +30,7 @@ yapf = "^0.28.0"
|
||||
pytest-datadir = "^1.3"
|
||||
tox = "^3.14"
|
||||
isort = "^4.3"
|
||||
pytest-cov = "^2.8"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
cmake-format = "cmake_language_server.formatter:main"
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = '0.1.0'
|
||||
__version__ = '0.1.1'
|
||||
|
||||
@@ -10,6 +10,15 @@ from typing import Dict, List, Optional, Pattern
|
||||
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):
|
||||
_cmake: str
|
||||
_build: Path
|
||||
@@ -17,6 +26,7 @@ class API(object):
|
||||
_builtin_commands: Dict[str, str]
|
||||
_builtin_variables: Dict[str, str]
|
||||
_builtin_variable_template: Dict[Pattern, str]
|
||||
_builtin_modules: Dict[str, str]
|
||||
_targets: List[str]
|
||||
_cached_variables: Dict[str, str]
|
||||
_generated_list_parsed: bool
|
||||
@@ -29,6 +39,7 @@ class API(object):
|
||||
self._builtin_commands = {}
|
||||
self._builtin_variables = {}
|
||||
self._builtin_variable_template = {}
|
||||
self._builtin_modules = {}
|
||||
self._targets = []
|
||||
self._cached_variables = {}
|
||||
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,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
@@ -131,7 +142,8 @@ foreach (variable ${variables})
|
||||
message("${variable}=${${variable}}")
|
||||
endforeach()
|
||||
''')
|
||||
p = subprocess.run([self._cmake, '-P', tmplist],
|
||||
p = subprocess.run(
|
||||
[self._cmake, '-P', str(tmplist)],
|
||||
cwd=cmake_files['paths']['source'],
|
||||
universal_newlines=True,
|
||||
stdout=subprocess.PIPE,
|
||||
@@ -173,6 +185,7 @@ endforeach()
|
||||
def parse_doc(self) -> None:
|
||||
self._parse_commands()
|
||||
self._parse_variables()
|
||||
self._parse_modules()
|
||||
|
||||
def _parse_commands(self) -> None:
|
||||
p = subprocess.run([self._cmake, '--help-commands'],
|
||||
@@ -185,9 +198,9 @@ endforeach()
|
||||
matches = re.finditer(
|
||||
r'''
|
||||
(?P<command>.+)\n
|
||||
-+\n\n
|
||||
-+\n+?
|
||||
[\s\S]*?
|
||||
(?P<signature>\ (?P=command)\s*\([^)]*\))
|
||||
(?P<signature>(?P=command)\s*\([^)]*\))
|
||||
''', p.stdout, re.VERBOSE)
|
||||
self._builtin_commands.clear()
|
||||
for match in matches:
|
||||
@@ -214,11 +227,7 @@ endforeach()
|
||||
self._builtin_variables.clear()
|
||||
for match in matches:
|
||||
variable = match.group('variable')
|
||||
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('. ', '. ')
|
||||
doc = _tidy_doc(match.group('doc'))
|
||||
if variable == 'CMAKE_MATCH_<n>':
|
||||
for i in range(10):
|
||||
self._builtin_variables[f'CMAKE_MATCH_{i}'] = doc
|
||||
@@ -229,6 +238,30 @@ endforeach()
|
||||
else:
|
||||
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]:
|
||||
return self._builtin_commands.get(command)
|
||||
|
||||
@@ -249,6 +282,24 @@ endforeach()
|
||||
if x.startswith(variable))
|
||||
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]:
|
||||
return [x for x in self._targets if x.startswith(target)]
|
||||
|
||||
|
||||
@@ -83,24 +83,46 @@ class Formatter(object):
|
||||
|
||||
|
||||
def main(args: List[str] = None):
|
||||
from pathlib import Path
|
||||
from argparse import ArgumentParser
|
||||
from .parser import ListParser
|
||||
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('-i',
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument('-i',
|
||||
'--inplace',
|
||||
action='store_true',
|
||||
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)
|
||||
|
||||
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()
|
||||
formatter = Formatter()
|
||||
for listpath in args.lists:
|
||||
if listpath is None:
|
||||
listpath = '(stdin)'
|
||||
content = sys.stdin.read()
|
||||
else:
|
||||
with listpath.open() as fp:
|
||||
content = fp.read()
|
||||
tokens, remain = list_parser.parse(content)
|
||||
@@ -110,14 +132,12 @@ def main(args: List[str] = None):
|
||||
if not remain:
|
||||
with listpath.open('w') as fp:
|
||||
fp.write(formatted)
|
||||
else:
|
||||
if args.diff:
|
||||
elif args.diff:
|
||||
diff = unified_diff(content.splitlines(True),
|
||||
formatted.splitlines(True), str(listpath),
|
||||
str(listpath), '(before formatting)',
|
||||
'(after formatting)')
|
||||
diffstr = ''.join(diff)
|
||||
if diffstr:
|
||||
print(diffstr, end='')
|
||||
else:
|
||||
print(formatted, end='')
|
||||
|
||||
@@ -23,8 +23,8 @@ class CMakeLanguageServer(LanguageServer):
|
||||
_parser: ListParser
|
||||
_api: API
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
self._parser = ListParser()
|
||||
self._api = None
|
||||
@@ -40,23 +40,21 @@ class CMakeLanguageServer(LanguageServer):
|
||||
self._api = API(cmake, Path(builddir))
|
||||
self._api.parse_doc()
|
||||
|
||||
@self.feature(COMPLETION, trigger_characters=['{'])
|
||||
@self.feature(COMPLETION, trigger_characters=['{', '('])
|
||||
def completions(params: CompletionParams):
|
||||
if (params.context.triggerKind ==
|
||||
CompletionTriggerKind.TriggerCharacter):
|
||||
token = ''
|
||||
trigger = params.context.triggerCharacter
|
||||
else:
|
||||
ret = self.cursor_word(params.textDocument.uri,
|
||||
word = self._cursor_word(params.textDocument.uri,
|
||||
params.position, False)
|
||||
if not ret:
|
||||
return None
|
||||
token = ret[0]
|
||||
token = '' if word is None else word[0]
|
||||
trigger = None
|
||||
|
||||
items: List[CompletionItem] = []
|
||||
|
||||
if trigger != '{':
|
||||
if trigger is None:
|
||||
commands = self._api.search_command(token)
|
||||
items.extend(
|
||||
CompletionItem(x,
|
||||
@@ -64,6 +62,7 @@ class CMakeLanguageServer(LanguageServer):
|
||||
documentation=self._api.get_command_doc(x))
|
||||
for x in commands)
|
||||
|
||||
if trigger is None or trigger == '{':
|
||||
variables = self._api.search_variable(token)
|
||||
items.extend(
|
||||
CompletionItem(x,
|
||||
@@ -71,12 +70,34 @@ class CMakeLanguageServer(LanguageServer):
|
||||
documentation=self._api.get_variable_doc(x))
|
||||
for x in variables)
|
||||
|
||||
if trigger != '{':
|
||||
if trigger is None:
|
||||
targets = self._api.search_target(token)
|
||||
items.extend(
|
||||
CompletionItem(x, CompletionItemKind.Class)
|
||||
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)
|
||||
|
||||
@self.feature(FORMATTING)
|
||||
@@ -97,15 +118,23 @@ class CMakeLanguageServer(LanguageServer):
|
||||
|
||||
@self.feature(HOVER)
|
||||
def hover(params: TextDocumentPositionParams):
|
||||
ret = self.cursor_word(params.textDocument.uri, params.position)
|
||||
if not ret:
|
||||
word = self._cursor_word(params.textDocument.uri, params.position,
|
||||
True)
|
||||
if not word:
|
||||
return None
|
||||
doc = self._api.get_command_doc(ret[0].lower())
|
||||
if not doc:
|
||||
doc = self._api.get_variable_doc(ret[0])
|
||||
if not doc:
|
||||
|
||||
candidates = [
|
||||
lambda x: self._api.get_command_doc(x.lower()),
|
||||
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 Hover(MarkupContent(MarkupKind.Markdown, doc), ret[1])
|
||||
|
||||
@self.thread()
|
||||
@self.feature(TEXT_DOCUMENT_DID_SAVE, includeText=False)
|
||||
@@ -114,21 +143,32 @@ class CMakeLanguageServer(LanguageServer):
|
||||
if self._api.query():
|
||||
self._api.read_reply()
|
||||
|
||||
def cursor_word(self,
|
||||
uri: str,
|
||||
position: Position,
|
||||
include_all: bool = True) -> Optional[Tuple[str, Range]]:
|
||||
def _cursor_function(self, uri: str, position: Position) -> Optional[str]:
|
||||
doc = self.workspace.get_document(uri)
|
||||
lines = doc.source.split('\n')[:position.line + 1]
|
||||
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)
|
||||
content = doc.source
|
||||
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
|
||||
for m in re.finditer(r'\w+', line):
|
||||
if m.start() <= cursor <= m.end():
|
||||
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()),
|
||||
Position(position.line, end)))
|
||||
|
||||
return word
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,59 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import pprint
|
||||
from subprocess import PIPE, run
|
||||
from threading import Thread
|
||||
|
||||
import pytest
|
||||
from pygls import features
|
||||
from pygls.server import LanguageServer
|
||||
|
||||
from cmake_language_server.server import CMakeLanguageServer
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def cmake_build(shared_datadir):
|
||||
from subprocess import run, PIPE
|
||||
source = shared_datadir / 'cmake'
|
||||
build = source / 'build'
|
||||
build.mkdir()
|
||||
run(['cmake', source],
|
||||
check=True,
|
||||
p = run(['cmake', str(source)],
|
||||
cwd=build,
|
||||
stdout=PIPE,
|
||||
stderr=PIPE,
|
||||
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
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
@@ -35,7 +35,14 @@ def test_read_cmake_files(cmake_build):
|
||||
assert api.query()
|
||||
api.read_reply()
|
||||
|
||||
import platform
|
||||
system = platform.system()
|
||||
if system == 'Linux':
|
||||
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):
|
||||
@@ -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('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']
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -72,3 +76,81 @@ if()
|
||||
) # h
|
||||
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
130
tests/test_server.py
Normal 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
|
||||
7
tox.ini
7
tox.ini
@@ -12,16 +12,15 @@ python =
|
||||
[testenv]
|
||||
whitelist_externals = poetry
|
||||
skip_install = true
|
||||
passenv = INCLUDE LIB LIBPATH Platform VCTools* VSCMD_* WindowsSDK*
|
||||
commands_pre =
|
||||
poetry install
|
||||
commands =
|
||||
poetry run pytest -sv tests
|
||||
poetry run pytest --cov-report=term --cov-report=xml --cov=src -sv tests
|
||||
|
||||
[testenv:lint]
|
||||
whitelist_externals = poetry
|
||||
skip_install = true
|
||||
commands =
|
||||
poetry run isort -c -rc src tests
|
||||
poetry run yapf -d -r src tests
|
||||
poetry run flake8
|
||||
poetry run flake8 src tests
|
||||
poetry run mypy src tests
|
||||
|
||||
Reference in New Issue
Block a user