Initial commit
This commit is contained in:
255
src/cmake_language_server/api.py
Normal file
255
src/cmake_language_server/api.py
Normal file
@@ -0,0 +1,255 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Pattern
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class API(object):
|
||||
_cmake: str
|
||||
_build: Path
|
||||
_uuid: uuid.UUID
|
||||
_builtin_commands: Dict[str, str]
|
||||
_builtin_variables: Dict[str, str]
|
||||
_builtin_variable_template: Dict[Pattern, str]
|
||||
_targets: List[str]
|
||||
_cached_variables: Dict[str, str]
|
||||
_generated_list_parsed: bool
|
||||
|
||||
def __init__(self, cmake: str, build: Path):
|
||||
self._cmake = cmake
|
||||
self._build = Path(build)
|
||||
self._uuid = uuid.uuid4()
|
||||
|
||||
self._builtin_commands = {}
|
||||
self._builtin_variables = {}
|
||||
self._builtin_variable_template = {}
|
||||
self._targets = []
|
||||
self._cached_variables = {}
|
||||
self._generated_list_parsed = False
|
||||
|
||||
def query(self) -> bool:
|
||||
if not self.cmake_cache.exists():
|
||||
return False
|
||||
|
||||
self.query_json.parent.mkdir(parents=True, exist_ok=True)
|
||||
with self.query_json.open('w') as fp:
|
||||
fp.write('''\
|
||||
{
|
||||
"requests": [
|
||||
{"kind": "codemodel", "version": 2},
|
||||
{"kind": "cache", "version": 2},
|
||||
{"kind": "cmakeFiles", "version": 1}
|
||||
]
|
||||
}''')
|
||||
|
||||
proc = subprocess.run([self._cmake, self._build],
|
||||
universal_newlines=True,
|
||||
capture_output=True)
|
||||
self.query_json.unlink()
|
||||
self.query_json.parent.rmdir()
|
||||
if proc.returncode != 0:
|
||||
logging.error(
|
||||
f'cmake exited with {proc.returncode}: {proc.stderr}')
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def read_reply(self) -> bool:
|
||||
reply = self._build / '.cmake' / 'api' / 'v1' / 'reply'
|
||||
indices = sorted(reply.glob('index-*.json'))
|
||||
if not indices:
|
||||
logger.error('no reply')
|
||||
return False
|
||||
with indices[-1].open() as fp:
|
||||
index = json.load(fp)
|
||||
try:
|
||||
responses = index['reply'][f'client-{self._uuid}']['query.json'][
|
||||
'responses']
|
||||
except KeyError:
|
||||
logger.error('no rensponse')
|
||||
return False
|
||||
for response in responses:
|
||||
if response['kind'] == 'codemodel':
|
||||
self._read_codemodel(reply / response['jsonFile'])
|
||||
elif response['kind'] == 'cache':
|
||||
self._read_cache(reply / response['jsonFile'])
|
||||
elif response['kind'] == 'cmakeFiles':
|
||||
self._read_cmake_files(reply / response['jsonFile'])
|
||||
|
||||
return True
|
||||
|
||||
def _read_codemodel(self, codemodelpath: Path):
|
||||
with (codemodelpath).open() as fp:
|
||||
codemodel = json.load(fp)
|
||||
config = codemodel['configurations'][0]
|
||||
self._targets[:] = [x['name'] for x in config['targets']]
|
||||
|
||||
def _read_cache(self, cachepath: Path):
|
||||
with cachepath.open() as fp:
|
||||
cache = json.load(fp)
|
||||
self._cached_variables.clear()
|
||||
for entry in cache['entries']:
|
||||
name = entry['name']
|
||||
value = self._truncate_variable(entry['value'])
|
||||
properties = {x['name']: x['value'] for x in entry['properties']}
|
||||
helpstring = properties.get('HELPSTRING', '')
|
||||
doc = []
|
||||
if helpstring:
|
||||
doc.append(helpstring)
|
||||
if value:
|
||||
doc.append(f'`{value}`')
|
||||
self._cached_variables[name] = '\n\n'.join(doc)
|
||||
|
||||
def _read_cmake_files(self, jsonpath: Path):
|
||||
'''inspect generated list files'''
|
||||
|
||||
if not self._builtin_variables or self._generated_list_parsed:
|
||||
return
|
||||
|
||||
with jsonpath.open() as fp:
|
||||
cmake_files = json.load(fp)
|
||||
|
||||
# inspect generated list files
|
||||
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||
tmplist = Path(tmpdirname) / 'dump.cmake'
|
||||
with tmplist.open('w') as fp:
|
||||
for listfile in cmake_files['inputs']:
|
||||
if not listfile.get('isGenerated', False):
|
||||
continue
|
||||
path = listfile['path']
|
||||
fp.write(f'include({path})\n')
|
||||
fp.write('''
|
||||
get_cmake_property(variables VARIABLES)
|
||||
foreach (variable ${variables})
|
||||
message("${variable}=${${variable}}")
|
||||
endforeach()
|
||||
''')
|
||||
p = subprocess.run([self._cmake, '-P', tmplist],
|
||||
cwd=cmake_files['paths']['source'],
|
||||
universal_newlines=True,
|
||||
capture_output=True)
|
||||
if p.returncode != 0:
|
||||
return
|
||||
|
||||
for line in p.stderr.split('\n'):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
k, v = line.split('=', 1)
|
||||
if k.startswith('CMAKE_ARG'):
|
||||
continue
|
||||
v = self._truncate_variable(v)
|
||||
if k in self._builtin_variables:
|
||||
self._builtin_variables[k] += f'\n\n`{v}`'
|
||||
else:
|
||||
for pattern, doc in self._builtin_variable_template.items(
|
||||
):
|
||||
if pattern.fullmatch(k):
|
||||
self._builtin_variables[k] = f'{doc}\n\n`{v}`'
|
||||
break
|
||||
else:
|
||||
# ignore variable with no document
|
||||
pass
|
||||
|
||||
self._generated_list_parsed = True
|
||||
|
||||
@property
|
||||
def query_json(self) -> Path:
|
||||
return (self._build / '.cmake' / 'api' / 'v1' / 'query' /
|
||||
f'client-{self._uuid}' / 'query.json')
|
||||
|
||||
@property
|
||||
def cmake_cache(self) -> Path:
|
||||
return self._build / 'CMakeCache.txt'
|
||||
|
||||
def parse_doc(self) -> None:
|
||||
self._parse_commands()
|
||||
self._parse_variables()
|
||||
|
||||
def _parse_commands(self) -> None:
|
||||
p = subprocess.run([self._cmake, '--help-commands'],
|
||||
stdout=subprocess.PIPE,
|
||||
universal_newlines=True)
|
||||
|
||||
if p.returncode != 0:
|
||||
return
|
||||
|
||||
matches = re.finditer(
|
||||
r'''
|
||||
(?P<command>.+)\n
|
||||
-+\n\n
|
||||
[\s\S]*?
|
||||
(?P<signature>\ (?P=command)\s*\([^)]*\))
|
||||
''', p.stdout, re.VERBOSE)
|
||||
self._builtin_commands.clear()
|
||||
for match in matches:
|
||||
command = match.group('command')
|
||||
signature = match.group('signature')
|
||||
signature = re.sub(r'^ ', r'', signature, flags=re.MULTILINE)
|
||||
self._builtin_commands[
|
||||
command] = '```cmake\n' + signature + '\n```'
|
||||
|
||||
def _parse_variables(self) -> None:
|
||||
p = subprocess.run([self._cmake, '--help-variables'],
|
||||
stdout=subprocess.PIPE,
|
||||
universal_newlines=True)
|
||||
|
||||
if p.returncode != 0:
|
||||
return
|
||||
|
||||
matches = re.finditer(
|
||||
r'''
|
||||
(?P<variable>.+)\n
|
||||
-+\n\n
|
||||
(?P<doc>[\s\S]+?)(?:\n\n|$)
|
||||
''', p.stdout, re.VERBOSE)
|
||||
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('. ', '. ')
|
||||
if variable == 'CMAKE_MATCH_<n>':
|
||||
for i in range(10):
|
||||
self._builtin_variables[f'CMAKE_MATCH_{i}'] = doc
|
||||
elif '<' in variable:
|
||||
variable = re.sub(r'<[^>]+>', r'[^_]+', variable)
|
||||
pattern = re.compile(variable)
|
||||
self._builtin_variable_template[pattern] = doc
|
||||
else:
|
||||
self._builtin_variables[variable] = doc
|
||||
|
||||
def get_command_doc(self, command: str) -> Optional[str]:
|
||||
return self._builtin_commands.get(command)
|
||||
|
||||
def search_command(self, command: str) -> List[str]:
|
||||
command = command.lower()
|
||||
return [x for x in self._builtin_commands if x.startswith(command)]
|
||||
|
||||
def get_variable_doc(self, variable: str) -> Optional[str]:
|
||||
doc = self._cached_variables.get(variable)
|
||||
if doc:
|
||||
return doc
|
||||
return self._builtin_variables.get(variable)
|
||||
|
||||
def search_variable(self, variable: str) -> List[str]:
|
||||
cached = frozenset(x for x in self._cached_variables
|
||||
if x.startswith(variable))
|
||||
builtin = frozenset(x for x in self._builtin_variables
|
||||
if x.startswith(variable))
|
||||
return list(cached | builtin)
|
||||
|
||||
def search_target(self, target: str) -> List[str]:
|
||||
return [x for x in self._targets if x.startswith(target)]
|
||||
|
||||
def _truncate_variable(self, v: str) -> str:
|
||||
width = 70
|
||||
return v[:width] + (v[width:] and '...')
|
||||
Reference in New Issue
Block a user