From 1fefcb4cbaa736c819e122dd733cfc7217a14e5f Mon Sep 17 00:00:00 2001 From: Regen Date: Sun, 1 May 2022 23:43:22 +0900 Subject: [PATCH] Use cmakelang formatter (#52) --- README.md | 3 +- poetry.lock | 46 ++++++- pyproject.toml | 2 +- src/cmake_language_server/formatter.py | 160 ----------------------- src/cmake_language_server/parser.py | 70 ----------- src/cmake_language_server/server.py | 46 +++---- tests/test_fomatter.py | 168 ------------------------- tests/test_parser.py | 83 ------------ 8 files changed, 66 insertions(+), 512 deletions(-) delete mode 100644 src/cmake_language_server/formatter.py delete mode 100644 src/cmake_language_server/parser.py delete mode 100644 tests/test_fomatter.py delete mode 100644 tests/test_parser.py diff --git a/README.md b/README.md index c0d0c95..24a2e84 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,11 @@ Alpha Stage, work in progress. ## Features - [x] Builtin command completion - [x] Documentation for commands and variables on hover -- [x] Formatting +- [x] Formatting (by [`cmake-format`](https://github.com/cheshirekow/cmake_format)) ## Commands - `cmake-language-server`: LSP server -- `python -m cmake_language_server.formatter`: CLI frontend for formatting ## Installation diff --git a/poetry.lock b/poetry.lock index 39b5991..f3e07f5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -20,6 +20,21 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +[[package]] +name = "cmakelang" +version = "0.6.13" +description = "Language tools for cmake (format, lint, etc)" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.13.0" + +[package.extras] +yaml = ["pyyaml (>=5.3)"] +html-gen = ["jinja2 (==2.10.3)"] + [[package]] name = "colorama" version = "0.4.4" @@ -142,11 +157,14 @@ ws = ["websockets (>=9.0.0,<10.0.0)"] [[package]] name = "pyparsing" -version = "2.4.7" +version = "3.0.7" description = "Python parsing module" -category = "main" +category = "dev" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" @@ -197,6 +215,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pytest = ">=2.7.0" +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "toml" version = "0.10.2" @@ -240,7 +266,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "8b534309919acadb4c5b088a7a9aff76066ab695b1d39bf507b32db898450b76" +content-hash = "1eaad6262e88c28de11d20ac8348f07b2a1b5afea514163be0965ac925588d91" [metadata.files] atomicwrites = [ @@ -251,6 +277,10 @@ attrs = [ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] +cmakelang = [ + {file = "cmakelang-0.6.13-py3-none-any.whl", hash = "sha256:764b9467195c7c36453d60a829f30229720d26c7dffd41cb516b99bd9c7daf4e"}, + {file = "cmakelang-0.6.13.tar.gz", hash = "sha256:03982e87b00654d024d73ef972d9d9bb0e5726cdb6b8a424a15661fb6278e67f"}, +] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, @@ -357,8 +387,8 @@ pygls = [ {file = "pygls-0.11.3.tar.gz", hash = "sha256:4d86fc854e6d6613cd42bf7511e9c6aac947fc8d62ff973a705570b036d969f2"}, ] pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, @@ -372,6 +402,10 @@ pytest-datadir = [ {file = "pytest-datadir-1.3.1.tar.gz", hash = "sha256:d3af1e738df87515ee509d6135780f25a15959766d9c2b2dbe02bf4fb979cb18"}, {file = "pytest_datadir-1.3.1-py2.py3-none-any.whl", hash = "sha256:1847ed0efe0bc54cac40ab3fba6d651c2f03d18dd01f2a582979604d32e7621e"}, ] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, diff --git a/pyproject.toml b/pyproject.toml index bc6251d..18cce69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,13 +20,13 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.6" pygls = "^0.11" -pyparsing = "^2.4" importlib-metadata = {version = "^4.8", python = "<3.8"} [tool.poetry.dev-dependencies] pytest = "^6.2" pytest-datadir = "^1.3" pytest-cov = "^2.11" +cmakelang = "^0.6.13" [tool.poetry.scripts] cmake-language-server = "cmake_language_server.server:main" diff --git a/src/cmake_language_server/formatter.py b/src/cmake_language_server/formatter.py deleted file mode 100644 index f24e42a..0000000 --- a/src/cmake_language_server/formatter.py +++ /dev/null @@ -1,160 +0,0 @@ -from typing import List, Optional - -from .parser import TokenList - - -class Formatter(object): - indent: str - lower_identifier: bool - - def __init__(self, indent: str = " ", lower_identifier: bool = True): - self.indent = indent - self.lower_identifier = lower_identifier - - def format(self, tokens: TokenList) -> str: - cmds: List[str] = [""] - indent_level = 0 - for token in tokens: - if isinstance(token, tuple): - raw_identifier = token[0] - identifier = raw_identifier.lower() - if identifier in ( - "elseif", - "else", - "endif", - "endforeach", - "endwhile", - "endmacro", - "endfunction", - ): - if indent_level > 0: - indent_level -= 1 - cmds[-1] = self.indent * indent_level - cmds[-1] += identifier if self.lower_identifier else raw_identifier - args = self._format_args(token[1]) - if len(args) < 2: - cmds[-1] += "(" + "".join(args) + ")" - else: - cmds[-1] += "(\n" - for arg in args: - cmds[-1] += self.indent * (indent_level + 1) + arg + "\n" - cmds[-1] += self.indent * indent_level + ")" - if identifier in ( - "if", - "elseif", - "else", - "foreach", - "while", - "macro", - "function", - ): - indent_level += 1 - elif token == "\n": - cmds.append("") - elif token[0] == "#": - if cmds[-1]: - cmds[-1] += token - else: - cmds[-1] = self.indent * indent_level + token - elif cmds[-1]: - cmds[-1] += token - - cmds = self._strip_line(cmds) - return "\n".join(cmds) + "\n" - - def _format_args(self, args: List[str]) -> List[str]: - lines = [""] - for i in range(len(args)): - arg = args[i] - if arg[0] == "#": - lines[-1] += arg - elif arg[0] == "\n": - lines.append("") - elif arg.isspace(): - if lines[-1]: - if i + 1 < len(args) and args[i + 1][0] == "#": - lines[-1] += arg - else: - lines[-1] += " " - else: - lines[-1] += arg - - return self._strip_line(lines) - - def _strip_line(self, lines: List[str]) -> List[str]: - """Delete empty lines at the start/end of the input""" - - ret: List[str] = [] - for line in lines: - line = line.rstrip() - if line != "" or len(ret) > 0: - ret.append(line) - while ret and ret[-1] == "": - del ret[-1] - return ret - - -def main(argss: Optional[List[str]] = None) -> None: - import sys - from argparse import ArgumentParser - from difflib import unified_diff - from pathlib import Path - - from . import __version__ - from .parser import ListParser - - 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") - group = parser.add_mutually_exclusive_group() - group.add_argument("-i", "--inplace", action="store_true", help="inplace edit") - 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(argss) - - 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) - formatted = content if remain else formatter.format(tokens) - - if args.inplace: - if not remain: - with listpath.open("w") as fp: - fp.write(formatted) - elif args.diff: - diff = unified_diff( - content.splitlines(True), - formatted.splitlines(True), - str(listpath), - str(listpath), - "(before formatting)", - "(after formatting)", - ) - diffstr = "".join(diff) - print(diffstr, end="") - else: - print(formatted, end="") - - -if __name__ == "__main__": - main() diff --git a/src/cmake_language_server/parser.py b/src/cmake_language_server/parser.py deleted file mode 100644 index 441f496..0000000 --- a/src/cmake_language_server/parser.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import List, Tuple, Union - -import pyparsing as pp - -CommandTokenType = Tuple[str, List[str]] -TokenType = Union[str, CommandTokenType] -TokenList = List[TokenType] - - -class ListParser(object): - _parser: pp.ParserElement - - def __init__(self) -> None: - newline = "\n" - space_plus = pp.Regex("[ \t]+") - space_star = pp.Optional(space_plus) - - quoted_element = pp.Regex(r'[^\\"]|\\[^A-Za-z0-9]|\\[trn]') - quoted_argument = pp.Combine('"' + pp.ZeroOrMore(quoted_element) + '"') - - bracket_content = pp.Forward() - - def action_bracket_open(tokens: pp.ParseResults) -> None: - nonlocal bracket_content - marker = "]" + "=" * (len(tokens[0]) - 2) + "]" - bracket_content <<= pp.SkipTo(marker, include=True) - - bracket_open = pp.Regex(r"\[=*\[").setParseAction(action_bracket_open) - bracket_argument = pp.Combine(bracket_open + bracket_content) - - unquoted_element = pp.Regex(r'[^\s()#"\\]|\\[^A-Za-z0-9]|\\[trn]') - unquoted_argument = pp.Combine(pp.OneOrMore(unquoted_element)) - - argument = bracket_argument | quoted_argument | unquoted_argument - - line_comment = pp.Combine("#" + ~bracket_open + pp.SkipTo(pp.LineEnd())) - bracket_comment = pp.Combine("#" + bracket_argument) - line_ending = ( - space_star - + pp.ZeroOrMore(bracket_comment + space_star) - + pp.Optional(line_comment) - + (newline | pp.lineEnd) - ) - - identifier = pp.Word(pp.alphas + "_", pp.alphanums + "_") - arguments = pp.Forward() - ( - arguments - << pp.ZeroOrMore( - argument | line_ending | space_plus | "(" + arguments + ")" - ).leaveWhitespace() - ) - arguments = pp.Group(arguments) - PAREN_L, PAREN_R = map(pp.Suppress, "()") - command_invocation = ( - identifier + space_star.suppress() + PAREN_L + arguments + PAREN_R - ).setParseAction(lambda t: (t[0], t[1].asList())) - - file_element = ( - space_star + command_invocation + line_ending | line_ending - ).leaveWhitespace() - file = pp.ZeroOrMore(file_element) - - self._parser = file - - def parse(self, liststr: str) -> Tuple[TokenList, str]: - for t, s, e in self._parser.scanString(liststr, maxMatches=1): - if s == 0: - return t.asList(), liststr[e:] - return [], liststr diff --git a/src/cmake_language_server/server.py b/src/cmake_language_server/server.py index 8364776..cb67941 100644 --- a/src/cmake_language_server/server.py +++ b/src/cmake_language_server/server.py @@ -1,5 +1,7 @@ import logging import re +import shutil +import subprocess from pathlib import Path from typing import Any, Callable, List, Optional, Tuple @@ -32,20 +34,16 @@ from pygls.lsp.types import ( from pygls.server import LanguageServer from .api import API -from .formatter import Formatter -from .parser import ListParser logger = logging.getLogger(__name__) class CMakeLanguageServer(LanguageServer): - _parser: ListParser _api: Optional[API] def __init__(self, *args: Any) -> None: super().__init__(*args) - self._parser = ListParser() self._api = None @self.feature(INITIALIZE) @@ -151,26 +149,30 @@ class CMakeLanguageServer(LanguageServer): return CompletionList(is_incomplete=False, items=items) - @self.feature(FORMATTING) - def formatting(params: DocumentFormattingParams) -> Optional[List[TextEdit]]: - doc = self.workspace.get_document(params.text_document.uri) - content = doc.source - tokens, remain = self._parser.parse(content) - if remain: - self.show_message("CMake parser failed") - return None + if shutil.which("cmake-format") is not None: - formatted = Formatter().format(tokens) - lines = content.count("\n") - return [ - TextEdit( - range=Range( - start=Position(line=0, character=0), - end=Position(line=lines + 1, character=0), - ), - new_text=formatted, + @self.feature(FORMATTING) + def formatting( + params: DocumentFormattingParams, + ) -> Optional[List[TextEdit]]: + doc = self.workspace.get_document(params.text_document.uri) + content = doc.source + formatted = subprocess.check_output( + ["cmake-format", "-"], + cwd=str(Path(doc.path).parent), + input=content, + universal_newlines=True, ) - ] + lines = content.count("\n") + return [ + TextEdit( + range=Range( + start=Position(line=0, character=0), + end=Position(line=lines + 1, character=0), + ), + new_text=formatted, + ) + ] @self.feature(HOVER) def hover(params: TextDocumentPositionParams) -> Optional[Hover]: diff --git a/tests/test_fomatter.py b/tests/test_fomatter.py deleted file mode 100644 index 87e367c..0000000 --- a/tests/test_fomatter.py +++ /dev/null @@ -1,168 +0,0 @@ -import sys -from contextlib import contextmanager -from io import StringIO -from pathlib import Path -from typing import Callable, Iterator - -from cmake_language_server.formatter import Formatter, main -from cmake_language_server.parser import ListParser -from pytest import CaptureFixture - - -def make_formatter_test(liststr: str, expect: str) -> Callable[[], None]: - def test() -> None: - tokens, remain = ListParser().parse(liststr) - actual = Formatter().format(tokens) - assert actual == expect - - return test - - -test_command = make_formatter_test("a()", "a()\n") -test_command_tolower = make_formatter_test("A()", "a()\n") -test_remove_space = make_formatter_test( - """ - #a - b ( c ) # d -""", - """\ -#a -b(c) # d -""", -) -test_indent_if = make_formatter_test( - """ -if() -a() # a - else() -# b -b() -endif() -""", - """\ -if() - a() # a -else() - # b - b() -endif() -""", -) -test_indent_if_nested = make_formatter_test( - """ -if() -if() -a() -b() -endif() -endif() -""", - """\ -if() - if() - a() - b() - endif() -endif() -""", -) -test_argument = make_formatter_test("a( b c d)", "a(b c d)\n") -test_argument_multiline = make_formatter_test( - """ -if() -a(b c -d # e -f -# g -) # h -endif() -""", - """\ -if() - a( - b c - d # e - f - # g - ) # h -endif() -""", -) - - -@contextmanager -def mock_stdin(buf: str) -> Iterator[None]: - stdin = sys.stdin - sys.stdin = StringIO(buf) - yield - sys.stdin = stdin - - -def test_main_stdin(capsys: CaptureFixture[str]) -> None: - with mock_stdin(" a()"): - main([]) - captured = capsys.readouterr() - assert captured.out == "a()\n" - assert captured.err == "" - - -def test_main_stdin_diff(capsys: CaptureFixture[str]) -> None: - 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: CaptureFixture[str], tmp_path: Path) -> None: - 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: CaptureFixture[str], tmp_path: Path) -> None: - 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: CaptureFixture[str], tmp_path: Path) -> None: - 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: CaptureFixture[str], tmp_path: Path) -> None: - 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 == "" diff --git a/tests/test_parser.py b/tests/test_parser.py deleted file mode 100644 index 4fac27d..0000000 --- a/tests/test_parser.py +++ /dev/null @@ -1,83 +0,0 @@ -from typing import Callable, List - -from cmake_language_server.parser import ListParser, TokenType - - -def make_parser_test( - liststr: str, expect_token: List[TokenType], expect_remain: str = "" -) -> Callable[[], None]: - def test() -> None: - actual_token, actual_remain = ListParser().parse(liststr) - assert actual_token == expect_token - assert actual_remain == expect_remain - - return test - - -test_command_no_args = make_parser_test("a()", [("a", [])]) -test_command_space = make_parser_test(" a ()", [" ", ("a", [])]) -test_command_arg = make_parser_test("a(b)", [("a", ["b"])]) -test_command_arg_space = make_parser_test("a ( b )", [("a", ["b"])]) -test_command_arg_escape = make_parser_test(r"a(\n\")", [("a", [r"\n\""])]) -test_command_arg_paren = make_parser_test("a((b))", [("a", ["(", "b", ")"])]) -test_command_arg_paren_paren = make_parser_test( - "a(((b)))", [("a", ["(", "(", "b", ")", ")"])] -) -test_command_arg_quote = make_parser_test(r'a("b\"")', [("a", [r'"b\""'])]) -test_command_arg_quote_cont = make_parser_test('a("\\\n")', [("a", ['"\\\n"'])]) -test_command_arg_quo_multiline = make_parser_test( - """a("b -c -")""", - [("a", ['"b\nc\n"'])], -) -test_command_arg_bracket_0 = make_parser_test("a([[b]])", [("a", ["[[b]]"])]) -test_command_arg_bracket_1 = make_parser_test("a([=[b]=])", [("a", ["[=[b]=]"])]) -test_command_arg_space = make_parser_test("a ( b )", [("a", [" ", "b", " "])]) -test_command_arg_multi = make_parser_test("a(b c)", [("a", ["b", " ", "c"])]) -test_command_multielement = make_parser_test( - """a( - b - c # c -)""", - [("a", ["\n", " ", "b", "\n", " ", "c", " ", "# c", "\n"])], -) -test_line_comment = make_parser_test("a() # b # c", [("a", []), " ", "# b # c"]) -test_bracket_comment = make_parser_test("#[[a]]#[[b]]", ["#[[a]]", "#[[b]]"]) -test_bracket_comment_nested = make_parser_test("#[=[[[a]]]=]", ["#[=[[[a]]]=]"]) -test_bracket_comment_multiline = make_parser_test( - "#[[\na\nb\nc\n]]", ["#[[\na\nb\nc\n]]"] -) -test_if_block = make_parser_test( - """if() - a() -else() - b() -endif()""", - [ - ("if", []), - "\n", - " ", - ("a", []), - "\n", - ("else", []), - "\n", - " ", - ("b", []), - "\n", - ("endif", []), - ], -) -test_comment_multi_linecomment = make_parser_test( - """a()# a -b() # b -c() # c""", - [("a", []), "# a", "\n", ("b", []), " ", "# b", "\n", ("c", []), " ", "# c"], -) - -test_incomplete_id = make_parser_test("a", [], "a") -test_incomplete_command = make_parser_test("a(", [], "a(") -test_incomplete_id_after_command = make_parser_test("a()\nb", [("a", []), "\n"], "b") -test_incomplete_command_after_command = make_parser_test( - "a()\nb(c", [("a", []), "\n"], "b(c" -)