From 79864d24eeb8690ee6f77d560c74dc5676cc1709 Mon Sep 17 00:00:00 2001 From: Regen Date: Tue, 31 Dec 2019 23:44:57 +0900 Subject: [PATCH] Add module parser --- src/cmake_language_server/api.py | 60 ++++++++++++++++-- src/cmake_language_server/server.py | 98 ++++++++++++++++++++--------- tests/test_api.py | 28 +++++++++ 3 files changed, 152 insertions(+), 34 deletions(-) diff --git a/src/cmake_language_server/api.py b/src/cmake_language_server/api.py index dcfb7d7..cf4659c 100644 --- a/src/cmake_language_server/api.py +++ b/src/cmake_language_server/api.py @@ -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 @@ -174,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'], @@ -215,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_': for i in range(10): self._builtin_variables[f'CMAKE_MATCH_{i}'] = doc @@ -230,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.+)\n +-+\n+? +(?:(?P
\w[\w\s]+)\n\^+\n+?)? +(?P.(?:.|\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) @@ -250,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)] diff --git a/src/cmake_language_server/server.py b/src/cmake_language_server/server.py index b95c0b8..1fd0786 100644 --- a/src/cmake_language_server/server.py +++ b/src/cmake_language_server/server.py @@ -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, - params.position, False) - if not ret: - return None - token = ret[0] + word = self._cursor_word(params.textDocument.uri, + params.position, False) + 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,19 +62,42 @@ class CMakeLanguageServer(LanguageServer): documentation=self._api.get_command_doc(x)) for x in commands) - variables = self._api.search_variable(token) - items.extend( - CompletionItem(x, - CompletionItemKind.Variable, - documentation=self._api.get_variable_doc(x)) - for x in variables) + if trigger is None or trigger == '{': + variables = self._api.search_variable(token) + items.extend( + CompletionItem(x, + CompletionItemKind.Variable, + 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: - return None - return Hover(MarkupContent(MarkupKind.Markdown, doc), ret[1]) + + 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 @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): + end = m.end() if include_all else cursor if m.start() <= cursor <= m.end(): - end = m.end() if include_all else cursor - return (line[m.start():end], + word = (line[m.start():end], Range(Position(position.line, m.start()), Position(position.line, end))) - + return word return None diff --git a/tests/test_api.py b/tests/test_api.py index e2baa04..e92c949 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -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('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']