diff --git a/README.md b/README.md index c142b3f..115f1c3 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ A language server for [tmux](https://github.com/tmux/tmux)'s tmux.conf. - [x] [Hover](https://microsoft.github.io/language-server-protocol/specifications/specification-current#textDocument_hover) - [x] [Completion](https://microsoft.github.io/language-server-protocol/specifications/specification-current#textDocument_completion) +![Diagnostic](https://github.com/Freed-Wu/tmux-language-server/assets/32936898/a92a7d41-4ade-486b-98ef-14382d6d4722) + ![Document hover](https://github.com/Freed-Wu/tmux-language-server/assets/32936898/631db877-4cde-4b87-9548-c0a66335a83d) ![Completion](https://github.com/Freed-Wu/tmux-language-server/assets/32936898/a9793a05-7da6-4fcb-88bf-4ca82ccfbfc1) diff --git a/src/tmux_language_server/__main__.py b/src/tmux_language_server/__main__.py index 9af6b4d..61ded86 100644 --- a/src/tmux_language_server/__main__.py +++ b/src/tmux_language_server/__main__.py @@ -55,6 +55,12 @@ def get_parser(): default="auto", help="when to display color, default: %(default)s", ) + parser.add_argument( + "--convert", + nargs="*", + default={}, + help="convert files to output format", + ) parser.add_argument( "--output-format", choices=["json", "yaml", "toml"], @@ -68,12 +74,13 @@ def main(): r"""Parse arguments and provide shell completions.""" args = get_parser().parse_args() - if args.generate_schema or args.check: + if args.generate_schema or args.check or args.convert: from tree_sitter_lsp.diagnose import check from tree_sitter_lsp.utils import pprint from tree_sitter_tmux import parser from .finders import DIAGNOSTICS_FINDER_CLASSES + from .schema import TmuxTrie if args.generate_schema: from .misc import get_schema @@ -84,6 +91,12 @@ def main(): indent=args.indent, ) return None + for file in args.convert: + pprint( + TmuxTrie.from_file(file, parser.parse).to_json(), + filetype=args.output_format, + indent=args.indent, + ) exit( check( args.check, diff --git a/src/tmux_language_server/assets/json/tmux.json b/src/tmux_language_server/assets/json/tmux.json index da79bd6..1acd514 100644 --- a/src/tmux_language_server/assets/json/tmux.json +++ b/src/tmux_language_server/assets/json/tmux.json @@ -4,11 +4,6 @@ "$comment": "Don't edit this file directly! It is generated by `tmux-language-server --generate-schema=tmux`.", "type": "object", "properties": { - "patternProperties": { - "@[-_\\da-zA-Z]": { - "type": "string" - } - }, "attach-session": { "description": "```tmux\nattach-session [-dErx] [-c working-directory] [-f flags] [-t target-session]\nattach [-dErx] [-c working-directory] [-f flags] [-t target-session]\n```\n\nIf run from outside tmux, create a new client in the current terminal and attach it to target-session. If used from inside, switch the current client. If -d is specified, any other clients attached to the session are detached. If -x is given, send SIGHUP to the parent process of the client as well as detaching the client, typically causing it to exit. -f sets a comma-separated list of client flags. The flags are:" }, @@ -91,7 +86,12 @@ "description": "```tmux\nshow-messages [-JT] [-t target-client]\nshowmsgs [-JT] [-t target-client]\n```\n\nShow server messages or information. Messages are stored, up to a maximum of the limit set by the message-limit server option. -J and -T show debugging information about jobs and terminals." }, "source-file": { - "description": "```tmux\nsource-file [-Fnqv] path ...\nsource [-Fnqv] path ...\n```\n\nExecute commands from one or more files specified by path (which may be glob(7) patterns). If -F is present, then path is expanded as a format. If -q is given, no error will be returned if path does not exist. With -n, the file is parsed but no commands are executed. -v shows the parsed commands and line numbers if possible." + "description": "```tmux\nsource-file [-Fnqv] path ...\nsource [-Fnqv] path ...\n```\n\nExecute commands from one or more files specified by path (which may be glob(7) patterns). If -F is present, then path is expanded as a format. If -q is given, no error will be returned if path does not exist. With -n, the file is parsed but no commands are executed. -v shows the parsed commands and line numbers if possible.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } }, "source": { "description": "```tmux\nsource-file [-Fnqv] path ...\nsource [-Fnqv] path ...\n```\n\nExecute commands from one or more files specified by path (which may be glob(7) patterns). If -F is present, then path is expanded as a format. If -q is given, no error will be returned if path does not exist. With -n, the file is parsed but no commands are executed. -v shows the parsed commands and line numbers if possible." @@ -367,9 +367,6 @@ "description": "```tmux\nunbind-key [-anq] [-T key-table] key\nunbind [-anq] [-T key-table] key\n```\n\nUnbind the command bound to key. -n and -T are the same as for bind-key. If -a is present, all key bindings are removed. The -q option prevents errors being returned." }, "set-option": { - "description": "```tmux\nset-option [-aFgopqsuUw] [-t target-pane] option value\nset [-aFgopqsuUw] [-t target-pane] option value\n```\n\nSet a pane option with -p, a window option with -w, a server option with -s, otherwise a session option. If the option is not a user option, -w or -s may be unnecessary - tmux will infer the type from the option name, assuming -w for pane options. If -g is given, the global session or window option is set." - }, - "set": { "description": "```tmux\nset-option [-aFgopqsuUw] [-t target-pane] option value\nset [-aFgopqsuUw] [-t target-pane] option value\n```\n\nSet a pane option with -p, a window option with -w, a server option with -s, otherwise a session option. If the option is not a user option, -w or -s may be unnecessary - tmux will infer the type from the option name, assuming -w for pane options. If -g is given, the global session or window option is set.", "properties": { "backspace": { @@ -991,8 +988,16 @@ "description": "```tmux\nset window-style style\n```\n\nSet the pane style. For how to specify style, see the \u201cSTYLES\u201d section.", "type": "string" } + }, + "patternProperties": { + "@[-_\\da-zA-Z]": { + "type": "string" + } } }, + "set": { + "description": "```tmux\nset-option [-aFgopqsuUw] [-t target-pane] option value\nset [-aFgopqsuUw] [-t target-pane] option value\n```\n\nSet a pane option with -p, a window option with -w, a server option with -s, otherwise a session option. If the option is not a user option, -w or -s may be unnecessary - tmux will infer the type from the option name, assuming -w for pane options. If -g is given, the global session or window option is set." + }, "show-options": { "description": "```tmux\nshow-options [-AgHpqsvw] [-t target-pane] [option]\nshow [-AgHpqsvw] [-t target-pane] [option]\n```\n\nShow the pane options (or a single option if option is provided) with -p, the window options with -w, the server options with -s, otherwise the session options. If the option is not a user option, -w or -s may be unnecessary - tmux will infer the type from the option name, assuming -w for pane options. Global session or window options are listed if -g is used. -v shows only the option value, not the name. If -q is set, no error will be returned if option is unset. -H includes hooks (omitted by default). -A includes options inherited from a parent set of options, such options are marked with an asterisk." }, diff --git a/src/tmux_language_server/finders.py b/src/tmux_language_server/finders.py index a7511dc..190d856 100644 --- a/src/tmux_language_server/finders.py +++ b/src/tmux_language_server/finders.py @@ -5,9 +5,10 @@ from dataclasses import dataclass from lsprotocol.types import DiagnosticSeverity -from tree_sitter_lsp.finders import ErrorFinder, QueryFinder +from tree_sitter_lsp.finders import ErrorFinder, QueryFinder, SchemaFinder -from .utils import get_query +from .schema import TmuxTrie +from .utils import get_query, get_schema @dataclass(init=False) @@ -30,6 +31,20 @@ def __init__( super().__init__(query, message, severity) +@dataclass(init=False) +class TmuxFinder(SchemaFinder): + r"""Tmuxfinder.""" + + def __init__(self) -> None: + r"""Init. + + :rtype: None + """ + self.validator = self.schema2validator(get_schema()) + self.cls = TmuxTrie + + DIAGNOSTICS_FINDER_CLASSES = [ ErrorFinder, + TmuxFinder, ] diff --git a/src/tmux_language_server/misc/__init__.py b/src/tmux_language_server/misc/__init__.py index 12a61b7..3ca057d 100644 --- a/src/tmux_language_server/misc/__init__.py +++ b/src/tmux_language_server/misc/__init__.py @@ -31,9 +31,7 @@ def get_schema() -> dict[str, Any]: f"`{project} --generate-schema={filetype}`." ), "type": "object", - "properties": { - "patternProperties": {"@[-_\\da-zA-Z]": {"type": "string"}} - }, + "properties": {}, } soup = get_soup("tmux.1", "groff", "mdoc") p = soup.find("p", string="CLIENTS AND SESSIONS") @@ -67,7 +65,7 @@ def get_schema() -> dict[str, Any]: continue if name == "backspace": isoption = 1 - schema["properties"]["set"]["properties"] = {} + schema["properties"]["set-option"]["properties"] = {} s = "" enum = [] if isoption: @@ -86,14 +84,14 @@ def get_schema() -> dict[str, Any]: description += "\n" + p.text.replace("\n", " ") description = description.replace("\u2212", "-") if isoption: - schema["properties"]["set"]["properties"][name] = { + schema["properties"]["set-option"]["properties"][name] = { "description": description, "type": _type, } if len(enum) > 1: - schema["properties"]["set"]["properties"][name]["enum"] = ( - enum - ) + schema["properties"]["set-option"]["properties"][name][ + "enum" + ] = enum if s in { "number", "height", @@ -102,7 +100,7 @@ def get_schema() -> dict[str, Any]: "time", "lines", }: - schema["properties"]["set"]["properties"][name][ + schema["properties"]["set-option"]["properties"][name][ "pattern" ] = "\\d+" else: @@ -112,4 +110,8 @@ def get_schema() -> dict[str, Any]: if name == "window-style": isoption = 0 p = p.find_next("p") + data = {"type": "array", "uniqueItems": True, "items": {"type": "string"}} + schema["properties"]["source-file"] |= data + data = {"patternProperties": {"@[-_\\da-zA-Z]": {"type": "string"}}} + schema["properties"]["set-option"] |= data return schema diff --git a/src/tmux_language_server/schema.py b/src/tmux_language_server/schema.py new file mode 100644 index 0000000..8e368aa --- /dev/null +++ b/src/tmux_language_server/schema.py @@ -0,0 +1,73 @@ +r"""Schema +========== +""" + +from dataclasses import dataclass + +from lsprotocol.types import Position, Range +from tree_sitter import Node +from tree_sitter_lsp import UNI +from tree_sitter_lsp.schema import Trie + +DIRECTIVES = { + "set_option_directive", + "source_file_directive", +} + + +@dataclass +class TmuxTrie(Trie): + r"""Tmux Trie.""" + + value: dict[str, "Trie"] | list["Trie"] | str # type: ignore + + @classmethod + def from_node(cls, node: Node, parent: "Trie | None") -> "Trie": + r"""From node. + + :param node: + :type node: Node + :param parent: + :type parent: Trie | None + :rtype: "Trie" + """ + if node.type == "value": + return cls( + UNI.node2range(node), parent, UNI.node2text(node).strip("'\"") + ) + if node.type == "file": + trie = cls(Range(Position(0, 0), Position(1, 0)), parent, {}) + for child in node.children: + if child.type not in DIRECTIVES: + continue + # directive name + _type = child.type.split("_directive")[0].replace("_", "-") + # add directive name to trie.value if it doesn't exist + _value: dict[str, Trie] = trie.value # type: ignore + if _type not in _value: + trie.value[_type] = cls( # type: ignore + UNI.node2range(child), + trie, + {} if _type != "source-file" else [], + ) + # the dictionary's key corresponding to directive name + subtrie: Trie = trie.value[_type] # type: ignore + # currently, only support set and source + # set is a dict, source is a list + value = subtrie.value # type: ignore + # fill subtrie.value + if child.type == "set_option_directive": + value: dict[str, Trie] + value[UNI.node2text(child.children[-2])] = cls.from_node( + child.children[-1], subtrie + ) + elif child.type == "source_file_directive": + value += [ # type: ignore + cls( + UNI.node2range(child.children[1]), + subtrie, + UNI.node2text(child.children[1]), + ) + ] + return trie + raise NotImplementedError(node.type) diff --git a/tests/tmux.conf b/tests/tmux.conf new file mode 100644 index 0000000..d1f359d --- /dev/null +++ b/tests/tmux.conf @@ -0,0 +1,2 @@ +set -g pane-base-index a +set -g set-titles not-on