Add module parser
This commit is contained in:
@@ -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
|
||||||
@@ -174,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'],
|
||||||
@@ -215,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
|
||||||
@@ -230,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)
|
||||||
|
|
||||||
@@ -250,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)]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -80,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']
|
||||||
|
|||||||
Reference in New Issue
Block a user