Skip to content

Commit

Permalink
💥 Support full LSP features
Browse files Browse the repository at this point in the history
  • Loading branch information
Freed-Wu committed Feb 19, 2024
1 parent 4514164 commit 19557ca
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 82 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@

A language server for [tmux](https://github.com/tmux/tmux)'s tmux.conf.

- [ ] [Diagnostic](https://microsoft.github.io/language-server-protocol/specifications/specification-current#diagnostic)
- [ ] [Document Link](https://microsoft.github.io/language-server-protocol/specifications/specification-current#textDocument_documentLink)
- [x] [Diagnostic](https://microsoft.github.io/language-server-protocol/specifications/specification-current#diagnostic)
- [x] [Document Link](https://microsoft.github.io/language-server-protocol/specifications/specification-current#textDocument_documentLink)
- [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)

Expand Down
3 changes: 3 additions & 0 deletions requirements/colorize.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env -S pip install -r

tree-sitter-lsp[colorize]
3 changes: 3 additions & 0 deletions src/tmux_language_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
`importlib.metadata.version() <https://docs.python.org/3/library/importlib.metadata.html#distribution-versions>`_.
"""

from typing import Literal

try:
from ._version import __version__, __version_tuple__ # type: ignore
except ImportError: # for setuptools-generate
__version__ = "rolling"
__version_tuple__ = (0, 0, 0, __version__, "")

__all__ = ["__version__", "__version_tuple__"]
FILETYPE = Literal["tmux"]
27 changes: 26 additions & 1 deletion src/tmux_language_server/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ def get_parser():
default=2,
help="generated json's indent",
)
parser.add_argument(
"--check",
nargs="*",
default={},
help="check file's errors and warnings",
)
parser.add_argument(
"--color",
choices=["auto", "always", "never"],
default="auto",
help="when to display color, default: %(default)s",
)
parser.add_argument(
"--output-format",
choices=["json", "yaml", "toml"],
Expand All @@ -56,8 +68,12 @@ def main():
r"""Parse arguments and provide shell completions."""
args = get_parser().parse_args()

if args.generate_schema:
if args.generate_schema or args.check:
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

if args.generate_schema:
from .misc import get_schema
Expand All @@ -68,6 +84,15 @@ def main():
indent=args.indent,
)
return None
exit(
check(
args.check,
parser.parse,
DIAGNOSTICS_FINDER_CLASSES,
None,
args.color,
)
)

from .server import TmuxLanguageServer

Expand Down
1 change: 1 addition & 0 deletions src/tmux_language_server/assets/queries/import.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(path) @path
35 changes: 35 additions & 0 deletions src/tmux_language_server/finders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
r"""Finders
===========
"""

from dataclasses import dataclass

from lsprotocol.types import DiagnosticSeverity
from tree_sitter_lsp.finders import ErrorFinder, QueryFinder

from .utils import get_query


@dataclass(init=False)
class ImportTmuxFinder(QueryFinder):
r"""Import Tmux finder."""

def __init__(
self,
message: str = "{{uni.get_text()}}: found",
severity: DiagnosticSeverity = DiagnosticSeverity.Information,
):
r"""Init.
:param message:
:type message: str
:param severity:
:type severity: DiagnosticSeverity
"""
query = get_query("import")
super().__init__(query, message, severity)


DIAGNOSTICS_FINDER_CLASSES = [
ErrorFinder,
]
175 changes: 98 additions & 77 deletions src/tmux_language_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,32 @@
==========
"""

import re
from typing import Any

from lsprotocol.types import (
TEXT_DOCUMENT_COMPLETION,
TEXT_DOCUMENT_DID_CHANGE,
TEXT_DOCUMENT_DID_OPEN,
TEXT_DOCUMENT_DOCUMENT_LINK,
TEXT_DOCUMENT_HOVER,
CompletionItem,
CompletionItemKind,
CompletionList,
CompletionParams,
DidChangeTextDocumentParams,
DocumentLink,
DocumentLinkParams,
Hover,
MarkupContent,
MarkupKind,
Position,
Range,
TextDocumentPositionParams,
)
from pygls.server import LanguageServer
from tree_sitter_lsp.diagnose import get_diagnostics
from tree_sitter_lsp.finders import PositionFinder
from tree_sitter_tmux import parser

from .finders import DIAGNOSTICS_FINDER_CLASSES, ImportTmuxFinder
from .utils import get_schema


Expand All @@ -37,6 +44,39 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
:rtype: None
"""
super().__init__(*args, **kwargs)
self.trees = {}

@self.feature(TEXT_DOCUMENT_DID_OPEN)
@self.feature(TEXT_DOCUMENT_DID_CHANGE)
def did_change(params: DidChangeTextDocumentParams) -> None:
r"""Did change.
:param params:
:type params: DidChangeTextDocumentParams
:rtype: None
"""
document = self.workspace.get_document(params.text_document.uri)
self.trees[document.uri] = parser.parse(document.source.encode())
diagnostics = get_diagnostics(
document.uri,
self.trees[document.uri],
DIAGNOSTICS_FINDER_CLASSES,
"tmux",
)
self.publish_diagnostics(params.text_document.uri, diagnostics)

@self.feature(TEXT_DOCUMENT_DOCUMENT_LINK)
def document_link(params: DocumentLinkParams) -> list[DocumentLink]:
r"""Get document links.
:param params:
:type params: DocumentLinkParams
:rtype: list[DocumentLink]
"""
document = self.workspace.get_document(params.text_document.uri)
return ImportTmuxFinder().get_document_links(
document.uri, self.trees[document.uri]
)

@self.feature(TEXT_DOCUMENT_HOVER)
def hover(params: TextDocumentPositionParams) -> Hover | None:
Expand All @@ -46,17 +86,25 @@ def hover(params: TextDocumentPositionParams) -> Hover | None:
:type params: TextDocumentPositionParams
:rtype: Hover | None
"""
word, _range = self._cursor_word(
params.text_document.uri, params.position, True
document = self.workspace.get_document(params.text_document.uri)
uni = PositionFinder(params.position).find(
document.uri, self.trees[document.uri]
)
properties = get_schema().get("properties", {})
if _range.start.character != 0:
properties = properties.get("set", {}).get("properties", {})
description = properties.get(word, {}).get("description", "")
if not description:
if uni is None:
return None
text = uni.get_text()
result = None
if uni.node.range.start_point[1] == 0:
result = get_schema()["properties"].get(text)
elif uni.node.type == "option":
result = get_schema()["properties"]["set"]["properties"].get(
text
)
if result is None:
return None
return Hover(
MarkupContent(MarkupKind.Markdown, description), _range
MarkupContent(MarkupKind.Markdown, result["description"]),
uni.get_range(),
)

@self.feature(TEXT_DOCUMENT_COMPLETION)
Expand All @@ -67,72 +115,45 @@ def completions(params: CompletionParams) -> CompletionList:
:type params: CompletionParams
:rtype: CompletionList
"""
word, _range = self._cursor_word(
params.text_document.uri, params.position, False
document = self.workspace.get_document(params.text_document.uri)
uni = PositionFinder(params.position, right_equal=True).find(
document.uri, self.trees[document.uri]
)
properties = get_schema().get("properties", {})
kind = CompletionItemKind.Function
if _range.start.character != 0:
properties = properties.get("set", {}).get("properties", {})
kind = CompletionItemKind.Constant
items = [
CompletionItem(
x,
kind=kind,
documentation=MarkupContent(
MarkupKind.Markdown, property.get("description", "")
),
insert_text=x,
if uni is None:
return CompletionList(False, [])
text = uni.get_text()
if uni.node.range.start_point[1] == 0:
return CompletionList(
False,
[
CompletionItem(
x,
kind=CompletionItemKind.Keyword,
documentation=MarkupContent(
MarkupKind.Markdown, property["description"]
),
insert_text=x,
)
for x, property in get_schema()["properties"].items()
if x.startswith(text)
],
)
for x, property in properties.items()
if x.startswith(word)
]
return CompletionList(False, items)

def _cursor_line(self, uri: str, position: Position) -> str:
r"""Cursor line.
:param uri:
:type uri: str
:param position:
:type position: Position
:rtype: str
"""
document = self.workspace.get_document(uri)
return document.source.splitlines()[position.line]

def _cursor_word(
self,
uri: str,
position: Position,
include_all: bool = True,
regex: str = r"[\w-]+",
) -> tuple[str, Range]:
"""Cursor word.
:param self:
:param uri:
:type uri: str
:param position:
:type position: Position
:param include_all:
:type include_all: bool
:param regex:
:type regex: str
:rtype: tuple[str, Range]
"""
line = self._cursor_line(uri, position)
for m in re.finditer(regex, line):
if m.start() <= position.character <= m.end():
end = m.end() if include_all else position.character
return (
line[m.start() : end],
Range(
Position(position.line, m.start()),
Position(position.line, end),
),
elif uni.node.type == "option":
return CompletionList(
False,
[
CompletionItem(
x,
kind=CompletionItemKind.Variable,
documentation=MarkupContent(
MarkupKind.Markdown, property["description"]
),
insert_text=x,
)
for x, property in get_schema()["properties"]["set"][
"properties"
].items()
if x.startswith(text)
],
)
return (
"",
Range(Position(position.line, 0), Position(position.line, 0)),
)
return CompletionList(False, [])
35 changes: 33 additions & 2 deletions src/tmux_language_server/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,45 @@
import os
from typing import Any

from tree_sitter.binding import Query

from . import FILETYPE

SCHEMAS = {}
QUERIES = {}


def get_query(name: str, filetype: FILETYPE = "tmux") -> Query:
r"""Get query.
:param name:
:type name: str
:param filetype:
:type filetype: FILETYPE
:rtype: Query
"""
if name not in QUERIES:
with open(
os.path.join(
os.path.join(
os.path.join(os.path.dirname(__file__), "assets"),
"queries",
),
f"{name}{os.path.extsep}scm",
)
) as f:
text = f.read()
from tree_sitter_tmux import language

QUERIES[name] = language.query(text)
return QUERIES[name]


def get_schema(filetype: str = "tmux") -> dict[str, Any]:
def get_schema(filetype: FILETYPE = "tmux") -> dict[str, Any]:
r"""Get schema.
:param filetype:
:type filetype: str
:type filetype: FILETYPE
:rtype: dict[str, Any]
"""
if filetype not in SCHEMAS:
Expand Down

0 comments on commit 19557ca

Please sign in to comment.